March 2026: From Idea to Working CI#
Goal#
Build myci from scratch — a platform-agnostic, local-first CI
system in Rust. Get it to the point where it can execute
workflows locally and in Kubernetes pods, with the foundation
for future growth.
Key Results#
The ci-cache docs pipeline runs via
myci run --executor k8sForgejo push and PR triggers a
mycirun and reports status backA pipeline publishes a package to the Forgejo package registry
Sprint Overview#
Sprint 17: Dev and testing setup (done)
Sprint 18: Testing practices (done)
Sprint 19: Streamed build output in UI (done)
Sprint 20: Expand conditions support (done)
Support
on.push.branchesandtags
Sprint 21: Pull request events + deployment
on.pull_requestwebhook + trigger matchingCommit status links back to build detail page
Deploy to cluster, system-wide webhook in Forgejo
Sprint 21b: UI and server polish (done)
Pipeline as first-class entity, ANSI rendering, OIDC graceful degradation, code quality cleanup
Sprint 22: Credentials and package publishing
Expose forge token to pipeline steps
End-to-end: pipeline publishes package to Forgejo
Sprint 23 (stretch): Pages via git-pages
Deploy git-pages in cluster
Pipeline pushes built docs to git-pages
Sprint NEW: Focus on e2e testing and core improvement
TBD: Per-user forge tokens
TBD: Automatic webhook registration
TBD: Consolidation (CLI refactor, server config, pipeline dedup)
TBD: Configuration file for MyCI
Backlog#
[ ] Refactor
StepRunnerto return async streams instead of acceptingEventSender— see notes below for scope analysis. Also reduces parameter count on trait methods (currently 8 params onrun_node).[ ] Revisit
#[cfg(test)]cross-crate visibility: test constructors live inmyci-testbecause#[cfg(test)]items inmyci-domainare invisible to downstream crates. Track Cargo issue #8379 and consider atest-utilsfeature flag if themyci-testdependency becomes awkward (circular dev-dep).[ ] Arch: Extract domain logic into a dedicated crate (or expand
myci-domain).Pipeline,Trigger,Job,Step,globand expression evaluation are pure domain logic currently in themycicrate. A crate boundary would enforce that domain has no infrastructure dependencies. Trigger: when a second consumer of domain logic appears ormycicrate grows too large.[ ] Arch: Redesign expression
Contextas a typed environment (minijinja-style). Todaygithub.*,env.*,steps.*are allHashMap<String, String>with dotted-key hacks for nested access. A typed provider model would let each context namespace resolve its own lookups, handle nesting naturally, and make future additions (secrets.*,matrix.*,runner.*) self-contained.[ ] Configuration file for
myci(executor defaults, image defaults)[ ] Podspec-like configuration for k8s executor (resource limits, volumes, service account)
[ ] Workspace volume size configurable per runner (once runner configuration exists). Currently global via ~MYCI_WORKSPACE_SIZE~.
[ ] Git server binding in the case of ~myci run~ in a ~Pod~
[ ] Context handling may need a cleanup after the naive alias introduction
[ ] Need solution for e2e testing: Forge, MyCI server, in cluster, then test
Already needed, at least to a degree to test out the handling of events, and also the cache usage in the case of remote repos.
[ ] Should the aliases be set by default? This should probably be configurable.
[ ] Cache handling
Concurrency, e.g. action cache
Unification
Use something existing instead of implementing a solution
[ ] Executable documentation to be sure it is correct
[ ] Action cache invalidation
[ ] Automatic server start for browser E2E tests (subprocess or in-process)
[ ] Atomic action cache writes (clone to temp dir, then rename)
[ ] DRY shutdown/cleanup waiting logic (~myci run~ vs ~myci server~)
Paths are fundamentally different (single cleanup vs JoinSet drain), only revisit if a third entry point appears
[ ] ~kubectl logs~ only shows stdout, not stderr (tee limitation); evaluate if acceptable long-term
[ ] Use tracing or prefixed output for executor tee so pod/host origin is distinguishable
[ ] Create file command files (
GITHUB_OUTPUT,GITHUB_STATE), pass paths via env[ ]
uses:step support (checkout skipped for now, Node.js actions work)[ ] Matrix expansion
[ ] GitLab CI format plugin
[ ] Artifact storage
[ ] Caching
[ ] Pipeline format plugin system (subprocess-based)
[ ] K8s tee wrapper shell injection: evaluate if string interpolation for commands (ADR-0022) is a concern given workflow YAML comes from repo owners
[ ] Keep pod event watcher alive until pod deletion
Currently aborted after step execution; late events (OOM kills, evictions, cleanup failures) may be missed
Investigate if watcher can run until pod is actually deleted
[ ] Emit infrastructure lifecycle as distinct event types
K8s operations silenced today:
mkdir -pbefore tar (null sender), tar upload stderr (eprint!), pod/PVC deletion (3 sites, tracing only), chisel logs (only on failure)User sees nothing between “step started” and first log line
Small scope (~1-2h) but risks rework if StepRunner stream refactor happens — consider doing them together
Run-level infrastructure events needed (e.g. git server start) — current
InfraLogis job-scoped, needs a variant withoutjobfield
[ ] Resource monitoring for k8s runs (CPU, memory, disk over time)
Poll metrics API + kubelet stats during job execution
Display sparklines in build detail UI
See ~context/ideas/resource-monitoring.md~
[ ] Worker/agent split: decouple server from executor
Protocol design spike (claim API, event push, heartbeat)
Extract worker loop from in-process task to standalone binary
runs-on:as label-based routing key
[ ] Binary cache infrastructure for k8s Nix builds
Deploy attic or harmonia in exp cluster
No MyCI code changes, pure infrastructure
See Nix Builds
[ ]
secrets.*expression context and secret managementWorkflow-level
secrets:definitions, per-user secret storageSecrets registered in
SecretMaskerautomaticallyService-level forge token exposure in sprint 22
[ ] Unified event-based output rendering
Remove direct
println!/eprintln!from executors and orchestrationBoth
myci run(CLI) and server render output exclusively from the event stream, after secret maskingShared rendering layer that formats events per context (terminal vs SSE/JSON), with filtering and verbosity support
[X] Provide nix environment with all needed tools (git, chisel, …)
[X] Extract TestApp and test helpers to myci-test for use from myci-e2e
[X] test-with compile-time env check: cached builds skip gated tests even when var is set
[X] K8s tar upload broken pipe on action copy — quality image was missing tar and nodejs
[X] Evaluate
myci runqueue/worker patternServer queue implemented in sprint 16, CLI evaluation in sprint 17 (no benefit, keep direct execution)
[X]
myci-dev setup-oauthalso registers the forge webhook[X] Fix duplicate ADR numbers
[X] Node actions require Node in the user’s container image
Resolved by runner container architecture: Node actions execute in the runner container, user’s step container only runs shell steps
[X] Server CLI config cleanup (~ServerConfig~ structure and wiring)
Generic executor leak resolved by run queue in sprint 16
[X] Web UI in Rust via WASM
[X] Basic stdout handling: don’t break on
::workflow commands[X] Webhook receiver (server mode)
[X] Expression evaluation (
${{ }}syntax)[X] Forgejo integration (commit status reporting)
[X] UI: The runs overview should include a job or pipeline id if possible
[X] Build detail page (steps, duration, log snippets)
[X] Q: Running the server locally, should local runs also provide state into the web ui? - Yes, will do in Sprint 17.
Notes#
StepRunner async stream refactor (scope analysis)#
Currently EventSender (mpsc channel) is passed down through 29
functions: run() → run_workflows() → run_pipelines() →
run_jobs() → Executor::run_job() → execute_steps() →
StepRunner::run_shell/run_node/run_docker() → read_lines().
9 functions send events directly, 20 pass it through.
The clean architecture direction: step runners return an async stream of events instead of accepting a sender. The caller collects the stream and forwards it. The port produces output, it doesn’t know about event infrastructure.
Why it’s a full sprint:
All 3
StepRunnertrait methods +Executor::run_job()need new signatures3 implementations (host, k8s, dry_run) all need updating
read_lines()does dual duty (accumulateVec<LogLine>+ send events) — needs splittingK8s has concurrent streams (stdout + stderr + pod events) that must merge — stream combinators add complexity
Host executor has separate stdout/stderr pipes — same merging
execute_steps()sends its own events (StepStarted/Finished) wrapping the runner’s output — careful interleaving neededDry-run ignores the sender — would return empty streams
Current approach works correctly. This is about making the port direction cleaner: producers return data, they don’t push into infrastructure. Worth doing but not urgent.
Next scribble#
Forge integration architecture#
Research in context/notes/forge-ci-integration.md.
External CI systems (Woodpecker, Jenkins, etc.) all integrate the same way: OAuth + webhooks + commit status API. No proprietary runner protocol needed.
OAuth/OIDC serves two purposes:
User authentication — user logs into MyCI web UI via the forge as identity provider (redirect-based OIDC flow)
API access — MyCI gets a token to act on the forge: register webhooks, read repo files, post commit statuses
Webhooks are the trigger — the forge POSTs events (push, PR) to MyCI’s server endpoint.
Minimal server components:
HTTP endpoint to receive webhook payloads
OAuth2 client to authenticate with the forge
API client to post commit statuses back
Trigger
myci runon incoming events
Web UI layers on top: user authenticates via OAuth, selects repos to enable (auto-registers webhooks), follows build progress. This is the Woodpecker model — “log in with Forgejo, flip a switch, done.”
The server side is fully decoupled from the executor. It is
“receive event, run pipeline, report status.” The existing
myci run with host or k8s executor stays unchanged underneath.
Incremental forge integration plan#
OAuth and a web UI are not prerequisites for CI functionality. Everything can be built incrementally, each step independently useful.
Step 1: myci run with status reporting (no server)
myci run detects the current repo’s remote URL. Token from
environment (MYCI_FORGE_TOKEN) or config file. Posts commit
status (pending before run, success/failure after) via the forge’s
REST API. User triggers manually, from cron, or from a git hook.
New code: small HTTP client that POSTs to the commit status API. The status API is nearly identical across forges:
Forgejo/GitHub:
POST /repos/{owner}/{repo}/statuses/{sha}GitLab:
POST /projects/{id}/statuses/{sha}
Step 2: myci server receiving webhooks (minimal HTTP server)
Thin layer on top of step 1. Listens for webhook payloads, triggers the same pipeline run, posts status back. Webhook registration is manual initially (user adds the URL in forge settings).
System-wide integration: Forgejo and GitLab support system-level webhooks — one URL that fires for all repos on the instance. No per-repo setup needed.
Step 3: myci login with OAuth (smooth auth)
CLI-based OAuth login, no web UI needed. Two common flows:
Device flow: CLI shows URL + code, user authorizes in browser, CLI polls for token. Works over SSH.
Localhost redirect: CLI starts temp server on localhost, opens browser, captures redirect with token.
Token stored in config file. Replaces manual PAT creation.
Later: Web UI
Web UI is a user experience layer, not a functional prerequisite. User authenticates via OAuth, selects repos (auto-registers webhooks), follows build progress. The Woodpecker model.