CI alternatives and local Docker-based mirroring
CI alternatives and local Docker-based mirroring
Section titled “CI alternatives and local Docker-based mirroring”Research output. No workflow YAML or runner topology is changed by this document — it captures findings so we can decide how (or whether) to invest.
- Free-tier minutes are not our bottleneck.
ci.ymland the heavy ML lanes already run on self-hosted Linux (runner-contract). The only GitHub-hosted minutes we still pay for are documented exceptions:docs-deploy,docs-quality,link_checker,release-binaries(windows-latest/macos-latest),vox-vscode-extension, and twoubuntu-latestsmokes insideci.yml(github-hosted-exceptions). - Local-Docker-as-gate already exists in skeleton form.
vox ci pre-push(local-ci-pre-push) is the supported entry point. The fastest, lowest-risk improvement is to graftact(or Earthly) onto that hook, not to introduce a new CI engine. - Replacing GitHub Actions wholesale is not warranted. GitLab CI is already mirrored (workflow-enumeration). Forgejo Actions and Gitea Actions are GH-Actions-compatible drop-ins worth tracking but offer no decisive win over our current self-hosted fleet.
- The biggest unrealised speedup is Rust build caching, not the CI
provider:
sccache+mold+ a sharedtarget/cache delivers ~2–4× wall-clock improvements in our matrix; provider choice is downstream of that.
Current state (concise)
Section titled “Current state (concise)”| Layer | What we use today | Notes |
|---|---|---|
| Default runner | [self-hosted, linux, x64] | Free in minute terms; bottleneck is wall-clock + capacity. |
| Docker / Buildx jobs | [self-hosted, linux, x64, docker] | Used by mesh-compose-config, docker-vox-image-smoke, all-features-matrix. |
| Browser / Playwright | [self-hosted, linux, x64, browser] | Chromium pool. |
| GH-hosted exceptions | ubuntu-latest, windows-latest, macos-latest | 7 workflow surfaces; documented. |
| Local mirror | vox ci pre-push | Quick / default / full modes (~30 s / 2–4 min / 10–25 min). |
| Mirror | .gitlab-ci.yml | Job parity for guards, fmt/clippy/doc, coverage, tests. |
The architecture already separates “guard logic” (Rust binaries under
crates/vox-cli/src/commands/ci/) from “workflow YAML” (.github/workflows/).
That makes provider replacement cheap: any of the alternatives below can shell
into the same vox ci … commands.
Local Docker-based gating
Section titled “Local Docker-based gating”Option A — act (nektos/act)
Section titled “Option A — act (nektos/act)”act reads .github/workflows/*.yml and runs each job inside a Docker image
that mimics the GitHub-hosted runner image. It is the closest-to-native local
substitute for ubuntu-latest.
Fit for this repo:
- Strong fit for the GH-hosted exceptions (
docs-quality.yml,link_checker.yml,vox-vscode-extension,visualizer-ingest-smoke,web-vite-build-smoke). These are small, Linux-only, and well-suited to a catalog image. - Weak fit for self-hosted-labelled jobs.
actmatchesruns-on: ubuntu-latestbut not[self-hosted, linux, x64, docker]without--platformoverrides. The composite labels (docker,browser,gpu) would need explicit mapping flags in.actrc. - Cannot reproduce GPU, CUDA
nvcc, or real Chromium-with-display lanes faithfully — those stay on the self-hosted fleet.
Integration shape:
- Add an
act-pre-pushmode tovox ci pre-pushthat runs a subset of workflows by--workflowsfilter against the local Docker daemon. - Cache layers: bind-mount
~/.cache/act,~/.cargo, and a project-localtarget-act/socargo buildis not paid per-push. - Default to opt-in, not on by default —
vox ci pre-push --act— to avoid surprising contributors without Docker.
Cost model: zero $; ~3–8 min added to a typical pre-push for the exception lanes. Catches GH-hosted-only failures (e.g. Node/pnpm version mismatch) before they burn minutes.
Caveats:
act’s default catalog image lags GitHub’s image ~weeks. We have already pinned action major versions (actions/checkout@v6,actions/cache@v5) and require runner v2.327.1+ on self-hosted;actimages need the same spot-check.- Secrets handling:
actreads.secretsfiles; never commit them..secretsis in.gitignore. - Network egress in
actcontainers is unrestricted by default; tighten with--network nonefor guard-only lanes.
Windows support. act runs on Windows via WinGet, Scoop, Chocolatey, or
the gh act extension; Docker Desktop with the WSL 2 backend is the supported
daemon. Install + troubleshooting tables: local-ci-pre-push.md
§Installing act.
Both --bind (cargo cache mount) and --artifact-server-path may need
Windows-specific overrides; documented in the same section.
Option B — Earthly
Section titled “Option B — Earthly”Earthly compiles Earthfiles into Buildkit pipelines that run in Docker.
Native shared-cache (--push --cache-from), parallelism, and reproducible
builds. Mature Rust support (earthly/lib/rust).
Fit:
- Strong for release artifact builds and multi-target matrix lanes
(
all-features-matrix, the per-cratecargo check --all-featuresmatrix, the Docker image smoke). - Weak as a wholesale GH Actions replacement: it does not consume
.github/workflows/*.yml. We would maintain Earthfiles in parallel.
Integration shape: keep GitHub Actions as the trigger surface; have CI
jobs earthly --ci +all-features for the heavy matrices. Same Earthfile runs
locally with one command. Buildkit cache can be pushed to a registry
(GitHub Container Registry already authorised in our permissions: packages: write).
Cost model: zero $ if cache is local; modest GHCR storage if cache is shared between contributors.
Option C — Dagger
Section titled “Option C — Dagger”Dagger is “CI as code” — pipelines authored in Go/Python/TypeScript that run on a Buildkit engine inside Docker. More flexible than Earthly, more code than YAML.
Fit: overkill for our current shape. We already have the abstraction
boundary in vox ci; introducing Dagger duplicates that. Re-evaluate only if
we want pipeline introspection or branching dataflow.
Option D — Buildkit + scripts (lowest-ceremony)
Section titled “Option D — Buildkit + scripts (lowest-ceremony)”For the narrow goal “let me run the same Linux build locally before push,”
a single Dockerfile.ci + vox ci docker-mirror would cover ~80 % of the
need without adopting a new tool. Re-uses our existing self-hosted runner
image if we publish it to GHCR.
Alternatives to GitHub Actions (provider-level)
Section titled “Alternatives to GitHub Actions (provider-level)”Ranked by fit for this codebase:
1. Stay on GitHub Actions + self-hosted (recommended)
Section titled “1. Stay on GitHub Actions + self-hosted (recommended)”Already done. The architecture’s strength is provider-agnostic guard
logic in vox ci. As long as that holds, switching providers is a YAML
rewrite, not a logic rewrite. Investing in vox ci parity is higher ROI than
investing in a new provider.
2. Forgejo Actions / Gitea Actions
Section titled “2. Forgejo Actions / Gitea Actions”Both are open-source forge stacks that re-implement the GitHub Actions
runner protocol. actions/checkout, actions/cache, and actions/setup-*
all work. Self-hostable; no per-minute fee.
When to consider: if we ever want to leave GitHub for sovereignty or
cost reasons, this is the path with the lowest migration cost — workflows
move almost verbatim. Tracked dependency: third-party actions in our
workflows are dtolnay/rust-toolchain, taiki-e/install-action,
pnpm/action-setup, actions/setup-node, actions/cache,
actions/checkout, actions/upload-artifact — all JS or container actions
that run on Forgejo/Gitea Actions today.
When NOT to consider: purely as a free-tier escape; we already escaped via self-hosted.
3. Woodpecker CI
Section titled “3. Woodpecker CI”Drone fork. YAML pipelines, Docker-native, OSS. Lighter than GitLab CI, no Actions compatibility. Migration cost = full YAML rewrite.
Fit: none right now. Worth knowing it exists if we ever want a container-first OSS server we control.
4. GitLab CI
Section titled “4. GitLab CI”Already mirrored at .gitlab-ci.yml. The parity is intentional — drift is
caught by vox ci command-compliance. No action needed.
5. Faster GitHub-Actions-compatible runner clouds
Section titled “5. Faster GitHub-Actions-compatible runner clouds”For the GH-hosted exception workflows where ubuntu-latest minutes burn,
drop-in faster runners exist:
| Provider | Pricing model | Notes |
|---|---|---|
| BuildJet | per-minute, ~50 % of GH price for 2× CPU | Drop-in runs-on: buildjet-4vcpu-ubuntu-2204. |
| Blacksmith | per-minute, similar tier | Cache acceleration for Rust. |
| RunsOn | flat $/mo, AWS-backed | Best for high-volume; we don’t qualify. |
| Namespace.so | per-minute, fast cold start | Good for matrix fan-out. |
Verdict: not worth integrating today. Our GH-hosted minutes are low-volume (docs/release/vscode), and adding a vendor adds an audit surface for something measured in dollars per month.
6. CircleCI / Buildkite / Travis
Section titled “6. CircleCI / Buildkite / Travis”Not recommended. None offer a meaningful delta over our current setup, and all introduce a second source-of-truth for pipeline definitions.
Recommendations (priority order)
Section titled “Recommendations (priority order)”These are sequenced by “smallest diff with biggest signal” first.
- Adopt
actas an opt-in pre-push lane for the GH-hosted exception workflows only. Addvox ci pre-push --actthat runsdocs-quality,link_checker,visualizer-ingest-smoke,web-vite-build-smoke, andvox-vscode-extensionagainstactwith cached~/.cache/act+ bind-mounted~/.cargo. Catches the failures that today only surface after push. - Publish the self-hosted runner image to GHCR so contributors can
docker pull ghcr.io/<org>/vox-ci-runnerand reproduce the heavy self-hosted lanes locally. This is the lever for “run the real CI locally,” notact. - Audit
actimage drift quarterly if we adopt it — pin the catalog image SHA in.actrcand bump alongside ouractions/*major-version bumps. - Defer Earthly / Dagger. Re-evaluate if
all-features-matrixwall-clock becomes a sustained pain point — Earthly’s shared Buildkit cache is the strongest mitigation. - Track Forgejo Actions as a contingency only. No work needed today;
a
gh-actions↔forgejo-actionsparity check could be added tovox ci command-compliancelater if the contingency becomes real. - Do not adopt CircleCI / Buildkite / Travis / Drone / Woodpecker.
Non-goals
Section titled “Non-goals”- Replacing the self-hosted runner fleet — out of scope; orthogonal to provider choice.
- Removing the
.gitlab-ci.ymlmirror — the parity is a safety net, not duplication. - Auto-blocking commits on
actresults — pre-push is the right surface perAGENTS.md; pre-commit is too noisy for this codebase’s build times.
Open questions for the next iteration
Section titled “Open questions for the next iteration”- Does
actcorrectly handle--shell: pwshsteps (we have a few inci.yml)? Needs a one-shot smoke run before any contributor-facing rollout. - Can the self-hosted image be sliced into thinner variants (basic / docker / browser) so GHCR pulls are cheap? Today they’re one runner per capacity pool tag.
- Is
cargo-llvm-covreliable insideact’s nested-Docker image without privileged mode? If not, thetestslane stays self-hosted-only — acceptable.