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.yml or gaia-*.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:

  1. GAIA template scaffold (the body generated from project-config.yaml).
  2. user-steps.steps_before_gaia -- spliced BEFORE the managed steps block.
  3. GAIA-generated jobs unioned with user-jobs.yml entries (YAML merge into jobs:).
  4. 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