Configuring CI Pipelines
This tutorial explains how to set up continuous integration and delivery in GAIA. You will learn how the promotion chain works, which preset fits your team, how to generate workflow files, and how to avoid the most common configuration mistakes -- including running your test suite four times per change when once or twice would suffice.
Why CI configuration matters
A well-configured CI pipeline gives you fast feedback without waste. A poorly configured one runs the same tests on push, on PR open, on merge to staging, and again on merge to main -- burning 50+ minutes of compute per change. GAIA's CI configuration is designed to prevent that by making trigger strategy and test tiering explicit choices rather than afterthoughts.
All CI configuration lives in the ci_cd section of
.gaia/config/project-config.yaml. This is the single source of
truth that
/gaia-ci-setup
reads when it generates your workflow files.
The promotion chain
The promotion chain defines how code moves from development to production. It is a list of environments that a change must pass through, in order, before it reaches end users.
# .gaia/config/project-config.yaml
ci_cd:
platform: github-actions
promotion_chain:
- staging
- production
This two-stage chain means: merge to staging first, run
smoke tests, then promote to production. A three-stage
chain adds a dev environment before staging.
The promotion chain is what
/gaia-dev-story
uses to decide where to push, whether to create a PR, and which
environments to deploy to. If you skip configuring it, the dev-story
workflow cannot complete its push/PR/CI/merge steps.
Built-in presets
GAIA offers four CI presets. Each preset pre-fills the promotion
chain, trigger rules, and check configuration with sensible defaults
for a given team shape. You choose a preset during
/gaia-init or
change it later with
/gaia-ci-edit.
| Preset | Promotion chain | Triggers | Best for |
|---|---|---|---|
solo |
main | Push to main | Single developer, no staging env |
small-team |
staging → production | PR to staging, merge to staging | 2--5 developers, one staging env |
standard |
dev → staging → production | PR to dev, merge to staging | 5--20 developers, code review required |
enterprise |
dev → staging → pre-prod → production | PR, merge, promotion gates | Large teams, compliance requirements |
You can customize any preset after selection. The preset is a starting point, not a constraint.
Generating workflow files
Once your ci_cd section is configured, run:
/gaia-ci-setup
This reads your project-config.yaml and generates
platform-specific workflow files. For GitHub Actions, it writes files
under .github/workflows/. For GitLab CI, it writes
.gitlab-ci.yml. The generated files include the trigger
rules, test commands, and deployment steps that match your
configuration.
If you already have workflow files and want to regenerate them after
changing your config, use the --regenerate flag:
/gaia-ci-setup --regenerate
This backs up your existing files before overwriting, so you can diff the changes.
Adding custom steps
Generated workflow files are overwritten on regeneration. To add
custom steps that survive regeneration, place them in
*.user-steps.yml files alongside the generated workflows.
GAIA's templates include these files via an include mechanism so your
custom build steps, notifications, or deployment hooks persist across
regenerations.
# .github/workflows/ci.user-steps.yml
# These steps are included automatically by the generated workflow.
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
channel-id: 'C01234ABCDE'
slack-message: 'CI failed on ${{ github.ref }}'
Keep custom steps focused on side effects (notifications, artifact
uploads, cache management). Core test and deploy steps belong in your
project-config.yaml so GAIA can reason about them.
Trigger best practices
The single most impactful CI decision is when to run tests. The goal: run the full suite exactly once per logical change, at the point where it provides the most value.
Recommended: test on PR, smoke on merge
Run the full test suite when a PR is opened or updated. On merge to the target branch, run only a fast smoke test (or skip tests entirely if the PR passed). This avoids the duplicate-test problem.
# .gaia/config/project-config.yaml
ci_cd:
triggers:
pull_request:
checks:
- lint
- unit-tests
- integration-tests
- security-scan
push_to_staging:
checks:
- smoke-test
push_to_production:
checks:
- smoke-test
- deploy
Alternative: test on push, skip PR checks
For solo developers or trunk-based development, test on every push to main. There is no PR step to duplicate.
What NOT to do
Do not run the full suite on push AND on PR open AND on merge to staging AND on merge to main. This runs your tests four times per change. If your suite takes 15 minutes, that is 60 minutes of CI time for a single change.
Concurrency groups
When a developer pushes multiple commits to a PR branch in quick succession, each push triggers a new CI run. Without concurrency groups, all those runs execute to completion -- wasting time and compute.
Configure concurrency groups to cancel in-progress runs when a new commit arrives on the same branch:
# .gaia/config/project-config.yaml
ci_cd:
concurrency:
group: ci-${{ github.ref }}
cancel_in_progress: true
GAIA includes this in the generated workflow files when you use the
small-team, standard, or
enterprise presets.
Path-based filters
If your repository contains multiple stacks (for example, a frontend and a backend in the same repo), use path filters to run only the relevant tests when a change touches only one stack.
# .gaia/config/project-config.yaml
stacks:
- name: frontend
path: packages/web
test_command: npm test
- name: backend
path: packages/api
test_command: pytest
ci_cd:
path_filters:
enabled: true
When path filters are enabled,
/gaia-ci-setup
generates workflow triggers that match the path of each
stack. A change to packages/web/ triggers only the
frontend test job; a change to packages/api/ triggers
only the backend test job. Changes to shared configuration files
(like the project root) trigger both.
See Shape: Full-Stack for a complete example of path filters in a multi-stack repo.
Common mistakes
Avoid these CI anti-patterns
- The 4x test run. Running the full suite on push, PR open, merge to staging, and merge to main. Pick one primary trigger (PR is best for teams) and use smoke tests for the rest.
-
Missing concurrency cancellation. Without
cancel_in_progress: true, rapid pushes to a PR branch queue up redundant CI runs. The last push is the only one that matters. - Re-running tests on merge. If the PR passed all checks and the merge is a fast-forward (or the base branch has not changed), the merge commit will pass too. A fast smoke test confirms the deploy artifact is healthy.
- No path filters in a multi-stack repo. Every change triggers every test suite, even when only one stack was touched. This is the most common source of unnecessary CI time in full-stack repositories.
The CI customization layered model
GAIA exposes a layered model for customizing CI without forking the
framework templates. The model is built on a filename-prefix contract that splits
every .github/workflows/*.yml file into one of four classes:
gaia-*.yml-- generated by/gaia-config-ci --regenerate; rewritten on every regen.gaia-*.user-jobs.ymlorgaia-*.user-steps.yml-- overlay files YOU author; stitched into the managed workflow but never overwritten.user-*.yml-- entirely user-authored; GAIA NEVER touches.- no prefix -- migration-eligible; the auto-rename flow prompts you to pick.
Four-phase stitching order
When /gaia-config-ci --regenerate runs, the stitcher composes the workflow in
this fixed order:
- GAIA template scaffold (the body generated from
project-config.yaml). user-steps.steps_before_gaia-- spliced BEFORE the managed steps block.- GAIA-generated jobs unioned with
user-jobs.ymlentries (YAML merge intojobs:). user-steps.steps_after_gaia-- spliced AFTER the managed steps block.
Block-level edges only -- there are no per-step insert_after / insert_before
markers. If you need finer-grained control, that's a fork.
template_overrides: declarative section
For simple modifications (disable a job, bump a timeout, pin an adapter version), use the
declarative ci_cd.template_overrides: block in project-config.yaml:
ci_cd:
template_overrides:
disable: [shellcheck] # remove from generated jobs:
timeout_overrides:
bats-tests: 15 # override timeout-minutes
adapter_versions:
markdownlint: "0.41.0" # pin adapter version
Five security-critical jobs are protected from disable: -- you
cannot disable commitlint, attribution-guard,
no-claude-attribution, secrets-scan, or
credential-audit.
Auto-rename migration flow
On a project that predates the layered model whose .github/workflows/ contains files without the
gaia-* / user-* prefix, the first
/gaia-config-ci --regenerate prompts you per-file: rename to
gaia-{base}.yml + scaffold overlay stubs, rename to user-{base}.yml,
or skip and defer. The flow is backup-first -- a sha256-verified copy lives at
.gaia-backup/ci-regen-{timestamp}/ before any rename.
What to read next
- CI Scenarios by Team Size -- concrete configurations for solo, small-team, standard, and enterprise setups.
- Environments and Promotion -- how environments, secrets, and the promotion chain work together.
- Test Strategy Configuration -- how to configure which tests run and when.
/gaia-ci-setup-- full command reference./gaia-ci-edit-- editing the promotion chain after initial setup.