| name | gaia-release |
| description | Cut a GAIA release end-to-end: draft notes, open release PR, run pre-tag verification, push the tag, monitor the publish pipeline, and produce the Discord announcement. Use when the user asks to 'cut a release', 'release vX.Y.Z', 'tag a release', or 'publish v...'. Pauses at every irreversible step for user approval. |
GAIA Release
Run a GAIA release end-to-end against the amd/gaia repo. The skill is a phased checklist with hard gates — each phase produces a concrete artifact, then stops and asks for confirmation before doing anything irreversible (opening a PR, pushing a tag, rerunning a CI job, posting an announcement).
The pre-tag verification phase exists because the v0.17.4 release uncovered two release-blocking bugs that merged-PR CI did not catch (squash-merge silently reverting version.py, and the AppImage smoke test's circular pip install amd-gaia==<unpublished> dependency). Do not skip it.
Argument parsing
Accept one argument in any of these shapes:
0.17.5 — bare semver
v0.17.5 — with the v prefix (the actual tag form)
0.17.5.1 — hotfix-style four-part version (rare; e.g. v0.15.4.1)
- (no argument) — read
__version__ from src/gaia/version.py, then suggest the next-patch bump (e.g. 0.17.4 → propose 0.17.5) and ask the user to confirm or override before continuing.
Normalise to the bare form internally (0.17.5) and the tag form externally (v0.17.5). If the user gives a version that is older than or equal to the current __version__, stop and ask — never silently roll backwards.
Resume detection (run before any phase)
The skill is reinvocation-safe — releases span days, and the user will return mid-flow. Before doing anything else, detect where in the flow we are and skip phases that already landed:
git fetch origin --tags
git checkout main && git pull
NOTES_ON_MAIN=$(git ls-tree -r origin/main --name-only | grep -c "^docs/releases/v<version>\.mdx$" || true)
PR_OPEN=$(gh pr list --repo amd/gaia --search "Release v<version> in:title" --state open --json number --jq '.[0].number' || true)
PR_MERGED=$(gh pr list --repo amd/gaia --search "Release v<version> in:title" --state merged --json number --jq '.[0].number' || true)
TAG_EXISTS=$(git tag --list "v<version>")
RELEASE_EXISTS=$(gh release view "v<version>" --repo amd/gaia --json tagName --jq '.tagName' 2>/dev/null || true)
Resume table:
| State | Action |
|---|
RELEASE_EXISTS matches | Release already shipped. Skip to Phase 6 (smoke test + announcement) only. |
TAG_EXISTS, no release | Tag pushed, publish workflow in progress or failed. Resume at Phase 5 (monitor). |
PR_MERGED, no tag | Notes on main. Resume at Phase 3 (pre-tag verification). Do not re-run Phase 1 — never overwrite merged notes. |
PR_OPEN | PR still open. Tell user "PR #N already open at — waiting for merge." Exit. |
NOTES_ON_MAIN ≥ 1 but no PR | Half-finished prior attempt landed notes without a PR (rare). Stop and ask the user before continuing. |
| Nothing matches | Fresh release. Start at Phase 1. |
Always announce the resume decision before continuing: "Detected v<version> state: PR #831 merged, no tag yet. Resuming at Phase 3 (pre-tag verification)."
Hard rules (do not violate)
These map to CLAUDE.md. Re-read them whenever this skill runs.
- Plain language first, in every artifact — the release notes, PR body, Discord post, and your own between-phase status updates all follow CLAUDE.md → How You Communicate: lead with what a user can now do, layer version numbers, SHAs, and pipeline mechanics underneath. Say each point once — don't restate a highlight in the changelog and again in the announcement.
- No Claude attribution anywhere — not in PR titles, PR bodies, commit messages (no
Co-Authored-By: Claude ... trailer), release notes, code comments, or the Discord announcement.
- No silent fallbacks — if a validator fails, a step times out, or a workflow run isn't found, stop with an actionable error. Do not retry blindly. Do not "proceed anyway."
- Release notes are bulleted, plain, and short — see Notes format in Phase 1. Every entry is one bullet, never a prose block; no narrative overview paragraph; no emoji; a hard word budget that is checked, not eyeballed. Sections in order:
## Breaking Changes (only if any), ## What's New, ## Bug Fixes, ## Known Issues (only if any), ## Contributors, ## Full Changelog. Patch releases do not include a pip install block.
- Match the previous release PR body shape exactly — read the most recent merged
Release vX.Y.Z PR (e.g. gh pr list --repo amd/gaia --state merged --search "Release v in:title" --limit 3). Open with # GAIA vX.Y.Z Release Notes (no MDX frontmatter in the PR body), end with a Release checklist section. Style drift here costs review cycles.
- The
version.py bump and its notes land in the same commit. docs.yml and tests/unit/test_docs_json_release.py both resolve docs/releases/v<version>.mdx and the docs.json navbar label from the declared __version__, so bumping ahead of the notes (the old 0.17.3 → "bump to 0.17.4 for development" pattern) turns every later docs and unit run red, not just the release PR. Never bump on its own.
- Bulletproof commits only — every change made by this skill must satisfy the four criteria in CLAUDE.md (validated, critiqued, scope-clean, no half-finished work) before being committed.
Phase 1 — Draft release notes (PR-ready)
Goal: produce docs/releases/v<version>.mdx matching house style; update navigation and the UI package.json.
Steps
-
Survey commits since previous tag, then sanity-check the version request against scope.
PREV=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo "Previous tag: $PREV"
git log "$PREV..HEAD" --pretty=format:'%h %s'
echo
echo "Total: $(git log $PREV..HEAD --oneline | wc -l) commits"
echo "Feat: $(git log $PREV..HEAD --oneline | grep -cE '^[a-f0-9]+ feat') feat commits"
echo "Fix: $(git log $PREV..HEAD --oneline | grep -cE '^[a-f0-9]+ fix') fix commits"
Group commits by theme (features, fixes, infra, docs). Extract the PR number from each subject ((#NNN)). For each non-trivial entry, open the linked PR or commit body to get the why — release notes need motivation, not just titles.
Scope-vs-version sanity check (do not skip): apply this rubric against the requested target version:
| Commit shape | Suggested release shape |
|---|
Only fix/docs/chore/ci, no feat | Patch (vX.Y.Z+1) ✓ |
1–2 feat commits, small scope | Patch acceptable, mention them as "What's New" |
3+ feat commits, or any commit titled ... vX.Y.Z milestone, default-model swap, breaking change, package layout change | Minor (vX.Y+1.0) — push back |
| Removal of a public API, CLI flag deletion, config schema break, version-pin floor raised in a non-additive way | Major () — push back |
Gate 1 — show the user the draft
Show the diff (git diff --stat plus the new .mdx file inline). Ask: "Approve these release notes and continue to Phase 2 (open PR)?" Wait for explicit yes. Iterate on tone/wording before continuing — much cheaper than fixing on main.
Phase 2 — Open the release PR
Goal: branch, commit, push, open PR, request review.
Steps
-
Branch and commit.
git checkout -b v<version>-release
git add docs/releases/v<version>.mdx docs/docs.json src/gaia/version.py \
src/gaia/apps/webui/package.json src/gaia/apps/webui/package-lock.json \
hub/components/terminal-hub/gaia-agent.yaml \
hub/components/agent-ui/gaia-agent.yaml
git status
git diff --cached --stat
git commit -m "Release v<version>"
git push -u origin v<version>-release
If git status shows anything outside those five files, stop and ask (bump-ui-version.mjs rewrites package-lock.json too — the prior release commit carries all five). The release PR must be scope-clean.
-
Read the most recent release PR body to match shape.
gh pr list --repo amd/gaia --state merged --search "Release v in:title" --limit 3 \
--json number,title,body | jq -r '.[0]'
Use that as the structural template for this PR body — same opening, same checklist, same section order. Do not invent a new shape.
-
Prepare the PR body file. Build it by stripping the MDX frontmatter from the release notes and appending the Release checklist section copied from the previous release PR.
awk '/^---$/{c++; next} c>=2' docs/releases/v<version>.mdx > /tmp/release-body.md
PREV_PR=$(gh pr list --repo amd/gaia --state merged --search "Release v in:title" --limit 1 --json number --jq '.[0].number')
gh pr view "$PREV_PR" --repo amd/gaia --json body --jq '.body' \
| awk '/^## Release checklist/{found=1} found' \
>> /tmp/release-body.md
-3 /tmp/release-body.md
-10 /tmp/release-body.md
Gate 2 — wait for merge
Stop. Tell the user: "PR opened at . Waiting for review/merge before pre-tag verification. Re-invoke this skill (or /loop) to continue once merged."
Do not poll, do not auto-merge. Do not push the tag from the un-merged branch.
Phase 3 — Pre-tag verification (the hard-won gate)
Goal: prove the merged commit on main actually builds clean before the tag locks it in. Do not skip this. Two release-blocking bugs in v0.17.4 would have shipped without this step.
Steps
-
Sync local main to the merged commit, and capture the SHA from the release PR (not just the local HEAD).
git checkout main && git pull
RELEASE_PR=$(gh pr list --repo amd/gaia --state merged --search "Release v<version> in:title" --json number --jq '.[0].number')
MERGED_SHA=$(gh pr view "$RELEASE_PR" --repo amd/gaia --json mergeCommit --jq '.mergeCommit.oid')
echo "Will tag: $MERGED_SHA (from PR #$RELEASE_PR)"
test "$(git rev-parse HEAD)" = "$MERGED_SHA" || { echo "Local main ($( git rev-parse HEAD)) does not match merged release PR SHA ($MERGED_SHA) — pull, or main has moved past the release commit"; exit 1; }
When you reach Gate 3 below, carry both $RELEASE_PR and $MERGED_SHA into the gate question text so Phase 4 can re-derive them from the answer rather than depending on shell variables that don't survive the pause.
-
Re-verify __version__ post-merge. Squash-merges have silently reverted this. If it doesn't match the target version, stop — open a follow-up PR to fix it before tagging. Never tag a wrong version.
grep -E '^__version__' src/gaia/version.py
-
Re-run the release notes validator on the merged tree.
python util/validate_release_notes.py docs/releases/v<version>.mdx --tag v<version>
-
Trigger Build Installers via workflow_dispatch against the merged SHA. This builds the same artifacts the tag push will build, on the same code, the tag exists.
Gate 3 — green run on the merged commit
Required before continuing:
__version__ matches the target.
validate_release_notes.py passes.
- Build Installers run on the merged commit is green (not yellow, not "mostly green except smoke tests").
Show the user the run URL, the release PR number (#$RELEASE_PR), and the merged SHA ($MERGED_SHA). Ask: "Pre-tag verification green on <sha> (release PR #N). Push tag v<version>?" — the SHA and PR number being in the question text means Phase 4 can re-derive them even if the user resumes in a fresh shell.
Phase 4 — Tag and trigger the publish pipeline
Goal: push the annotated tag; do nothing else.
Steps
-
Confirm you are on main at the verified SHA. Re-derive $MERGED_SHA from the release PR (the SHA from Gate 3) — do not trust shell state across the gate pause.
git checkout main
git pull
MERGED_SHA=$(gh pr view <release-pr-number> --repo amd/gaia --json mergeCommit --jq '.mergeCommit.oid')
test "$(git rev-parse HEAD)" = "$MERGED_SHA" || { echo "main moved past verified SHA $MERGED_SHA — re-verify (Phase 3) before tagging"; exit 1; }
-
Tag and push.
git tag -a v<version> <merged-sha> -m "Release v<version>"
git rev-list -n1 v<version>
git push origin v<version>
-
Confirm the publish workflow picked it up.
sleep 10
gh run list --repo amd/gaia --workflow publish.yml --limit 3
The flow is: validate → build (build-pypi + build-npm + build-desktop-installers) → approve-publish (manual gate) → publish (publish-pypi + publish-npm) → post-publish-smoke → github-release → refresh-context7. Surface the run URL.
Phase 5 — Monitor and approve
Goal: watch the run, distinguish flake from real failure, surface the manual approval gate to the human.
Steps
-
Watch the run.
gh run watch <run-id> --repo amd/gaia
-
On flake (timeout-only, no logic error in the failing step's logs): gh run rerun <run-id> --failed is non-destructive — re-runs only failed jobs and their downstream. Do not rerun the whole workflow; that wastes the publish budget.
-
On real failure (logic error, missing artifact, validator failure that wasn't there before): stop. Do not push past a red build. Do not delete and re-tag — that path is messy. Surface the failing step + log to the user.
-
Manual approval gate — when the run reaches the approve-publish job (gated on the publish GitHub environment), surface the URL to the user. Tell them: "Manual approval required at . Claude cannot click this — please approve in browser when ready." Then wait.
-
After approval, PyPI + npm + GitHub Release jobs run in parallel. Watch to completion.
-
refresh-context7 is the terminal job and may legitimately fail — that is not a release failure. This job runs after PyPI, npm, GitHub Release, and the desktop installers are already published. Context7's API rejects refresh requests inside a short cooldown window (observed: ~3–6 days between releases) with HTTP 429 (rate-limited); the workflow tolerates 429 and treats any other status as a hard failure. If the job is red, the release is still live. Open the job log, read the Response body: block between the ::stop-commands:: markers, and either accept the cooldown reason or file a follow-up about a new rejection cause. Do not delete or re-tag — see the recovery guidance in the Notes section below.
-
Never add set -x or curl -v to the refresh-context7 step to "debug" a failure. GHA only masks the verbatim secret value; curl -v prints the Authorization: Bearer <token> header, which is a transformed form that GHA's masking does not catch. Read the captured response body instead — it carries the same diagnostic signal without the leak risk.
Phase 6 — Post-release verification + announcement
Goal: confirm artifacts are live, draft the Discord announcement.
Steps
-
Smoke test the published wheel.
pip install --upgrade amd-gaia==<version>
gaia -v
Must report <version>. If it reports the previous version, the squash-merge version.py regression slipped through — escalate immediately.
-
Verify the GitHub release page.
gh release view v<version> --repo amd/gaia
Required artifacts: .whl, .tar.gz, .deb, .AppImage, .dmg, .exe, and the latest*.yml files for the Electron auto-updater. If any are missing, the corresponding build job didn't run or didn't upload — investigate.
-
Draft the Discord announcement. Read the just-shipped release notes (docs/releases/v<version>.mdx) to populate the highlight list — reuse the What's New bullets near-verbatim plus any Bug Fix worth surfacing. Same Notes format rules apply: bullets, plain, factual, no emoji.
The template and format rules live in
reference/discord-announcement.md — read it and
copy the template verbatim. Every element in it is load-bearing (the @gaia role
mention, the backticked version, the fenced install block, the fixed boilerplate
sentences); do not improvise the shape.
Gate 6 — present, do not auto-post
Show the user the smoke-test output, the artifact list, and the drafted Discord announcement in a fenced markdown block. Ask: "Post the announcement to Discord?" Wait for explicit yes — Discord posting is human-only (Claude does not have Discord access here, and announcements are visible to all users).
Output between phases
After every phase, output:
- Phase N complete.
- What changed / what was verified (1–3 bullets).
- Concrete artifact (URL, SHA, file path, or fenced draft).
- Gate question (always ends with
?, names the next destructive action).
Do not bundle two phases into one user prompt. The gates exist for review.
Notes
- The argument-passing convention is the target tag, not the previous tag. If the user says "release v0.17.5", that is what gets created — the previous tag is derived via
git tag --sort=-v:refname | head -1.
- Hotfix releases (
v0.15.4.1) follow the same flow; the validate_release_notes.py check accepts the four-part form.
- Minor/major releases (
v0.18.0, v1.0.0) need a richer notes structure — a "Highlights" block, the pip install instructions, and migration notes if breaking. The skeleton above is patch-shaped; expand for non-patch releases by mirroring the prior minor/major release notes.
- If the publish run fails partway through (e.g. PyPI publishes but npm doesn't), do not delete the tag and start over. Resolve the failing job, rerun only that job (
gh run rerun <run-id> --failed), and let the rest of the pipeline complete idempotently. The tag is the source of truth — preserve it.