CI workflows
These workflows run only on the GAIA repo; .gaia/release-exclude keeps them out of the release bundle. Each workflow’s name: starts with GAIA: , and a CLI test checks that it does. The workflows that ship to every project (Tests, Chromatic, PR Conventions) are documented in Git hooks and conventions.
None of these workflows runs the pre-merge audit. The audit runs locally, through the Code Audit Team.
release.yml
Section titled “release.yml”Triggers on v*.*.* tag pushes. Builds and publishes the release bundle.
- Checkout at the tag with full history, and derive the version.
- Verify
.gaia/VERSIONmatches the tag, so a stray tag cannot publish. - Extract the CHANGELOG section for the release; fails when the heading is missing.
- Verify the manifest is up to date and the wiki was scrubbed for release.
- Verify the committed CLI bundles match a fresh build from
.gaia/cli/src. - Stage the release tree from
git ls-files, minus.gaia/release-exclude. - Bundle-time scrub: removes
gaia:maintainer-onlyblocks and checks that no contributor-only content leaked into the shipped files. - Verify runtime dependencies against the staged tree.
- Distribution test gate: runs the distribution tests against the staged tree. They check that the expected files are present, that the maintainer-only blocks were stripped cleanly, that the tree unpacks and installs with only Node and the base system tools on the path, and that the bundled
gaiaCLI still runs key project flows against it (for examplegaia init strip-branding). A failure stops the release before anything publishes. - Build
gaia-bundle-<tag>.tar.gzand its.sha256. Thegaia-bundle-prefix is deliberate. A 1.6.1/update-gaialooks forgaia-<tag>.tar.gz, so the new name makes it fail at the download instead of misreading the new folder layout. - Compose the release body, starting with the line that sends 1.6.1 projects to the 2.0.0 migration, then create the GitHub Release with both files.
The job holds a contents: write token and keeps it tight. The checkout does not save the token to git config, the CLI workspace install runs with --ignore-scripts so no package install script runs there, and of the later steps only Create GitHub Release receives the token.
cli-tests.yml
Section titled “cli-tests.yml”Its jobs run on pull requests to main and on workflow_dispatch (a run started by hand). The workflow also lists pushes to main as a trigger, but every job skips on a push. Three jobs:
- Vitest (
.gaia/cli) (required): for the CLI, typecheck, lint, and the Vitest tests. It also runs the bats tests that check GAIA’s readers of generated-region markers agree with each other, checks that the committed CLI bundles match a fresh build from source, runs the/gaia-fitnesslinter over GAIA’s own rules, skills, and other Claude files, checks that the label tables inwiki/concepts/GitHub Labels.mdmatch.gaia/labels.json(seegaia labels), and checks that the third-party skills copied into the repo (vendored skills) match the hashes recorded under.gaia/vendor. On a pull request, most steps run only when the change touches the files they cover; aworkflow_dispatchrun runs every step. - Distribution harness (advisory): the distribution tests that need neither Docker nor a token.
- Shipped-surface leak check (advisory): builds the release tree, then checks that no shipped file names a contributor-only path and that removing the maintainer-only blocks leaves no partial marker behind.
A commit pushed by a GitHub Actions bot, using the workflow’s own token, starts no pull-request run. The required Vitest (.gaia/cli) check then never appears, and the merge stays blocked. The fix is to start the workflow by hand, which takes write access to the GAIA repo. An outside contributor on a fork cannot, so mention it on the pull request instead. With write access, ask:
The required Vitest (.gaia/cli) check is missing on PR #<N>. Start the GAIA: CLI Tests workflow on its branch with workflow_dispatch and wait for it to report.audit-ci-tests.yml
Section titled “audit-ci-tests.yml”Like cli-tests.yml, its jobs run on pull requests to main and on workflow_dispatch, and skip on a push to main. Runs GAIA’s bats tests for the shipped scripts and hooks (.gaia/scripts/tests/, .github/audit/tests/, .gaia/tests/), split across parallel jobs (shards). It also runs the commitlint (opens in a new tab) tests and the tests for the RED gate (the check that a test was seen failing first) that need Node or Chromium. A final Audit CI Tests job passes only when every shard passes, so one required check covers them all. A bot-pushed commit leaves that check missing in the same way as Vitest (.gaia/cli), and the same by-hand run fixes it.
Projects get these scripts as code GAIA controls and never edit, so the tests only need to catch changes made on the GAIA repo.
shell-lint.yml
Section titled “shell-lint.yml”Triggers on pull requests to main. Two advisory jobs:
- Shellcheck: runs shellcheck over every tracked
*.shand*.batsfile through.gaia/tests/shell-lint.sh. - Bash 3.2 parse: on a macOS runner, checks that every tracked
*.shfile parses under the/bin/bash3.2 that ships with macOS, since GAIA’s shell scripts must run there.
forensics-triage.yml
Section titled “forensics-triage.yml”Triggers on issues opened or labeled with gaia-forensics, the label /gaia-forensics files with. Runs Claude Code to triage the report. It skips an issue already labeled gaia-triaged, then parses the report, classifies it, and checks whether the fix is in scope. For an in-scope fix it commits the fix on a new branch, runs the Quality Gate on it, and opens a draft pull request. A final step always adds the gaia-triaged label, however the run went.
The run is split into three jobs, so no job that runs the model or a model-edited file can write to GitHub:
triagehas read-only access. It runs both Claude Code steps (classify, then apply the fix) with project hooks turned off, and hands its outcome, including the fix commit, to the next jobs as an artifact.quality-gatehas read-only access and no secrets. It checks the fix commit’s paths again with its own copy of the scope check before checking the fix out, then runs the Quality Gate withpnpm install --ignore-scripts, so no install script runs on the edited files.publishis the only job that can write. It labels and comments on the issue and, when the gate passes, pushes the fix and opens the draft pull request. It never checks out the fix.
Its helpers live in .github/forensics/, all contributor-only. The scope check is a security boundary: it allows a short list of paths and denies everything else, including:
- anything under
.github/workflows/, so it cannot change its own workflow; - files that run on their own once the branch is checked out locally:
.claude/hooks/,.claude/settings.json,.claude/settings.local.json,.gaia/statusline/,.githooks/, and the CLI bundles.gaia/cli/gaiaand.gaia/cli/gaia-maintainer; - pnpm’s install-time files, matched by name at any depth:
package.json,pnpm-workspace.yaml,pnpm-lock.yaml,.npmrc,.pnpmfile.cjs, and.pnpmfile.mjs.
These matches ignore letter case, and a path with a .. segment or a non-ASCII character is rejected outright.
The pull request stays a draft until you mark it ready. Read its diff before you check the branch out. A local checkout runs the branch’s hooks and statusline in your session, and reading first means the scope check is not the only guard on them. Claude can show you the diff without checking anything out:
Show me the diff of draft PR #<N> with gh pr diff, without checking the branch out. Flag any change under .claude/hooks/, .claude/settings*.json, .gaia/statusline/, .githooks/, or to package.json, pnpm files, or .npmrc.