The workflow files are the source of truth for triggers and job settings. Image
publication is separate from deployment; these workflows do not deploy workloads.
Changes under .github/** remain owner-protected.
CI runs on every pull request targeting main and every push
to main, without path filters. Jobs check the exact PR head or push commit:
command-deadlines: command deadline and process-cleanup tests viamake test-timeouts(5 minutes).lint: Trunk checks with explicit base/head SHAs and check annotations (5 minutes).
Stack-specific checks run in their own workflows, filtered by path:
- Go: golangci-lint,
go test -race(sharded across two jobs:internal/tasks,internal/sessionsandinternal/workspacesin one, everything else in the other), generated-code drift, the MCP tool contract (make check-mcp-tools), and the prod image build fromserver/Dockerfile. The MCP contract is generated bygo generate ./internal/mcpserverand checked bymake check-mcp-tools. Go nightly reruns the full suite withgo test -race -count=1 ./...once a day as a backstop against a stale cached result, since the PR jobs above cache Go build and module state across runs. - Web:
tsc, ESLint, Vitest, the Vite build, design checks, API and theme drift, the static server test, and theweb/Dockerfilebuild. - Contract: Redocly lint of
api/openapi.yaml.
CI uses ubuntu-latest, read-only contents/actions permissions by default, and
check-write permission for lint. It publishes no images or job outputs.
Mainloop dev image runs on PRs targeting main and
pushes to main only when one of these paths changes (no manual trigger):
.devcontainer/**,.trunk/**,scripts/with-timeout.mjspackage.json,pnpm-lock.yaml,pnpm-workspace.yaml,.npmrc.github/workflows/dev-image.yml
PR-only validate builds .devcontainer/Dockerfile without pushing, separately
for linux/amd64 on ubuntu-latest and linux/arm64 on ubuntu-24.04-arm
(10 minutes each). Main pushes instead run publish-amd64 and publish-arm64
on those native runners (10 minutes each). Each publishes
ghcr.io/oldsj/mainloop-dev by digest and exposes a digest job output.
The dependent merge job (3 minutes, ubuntu-latest) combines both digests into
a multiarchitecture index with tags <full commit SHA>, sha-<full commit SHA>
and latest. It validates and exposes the index digest, and records the source,
tags and immutable image reference in the job summary. This workspace image is
distinct from the application images below.
The workspace cache image (images/workspace-cache) is no longer built by GitHub Actions.
It is built in the cluster that runs the workspaces, by an Argo Workflows CronWorkflow on
BuildKit, and published to the cluster's own OCI registry. The workflow and the BuildKit
service live in the infrastructure repository (k8s/apps/ci, k8s/apps/buildkit).
Workspace nodes then pull the image over the LAN instead of from ghcr, which was too slow
from the cluster to start workspaces on a new digest (#274).
Every 30 minutes the workflow clones main and builds the dev image
(.devcontainer/Dockerfile) when its inputs change, then the cache image when its inputs
(images/workspace-cache/**, server/go.mod, server/go.sum, the pnpm manifests and
the dev image digest) or the UTC date change, so it rebuilds once a day otherwise. Builds
run natively on an arm64 node and push zstd-compressed OCI layers, which a node unpacks
several times faster than gzip.
A smoke step runs the new image by digest on the workspace node: a fresh clone at the
commit must build with zero compiles (go build and test binaries) and install the pnpm
workspace offline, with each of GOMODCACHE, GOCACHE, npm_config_store_dir and
npm_config_virtual_store_dir under /opt/cache. Only then does cache-main move, and
only if the commit is still the head of main. Because the smoke step runs on the
workspace node, the node already holds the image when environments start following it.
The tags (cache-<sha>, one per build) are not pruned yet.
Publish images runs on all pushes to main and manual
workflow_dispatch, without path filters. Its publish matrix builds the Go server
(server/Dockerfile, prod target) and the React app (web/Dockerfile) on
ubuntu-24.04-arm, publishing only linux/arm64 images (15 minutes each):
ghcr.io/oldsj/mainloop-backend:<first seven commit SHA characters>(the Go server; the image keeps its historical name because the production manifests pin it)ghcr.io/oldsj/mainloop-frontend:<first seven commit SHA characters>
The frontend uses the workflow's production API build argument. There is no
latest tag or job-level output. Step outputs provide the short SHA and build
digest; summaries record source, tag, digest and an immutable infrastructure pin.
The image jobs are not restricted to main, but the release job is: it runs only on
main, and only while its commit is still the tip of main. It force-pushes the
release branch and a release-<sha7> tag with the digests pinned in the prod overlay.
Argo tracks release; no infrastructure PR is needed per release.
Publish live dev images runs on pushes to main
and manual workflow_dispatch, without path filters. Its publish matrix is
explicitly restricted to refs/heads/main, including manual runs. It builds the
dev target of the Go server and the React app on ubuntu-24.04-arm, publishing
only linux/arm64 images (15 minutes each):
ghcr.io/oldsj/mainloop-server-devghcr.io/oldsj/mainloop-web-dev
Both receive <first seven commit SHA characters> and main tags; main is the
mutable one, and the shared mainloop-dev overlay pins it. Step outputs
provide the short SHA and digest; summaries record source, tags and digest, with no
job-level output. These are application dev targets, not the multiarchitecture
workspace image. Publishing them does not start or update the shared instance.
Each workflow cancels in-progress runs in its own workflow/ref concurrency group
(dev-image uses dev-image-${{ github.ref }}). Matrix jobs disable fail-fast so
independent builds can finish. Publication jobs have package-write and contents-read
permissions; workspace PR validation has contents-read only. Builds use GitHub
Actions caches separated by image or architecture. The job timeouts above are
limits, not measured runtimes or qualification claims.