CI alternatives and local Docker-based mirroring
Update (2026-06-03): The parallel GitLab CI mirror (
.gitlab-ci.yml) has been retired and deleted — GitLab CI is no longer a supported target. The notes below that described it as “already mirrored” / “a safety net” are kept for historical context but no longer reflect the repo; GitHub Actions (.github/workflows/) is the sole CI surface.Update (2026-09-22): The self-hosted fleet is also retired (hosted-primary migration; Tasks 9/10/14 of
docs/superpowers/plans/2026-09-22-ci-audit-remediation.md). Every workflow now runs on GitHub-hosted runners. This page was written while the fleet was the default, and its comparisons still assume it. The conclusions survive — hosted minutes are free for a public repo, guard logic lives invox ciso provider choice is a YAML rewrite, and Rust build caching is the real lever — but read every “self-hosted” / “local fleet” mention below as describing the pre-2026-09 setup, not today’s.
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. GitHub-hosted minutes are free for a public repo like vox — see runner-contract for the current runner assignment.
- 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 was previously kept as a parallel mirror but was removed on 2026-06-03 — the parallel pipeline was not worth maintaining. 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 | ubuntu-latest (plus windows-latest / macos-latest where a job needs them) | Free minutes: vox is public. The only self-hosted labels left are two starved GPU lanes; see runner contract §Runners. |
| PR gate | ci.yml — linux + ui under the gate aggregator | 30-min cap; advisory windows leg in the merge queue. |
| Slow lanes | nightly.yml and the other scheduled workflows | 180-min cap. |
| Local mirror | vox ci pre-push | Fast / --complete / --full tiers. |
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; the supported daemon is the WSL2-native Docker Engine
(docker-ce installed inside the WSL2 distro — Docker Desktop is not used on this
host). No special invocation is
needed: act picks up the active docker-wsl SSH context like any other Docker
client. 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.
macOS support. act installs via Homebrew (brew install act). As on Windows,
Docker Desktop is not used — the supported daemon is colima
(brew install colima), which exposes a normal Docker socket that act picks up
through its own colima context with no special invocation.
Size the VM for the host and enable Rosetta — .actrc pins
--container-architecture linux/amd64 so local runs match the amd64
GitHub-hosted runners, and without Rosetta those containers fall back to slow QEMU
emulation:
colima start --vm-type vz --vz-rosetta --cpu 12 --memory 48 --disk 100colima defaults to 2 CPU / 2 GiB, which is far too small for a Rust workspace this
size; scale the flags to the machine (leave the host several cores and enough RAM
for a native cargo build alongside). Verify with colima list and
docker info --format '{{.NCPU}} {{.MemTotal}}', and confirm emulation works with
docker run --rm --platform linux/amd64 alpine uname -m (expect x86_64).
Apple Silicon caveat: the pinned catthehacker/ubuntu:act-* images publish
linux/arm64/v8 as well as linux/amd64. Dropping the architecture pin makes act
silently use the arm64 variant — faster, but it exercises a different toolchain
than CI, the same class of false green as mapping windows-latest onto a Linux
image.
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”Retired (2026-06-03). Vox previously maintained a .gitlab-ci.yml mirror
for job parity; it was deleted because keeping a parallel pipeline alongside
GitHub Actions was not worth the upkeep, and GitLab CI is no longer a supported
target here. It remains a viable alternative provider in the abstract should
we ever migrate, but re-adopting it would now be a full from-scratch port, not a
re-sync — not recommended.
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.Obsolete (2026-09): the runner image and its scripts were deleted with the fleet. With every job onubuntu-latest,act’s catalog image is the “run the real CI locally” lever.- 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”- Runner-fleet topology — orthogonal to provider choice (and moot since the fleet’s 2026-09 retirement).
- 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.