Automated Versioning and Deploy
GAIA automates the full release-to-deploy pipeline: derive a version from your commit history (or calendar, or a manual prompt), bump every version-carrying file in one atomic operation, then deploy affected components in dependency order with health gates between each stack. This tutorial walks through three release-strategy scenarios, a multi-stack ordered deploy with health gates, and a partial-deploy recovery scenario. It finishes with the affected-set data contract that ties CI, commit trailers, and the deploy pipeline together.
Prerequisites
You need a project-config.yaml with a
release section and at least one entry in
stacks. Read
Configuring CI Pipelines
and
Environments & Promotion
first if you have not set those up yet.
Overview
The release and deploy pipeline has two stages:
-
Release -- resolve the next version (using the
configured strategy), bump every file listed in
release.version_files[], commit, tag, push, and create a GitHub Release. The/gaia-releasecommand orchestrates this. -
Deploy -- after the release merge, the promotion
trigger fires, resolves which components changed (the
affected set), and dispatches per-environment deploys
in dependency order with health gates. The
/gaia-deploycommand orchestrates this.
The two stages are independent: you can run
/gaia-release without deploying (for libraries), or
run /gaia-deploy without a release (for manual
version management). When both are configured, the promotion trigger
chains them automatically after a merge to the promotion branch.
Release strategies
The release.strategy key in your
project-config.yaml controls how the next version is
determined. Three strategies are available:
| Strategy | How the version is derived | Best for |
|---|---|---|
conventional-commits |
Classifies commits since the last v* tag using
the Conventional Commits spec. feat = minor,
fix = patch,
BREAKING CHANGE or ! suffix = major.
The highest-precedence bump wins. When no qualifying commits
exist, the release exits cleanly with no version bump.
|
Libraries, APIs, frameworks that follow semver strictly. |
manual |
Prompts the user for the target version. No commit-derivation
is performed -- the user supplies patch,
minor, major, or an explicit
X.Y.Z.
|
Projects with non-standard versioning, or teams that want explicit control over every version number. |
calendar |
Derives a CalVer version from the current date:
YYYY.MM.PATCH where PATCH
auto-increments based on existing tags for the current month.
|
SaaS products, documentation sites, projects that ship on a regular cadence. |
When release.strategy is absent from your config, the
behavior defaults to manual.
Scenario 1: Conventional commits
Configuration
A Node.js API that follows Conventional Commits. The version lives
in package.json and a plain-text VERSION
file.
# .gaia/config/project-config.yaml
release:
strategy: conventional-commits
version_files:
- package.json
- VERSION
Walkthrough
-
Verify you are on
main./gaia-releaserefuses to run on any other branch. -
The strategy resolver runs.
It reads
release.strategy: conventional-commitsfrom config, classifies all commits since the lastv*tag, and emits a machine-readable result. -
Example: three commits since
v1.2.3.
The resolver classifies:fix(auth): handle expired tokens gracefully feat(api): add bulk-import endpoint fix(docs): correct API examplefix(patch),feat(minor),fix(patch). The highest-precedence bump is minor. -
Dry-run preview.
The version-bump script runs with
--dry-runfirst, printing the planned changes without writing:{ "old_version": "1.2.3", "new_version": "1.3.0", "bump_type": "minor", "bumped": [ { "file": "package.json", "format": "json", "old": "1.2.3", "new": "1.3.0" }, { "file": "VERSION", "format": "text", "old": "1.2.3", "new": "1.3.0" } ] } - Execute the bump. The script writes both files atomically -- either all listed files are bumped, or none are. On success, it emits the same JSON summary.
-
Commit, tag, push, GitHub Release.
/gaia-releasestages exactly the files from thebumped[]array, commits with a conventional message (chore(release): bump version to v1.3.0), creates an annotated tagv1.3.0, pushes both, and creates a GitHub Release.
No releasable changes
When all commits since the last tag are non-qualifying (e.g., only
chore: or docs: commits), the resolver
emits bump=none. The release exits cleanly (exit 0)
with a "no releasable changes" message. No version bump, no tag, no
release.
Scenario 2: Manual versioning
Configuration
A mobile app where the product team controls version numbers explicitly.
# .gaia/config/project-config.yaml
release:
strategy: manual
version_files:
- package.json
- ios/App/Info.plist
- android/app/build.gradle
Walkthrough
-
The strategy resolver emits
strategy=manual. No commit classification happens. -
You provide the bump specifier.
/gaia-releaseprompts you: supplypatch,minor,major, or an explicit version like2.0.0. -
Dry-run, then execute.
Same two-phase process as Scenario 1 -- preview with
--dry-run, then execute.{ "old_version": "1.4.2", "new_version": "2.0.0", "bump_type": "2.0.0", "bumped": [ { "file": "package.json", "format": "json", "old": "1.4.2", "new": "2.0.0" }, { "file": "ios/App/Info.plist", "format": "json", "old": "1.4.2", "new": "2.0.0" }, { "file": "android/app/build.gradle", "format": "json", "old": "1.4.2", "new": "2.0.0" } ] } - Commit, tag, push, GitHub Release. Identical to Scenario 1.
When to use manual
Choose manual when your version numbers carry
product-level meaning (e.g., marketing versions, app store
submission versions) that cannot be derived from commit messages
alone.
Scenario 3: Calendar versioning
Configuration
A SaaS platform that ships on a regular cadence and uses CalVer.
# .gaia/config/project-config.yaml
release:
strategy: calendar
version_files:
- plugin.json
- VERSION
Walkthrough
-
The strategy resolver derives the version from today's date.
Format:
YYYY.MM.PATCH. If no tags exist for the current month,PATCHstarts at0. Ifv2026.06.0already exists, the next version is2026.06.1. -
The version is passed directly to the bump script.
Unlike
conventional-commits, no commit classification is needed. The resolver emits the full version string.{ "old_version": "2026.05.3", "new_version": "2026.06.0", "bump_type": "2026.06.0", "bumped": [ { "file": "plugin.json", "format": "json", "old": "2026.05.3", "new": "2026.06.0" }, { "file": "VERSION", "format": "text", "old": "2026.05.3", "new": "2026.06.0" } ] } - Commit, tag, push, GitHub Release. Identical to the other strategies.
When to use calendar
Choose calendar when your users expect time-based
versions (e.g., documentation sites, SaaS platforms, internal
tools with regular release trains). The auto-incrementing
PATCH handles multiple releases within the same month.
Per-component deploy
When your project has multiple stacks, /gaia-deploy
deploys them in dependency order using the deploy_order
field on each stack. A health check runs after each stack deploys;
the next stack in order does not begin until the previous stack's
health check passes.
Two deploy modes are available:
| Mode | Behavior on health-check failure |
|---|---|
strict (default) |
Halt all downstream stacks immediately. No further deploys happen. Exit code 1. |
best-effort |
Mark the failing stack HOLD. Already-deployed
stacks stay live. Downstream dependents are marked
SKIPPED. Emit a PARTIAL-DEPLOY
verdict with a per-component status table. Exit code 3. |
Scenario 4: Ordered multi-stack deploy with health gates
Configuration
A microservices project with three stacks deployed in a specific order: the database migration runs first, then the API, then the frontend.
# .gaia/config/project-config.yaml
stacks:
- name: db-migrate
path: packages/db
language: typescript
deploy_order: 1
health_check:
command: "pg_isready -h localhost -p 5432"
timeout: 30
- name: api
path: packages/api
language: typescript
deploy_order: 2
health_check:
command: "curl -sf http://localhost:3000/health"
timeout: 60
post_deploy_smoke:
command: "npm run smoke:api"
timeout: 120
- name: web
path: packages/web
language: typescript
deploy_order: 3
health_check:
command: "curl -sf http://localhost:8080/health"
timeout: 30
post_deploy_smoke:
command: "npx playwright test --project=smoke"
timeout: 180
Deploy sequence (strict mode)
-
db-migrate (deploy_order: 1) deploys first. After
deploy, its health check runs:
pg_isready -h localhost -p 5432. Timeout: 30 seconds. -
api (deploy_order: 2) deploys next -- only after
db-migrate's health check passes. After deploy, its health check
runs:
curl -sf http://localhost:3000/health. If healthy, its smoke test runs:npm run smoke:api. - web (deploy_order: 3) deploys last. Health check, then smoke test.
What happens when a health check fails (strict mode)
If the api health check fails (non-zero exit or
timeout exceeded), all downstream stacks (web) are
skipped. The deploy exits with code 1 and a failure diagnostic.
The skill suggests consulting your rollback plan but does not
execute a rollback automatically.
Stacks without deploy_order
Stacks that do not set deploy_order are deployed
after all ordered stacks, in alphabetical order by
name. This ensures deterministic behavior without requiring every
stack to specify an order.
Scenario 5: Partial-deploy recovery (best-effort mode)
What best-effort mode does differently
In best-effort mode, a health-check failure does not
halt everything. Instead:
- The failing stack is marked
HOLD. - Already-deployed stacks remain live.
-
Downstream stacks that depend on the failing stack are marked
SKIPPED. -
The deploy emits a
PARTIAL-DEPLOYcomposite verdict and writes a per-component status table.
Example: api health check fails
Using the same three-stack configuration from Scenario 4, but with
--mode best-effort:
| Stack | Status | Explanation |
|---|---|---|
db-migrate |
DEPLOYED | Deployed and healthy. |
api |
HOLD | Deployed, but health check failed. Marked HOLD. |
web |
SKIPPED | Not deployed. Skipped because upstream (api)
is in HOLD. |
Composite verdict: PARTIAL-DEPLOY. Exit code: 3.
Version-manifest snapshot and crash recovery
Before the first component deploys, best-effort mode writes a version-manifest snapshot recording each component's pre-deploy version. If a crash or interrupt occurs mid-deploy, a restart reads the snapshot and resumes from the last incomplete component rather than redeploying components that already succeeded. On successful completion, the snapshot is archived.
Recovering from a HOLD
After diagnosing and fixing the failing stack:
- Fix the root cause (e.g., the
apihealth endpoint). - Re-run
/gaia-deploy --env staging --version v1.3.0 --mode best-effort. -
The crash-recovery logic reads the version manifest and resumes
from the
apistack, then continues toweb.
The affected-set data contract
The deploy pipeline needs to know which components changed in a given CI run. The affected-set resolver answers this question using a three-channel fallback chain. It always resolves to something -- the deploy pipeline never silently deploys nothing.
The resolver
(resolve-affected-set.sh) emits a JSON object naming
both the affected stacks and which resolution channel succeeded:
{"stacks":["api","web"],"channel":"ci-artifact"}
The channel field is one of: ci-artifact,
commit-trailer, or full-deploy. Consumers
can log this value for observability without parsing the resolution
logic themselves.
Channel 1: CI artifact (primary)
The selective-test pipeline's plan job writes a JSON
file and uploads it as a GitHub Actions artifact named
affected-set. The file is named
affected-set.json and contains a single
stacks array:
// Selective deploy -- only api and web changed
{"stacks":["api","web"]}
// Full deploy (escalation from stale dependency graph)
{"stacks":["*"]}
// Docs-only change -- no deploy needed
{"stacks":[]}
Rules:
-
stacksis a JSON array of strings. Each string is a stack name declared inproject-config.yamlunderstacks[].name. -
The wildcard sentinel
["*"]means "all stacks" (full deploy). -
An empty array
[]means "no stacks affected" -- no deploy needed.
The promotion trigger downloads this artifact and passes its path
to the resolver via --artifact <path>. If the
download fails (artifact expired, manual trigger), the resolver
transparently falls through to the next channel.
Channel 2: Commit trailer (secondary)
When the CI artifact is unavailable (manual deploy, workflow re-run, or cross-workflow trigger), the resolver falls back to parsing a commit trailer from the HEAD commit message. Two trailer names are accepted:
Affected-Set: ["api","worker"]
Affected-Components: ["web"]
The trailer value must be a JSON array of stack-name strings. The
resolver reads the first matching trailer (Affected-Set
preferred, then Affected-Components). Invalid or absent
trailers cause the resolver to fall through to the safety net.
When to use commit trailers
Commit trailers are useful when you need to manually control which components deploy -- for example, during a hotfix where CI did not run the full selective-test pipeline, or when deploying from a manually-triggered workflow.
Channel 3: Full-deploy fallback (safety net)
When neither the CI artifact nor a commit trailer is available, the resolver emits the full-deploy sentinel -- every component deploys. This guarantees that the deploy pipeline never silently deploys nothing.
When --config points to a valid
project-config.yaml, the resolver enumerates all
stacks[].name entries from the config and lists them
explicitly. Without a config file, it emits the wildcard sentinel
["*"].
// With config -- all named stacks
{"stacks":["db-migrate","api","web"],"channel":"full-deploy"}
// Without config -- wildcard
{"stacks":["*"],"channel":"full-deploy"}
Configuration reference
This section collects every configuration field covered in this
tutorial. All fields live in
.gaia/config/project-config.yaml.
Release configuration
# .gaia/config/project-config.yaml
release:
strategy: conventional-commits # or: manual | calendar (default: manual)
version_files:
- package.json # JSON with a top-level "version" key
- plugin.json # Also JSON
- VERSION # Plain-text file: bare semver string
| Field | Type | Default | Description |
|---|---|---|---|
release.strategy |
string | manual |
How the next version is derived:
conventional-commits, manual, or
calendar.
|
release.version_files |
string[] | (none) |
File paths relative to the project root. Each file is bumped
by the version-bump script. Supported formats: JSON with a
top-level "version" key, or a plain-text file
containing only a semver string.
|
Releasing: the auto-prepared release PR (and the token it needs)
On a push to the final promotion branch, release.yml
runs in prepare mode: it computes the next version,
bumps the version_files + CHANGELOG on a
release/vX.Y.Z branch, pushes that branch, and opens a
chore(release): vX.Y.Z pull request. Merging that PR
fires release.yml in publish mode,
which cuts the tag and the GitHub Release.
The release PR open requires a token that may not be the
default. The built-in GITHUB_TOKEN is blocked
from creating PRs unless the repository enables
Settings → Actions → General → Workflow permissions →
"Allow GitHub Actions to create and approve pull requests."
When that setting is off and no PAT is configured, prepare mode
still pushes the release/vX.Y.Z branch but
cannot open the PR — it writes a prominent job-summary
notice with a one-click "open PR" link, and a human must open + merge
it to ship. To make releases fully hands-off, configure one of:
- a
RELEASE_PATrepository secret (a PAT withreposcope) —release.ymlprefers it overGITHUB_TOKENand it works regardless of the org setting; or - enable the "Allow GitHub Actions to create and approve pull requests" workflow permission above.
Which command ships the artifact: release.yml vs /gaia-publish
For a project whose distribution.release_workflow is set
and whose channel is a path-based marketplace (the workflow itself
cuts the tag + Release), the release_workflow is
the publish — driven by the merged release PR above.
/gaia-publish is then the post-hoc
await-and-verify gate (pre-publish CI gate → confirm the
release-workflow outcome → post-publish registry probe → verdict), not
a second, competing publish trigger. For a direct registry channel
(npm, pypi, app stores) with no
release_workflow, /gaia-publish dispatches
the channel adapter directly. Never drive both as independent
publishers of the same version. /gaia-deploy correctly
HALTs for kind: branch-only environments and points you
to /gaia-publish.
Per-stack deploy configuration
# .gaia/config/project-config.yaml
stacks:
- name: api
path: packages/api
language: typescript
deploy_order: 2 # Lower deploys first; absent = after all ordered stacks (alpha)
health_check:
command: "curl -sf http://localhost:3000/health"
timeout: 60 # Seconds; default 30
post_deploy_smoke:
command: "npm run smoke:api"
timeout: 120 # Seconds; default 60
| Field | Type | Default | Description |
|---|---|---|---|
stacks[].deploy_order |
integer | (none) |
Controls the order in which stacks deploy. Lower values deploy
first. Stacks without deploy_order deploy after
all ordered stacks, in alphabetical order by name.
|
stacks[].health_check.command |
string | (none) | Shell command to execute after deploying the stack. Exit code 0 = healthy, non-zero = unhealthy. Must pass before the next stack in deploy_order begins. |
stacks[].health_check.timeout |
integer | 30 |
Maximum seconds to wait for the health-check command. Timeout exceeded = treated as failure. |
stacks[].post_deploy_smoke.command |
string | (none) | Shell command to execute as a post-deploy smoke test. Runs after the health check passes (or immediately after deploy when no health check is configured). |
stacks[].post_deploy_smoke.timeout |
integer | 60 |
Maximum seconds to wait for the smoke command. |
Validating your configuration
After editing release or deploy configuration, validate:
/gaia-config-validate
Invalid field types, unknown strategy values, or missing required fields produce a validation error with the exact JSONPath location. Fix the error and re-validate.
What to read next
- /gaia-release -- command reference for cutting a release.
- /gaia-deploy -- command reference for the five-phase deploy pipeline.
- /gaia-deploy-post -- post-deploy health and metric validation.
- Environments & Promotion -- promotion chains and environment configuration.
- Selective Test Execution -- the CI pipeline that produces the affected-set artifact.
- Configuring CI Pipelines -- the layered CI model and presets.