One rig per backend, each under its own folder, because the rigs are not uniform:
- Self-hostable backends (
forgejo/) run a disposable container (plus an Actions runner) —compose.yaml+up.sh/down.sh. - SaaS backends (
github/,gitlab/,jira/) can't be containerized; their rigs run against a real throwaway repo/project named in a gitignored.env, tag everything they create ([rig],rig/*), and tear only that down — nocompose.yaml.
What every rig has in common is the output: up.sh leaves a .work/ directory — a throwaway
git repo holding .flightdirector/config.json + .flightdirector/secrets.json pointed at the rig — which the
adapters then run against. .work/ is gitignored for every backend.
test-rig/
forgejo/ compose.yaml up.sh down.sh smoke.sh workflows/ README.md (container + runner)
github/ up.sh down.sh smoke.sh workflows/ README.md (real repo, no compose)
gitlab/ up.sh down.sh smoke.sh gitlab-ci*.yml README.md (real project)
jira/ up.sh down.sh smoke.sh README.md (issues axis only)
Scripts that sit at this level are backend-free — they need no up.sh, no container, and no
token, so they run anywhere:
smoke-worktree-anchor.sh— regression test for the nested-worktree bug (#57). Builds a throwaway repo, proves a cwd-relativeworktree addnests under the previous worktree, and proves theROOT=idiom extracted fromworking-an-issue/SKILL.mddefeats it. Because it greps the idiom out of the shipped skill, it fails if that snippet drifts.
The adapter contract is backend-agnostic, so the smoke.sh assertions are largely the same
across rigs — only provisioning differs. All three code rigs now carry the same ci watch /
ci log section (#138, #186); extracting the shared assertions up to this level, driven by each
backend's up/down, is the obvious next step and has not been done yet.
The rigs workflow (.github/workflows/rigs.yml) runs them on the forge: Actions → rigs →
Run workflow, with the input all, or a space-separated subset (github gitlab).
- Manual only (
workflow_dispatch). Each rig waits minutes on real CI and writes to a shared throwaway repo, so it must not fire on every push. Running them is instead part of cutting a release, andscripts/tag-release.shprints the reminder. - Secrets. The SaaS rigs read
FLIGHT_GH_REPO/FLIGHT_GH_TOKENandFLIGHT_GITLAB_TOKEN/_API/_PROJECTfrom the repo's Actions secrets — scoped to the throwaway test repos only, and they expire, so check the dates when a rig starts failing on auth. Missing secrets skip on the public GitHub mirror — this workflow is mirrored there and those secrets never will be — but fail anywhere else, because on the forge that owns them their absence means a rig did not run, and that must not report green. An expired token is caught either way: the secret is there, so the rig runs and dies on a real 401. (FLIGHT_GH_APIis only needed for GitHub Enterprise Server.) RIG_STRICT=1. Locally, a rig that cannot reach a CI verdict — no runner, or CI too slow — warns and carries on. Unattended, a warning nobody reads is a silent skip, so the workflow setsRIG_STRICT=1and those cases fail the job instead.up.sh/smoke.shhonour it anywhere.- Teardown always runs, so a red smoke test cannot leave rig branches and open PRs behind.
- One run at a time. The workflow takes a
live-test-rigsconcurrency group, because everydown.shdeletes allrig/*branches and closes all[rig]issues/PRs — two concurrent runs would tear down each other's work. That cannot protect against someone running a rig locally while CI runs: the two would collide the same way, so don't. - The forge token has to be unshadowed. A Forgejo Actions job is handed
FORGEJO_TOKENin its environment, and the dispatcher resolvesLS_TOKEN/FLIGHT_TOKEN/FORGEJO_TOKENahead of the repo'ssecrets.jsonwithout reporting which won (#177) — so a rig would authenticate to GitHub or GitLab with a Forgejo token and 401 on every call, while its ownup.sh(which uses its own variable and curl directly) provisions happily. Each rig step thereforeunsets the three names first. Remove that once #177 lands. - The Forgejo rig drives the runner host's docker, since it brings up its own instance and
Actions runner with compose. This forge runs jobs in a container that has the docker socket
but no docker CLI, so that job runs without a
container:of its own and installs a pinned, checksummed client (plus compose andjq— it cannot assume the image has them). The daemon it talks to is the host's, which has two consequences worth knowing:- the rig's containers are siblings on the runner host, not children of the job, so a run
that dies before teardown leaves
flight-rig-*containers there; the job clears them before it starts, and tears down in analways()step; - the published port lands on the host, so
localhostinside the job is the wrong address. The job passesRIG_HOST(its default gateway, read from/proc/net/route— it is not the same address every run) andRIG_PORT=3737. The compose network is unaffected: the runner container still reaches Forgejo ashttp://forgejo:3000.
- the rig's containers are siblings on the runner host, not children of the job, so a run
that dies before teardown leaves