gaia-release · git:20261019.c4b5352 · 2026-10-19 · sha256 d25ae879a0a12bda
gaia-release git:20261019.c4b5352A
Immutable. This exact content never changes and is served at /api/v1/blob/d25ae879a0a12bda.
---
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. Also cuts release candidates (vX.Y.Z-rcN) for testing before the final. Use when the user asks to 'cut a release', 'cut a release candidate', '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](../../../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:
```bash
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). |
| `v<version>-rcN` tags exist, no final tag | A release candidate is out. Resume at Phase 3.5 step 4 (test pass) for the highest `N`. List them with `git tag --list "v<version>-rc*"`. |
| `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 <url> — 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)."*
A `PR_OPEN` or `PR_MERGED` state may have been produced by the nightly automation
rather than a person: [`.github/workflows/nightly-patch-release.yml`](../../../.github/workflows/nightly-patch-release.yml)
drafts Phase 1-2 on its own when a patch release is due, and stops before merge and
tag. Treat that PR exactly like a human-drafted one — review the notes, then resume
at the phase the table gives.
## Hard rules (do not violate)
These map to [CLAUDE.md](../../../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](../../../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.
- **Pushing tags is irreversible.** Always confirm the SHA the tag will point to and the green status of the pre-tag verification run before `git push origin v<version>`.
- **Manual approval gate at the publish step** is human-only — Claude cannot click the GitHub environment "approve" button. Surface the run URL and stop.
---
## 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
1. **Survey commits since previous tag, then sanity-check the version request against scope.**
```bash
PREV=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[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** (`vX+1.0.0`) — push back |
If the requested version doesn't match the rubric, **stop and surface the mismatch**: *"You asked for `v<requested>` (patch). I see N feat commits since `<prev>` including `<one or two examples>` — this looks minor-shaped. Continue as patch, or bump to `v<suggested>`?"* Do not silently proceed.
2. **Read recent release notes for structure only** — frontmatter shape, section headings, PR-link format. Take **length and tone from *Notes format* below, not from the files**: [v0.21.1](../../../docs/releases/v0.21.1.mdx) is the model at 267 narrative words, while v0.22.0 (1705) and v0.23.0 (1119) were flagged in review as too much text in too-blocky a form — they are the regression this section exists to prevent.
Patch releases are short. Minor/major releases carry more bullets and a `pip install` block — the per-bullet shape is identical, only the count grows.
3. **Create [docs/releases/v<version>.mdx](../../../docs/releases/)** with this skeleton (adapt to whether it's patch / minor / major):
```mdx
---
title: "v<version>"
description: "<one plain line: what shipped. Not a pitch.>"
---
# GAIA v<version> Release Notes
<Optional single sentence naming what this release is about. Drop it entirely if
the bullets already say it — that is the common case. Never a paragraph.>
## Breaking Changes
- **<what changed>** — <what to do instead>. (PR [#NNN](https://github.com/amd/gaia/pull/NNN))
## What's New
- **<what the user can now do>** — `<gaia command>`. <At most one more sentence, only
if the title genuinely needs it.> (PRs [#NNN](https://github.com/amd/gaia/pull/NNN), [#NNN](https://github.com/amd/gaia/pull/NNN))
## Bug Fixes
- **<what was broken>** — <what now happens>. (PR [#NNN](https://github.com/amd/gaia/pull/NNN))
## Known Issues
- **<what still does not work>** — <workaround, or "tracked in [#NNN](https://github.com/amd/gaia/issues/NNN)">.
## Contributors
- [@handle](https://github.com/handle) — <what they contributed>. (PR [#NNN](https://github.com/amd/gaia/pull/NNN))
## Full Changelog
**N commits** since v<previous>:
- `<sha>` — <subject>
- ...
Full Changelog: [v<previous>...v<version>](https://github.com/amd/gaia/compare/v<previous>...v<version>)
```
Omit `## Breaking Changes` and `## Known Issues` when empty — no "None" placeholder. Drop
the `---` rules between sections; the headings already separate them.
**Generate the changelog by introspecting git, and escape it for MDX.** Do not
hand-transcribe subjects, and do not pipe `git log` output in raw — a subject
containing `<` or `{` is valid git and invalid MDX, which fails CI's `mintlify
validate`. v0.22.0 hit this on `#1791`'s subject (*"fix Mintlify MDX validation
(unescaped '<' breaks the docs check)"*) — the one commit whose subject describes
the bug it causes.
```bash
# Generate the `- `<sha>` — <subject>` lines, escaping the two characters MDX
# treats as syntax. `\<` and `\{` render as literal `<` / `{` (verified against
# `mintlify validate`), and shas never contain them, so escaping the whole line
# is safe. Subjects like "AMD <> SpecificAI" survive correctly.
git log v<previous>..HEAD --pretty=format:'- `%h` — %s' \
| sed -e 's/</\\</g' -e 's/{/\\{/g' > /tmp/changelog.txt
# Guard: nothing hazardous may survive.
grep -nE '(^|[^\\])[<{]' /tmp/changelog.txt && echo "MDX HAZARD — fix before continuing" || echo "changelog MDX-safe"
```
Sanity-check the count against `git log --oneline | wc -l` **after** writing the file
(`wc -l` under-counts by one when the last line has no trailing newline — v0.22.0
claimed 197 when the real count was 198). The claimed count, the listed lines, and
`git log` must all agree.
**Notes format (apply to every entry — this is the point of the skill).** Tone is
already governed by [CLAUDE.md → How You Communicate](../../../CLAUDE.md#how-you-communicate)
— plain language, outcome first, each point made exactly once. Do not restate or soften
it here. What *is* release-notes-specific is the shape:
- **Bullets, not paragraphs.** Every entry is one bullet: a bold clause naming what the
user can now do, then at most one sentence, then the PR links. No `###` prose blocks
inside `What's New`.
- **Say it once.** A highlight appears in the bullet list or in the opening sentence,
never both. The frontmatter `description` is not a third copy.
- **Factual, not promotional.** State the capability and stop. Banned: emoji, "we're
excited to announce", "finally", "blazing(-fast)", "game-changer", "seamless",
"powerful", "unlock", "Here's the good stuff", "makes X effortless", "no more
crashes", invented benchmarks.
- **Order by what users run.** Agents and commands first (`gaia hub`, `gaia email`, …);
SDK, CI, and refactor work last, or omitted when it has no user-visible effect.
- **Word budget — a cap, not a target.** **≤ 350 words** for a patch, **≤ 600** for a
minor/major, measured from the top down to the first of `## Bug Fixes` / `## Known
Issues` / `## Contributors` / `## Full Changelog`. Those four are reference lists —
their length is set by how many fixes actually shipped, and squeezing them hides work.
The cap is on the narrative part, which is what bloats. Over budget means cut entries,
not reflow them. Step 8 checks it. (Calibration, same measure: v0.21.1 shipped at 267,
v0.22.0 at 1705, v0.23.0 at 1119.)
**Example — one highlight:**
> **Bad** (prose block, promotional, three sentences where one works):
> ### Triage your inbox from the terminal — `gaia email`
> Point GAIA at your inbox and it sorts the noise from what needs you: drafts
> replies to routine mail, flags what's urgent, leaves the rest. Runs locally, so
> your mail never leaves your machine. Try it: `gaia email`.
> **Good** (one bullet, plain, factual):
> - **Triage your inbox from the terminal** — `gaia email` sorts mail, drafts replies to
> routine messages, and flags what needs you. Runs locally. (PR [#1234](https://github.com/amd/gaia/pull/1234))
4. **Update [docs/docs.json](../../../docs/docs.json):**
- Add `releases/v<version>` to the Releases tab.
- Bump the navbar label (e.g. `v<previous-version> · Lemonade <previous-lemonade>` → `v<version> · Lemonade <current-lemonade>`). Read [src/gaia/version.py](../../../src/gaia/version.py) for the `LEMONADE_VERSION` constant — it is the source of truth, and the navbar may be drifted from it (Lemonade bumps land outside release PRs).
5. **Sync the UI package version.**
```bash
node installer/version/bump-ui-version.mjs
```
Confirm [src/gaia/apps/webui/package.json](../../../src/gaia/apps/webui/package.json) now reads the new version.
6. **Bump the hub component manifests.** The Terminal UI (component id `terminal-hub`) and Agent UI publish to
the Agent Hub R2 catalog on a version tag, and `release_components.yml` gates
the tag against each manifest — R2 paths are immutable, so publishing under a
stale version cannot be undone. A mismatch **fails the release**, by design.
```bash
sed -i '' "s/^version: .*/version: <version>/" \
hub/components/terminal-hub/gaia-agent.yaml \
hub/components/agent-ui/gaia-agent.yaml # GNU sed: drop the ''
grep -H '^version:' hub/components/*/gaia-agent.yaml # both must read <version>
```
Then bump the Terminal UI's **`min_gaia_version`** to `<version>` too. It needs
daemon host API v1.1, which no release before this one ships — so the release it
publishes alongside *is* its minimum, and `release_components.yml` refuses to
publish it under an older core. Leave `agent-ui`'s alone: it talks to
`gaia.ui.server`, not the daemon control plane.
```bash
sed -i '' "s/^min_gaia_version: .*/min_gaia_version: \"<version>\"/" \
hub/components/terminal-hub/gaia-agent.yaml # GNU sed: drop the ''
python util/check_component_core_api.py --release-version <version>
```
Finally, record what the **previous** release shipped in `RELEASED_DAEMON_API`
in [util/check_component_core_api.py](../../../util/check_component_core_api.py) — the
guard resolves an already-published version from that table, and a missing row
makes it fall back to trusting this tree, which is the drift it exists to catch.
7. **Confirm `__version__` is correct.**
```bash
grep -E '^__version__' src/gaia/version.py
```
The post-prior-release bump usually handles this, but a squash-merge can revert it silently. If it's wrong, edit it. If it's right but reverted later (see Phase 3), the validator will catch it.
8. **Validate — both checks.** Run from the repo's activated venv (`source .venv/bin/activate` on Linux/macOS, `.venv\Scripts\activate` on Windows; the bare-`python` Microsoft Store stub will fail). If you're working from a git worktree without its own venv, run from the parent checkout's venv.
```bash
python util/validate_release_notes.py docs/releases/v<version>.mdx --tag v<version>
(cd docs && npx -y mintlify@latest validate) # the docs.yml `validate` job — MUST also pass
# Word budget from *Notes format* — the narrative part only, not the reference lists.
# Cap: 350 words for a patch, 600 for a minor/major. Over budget means cut entries.
awk '/^## (Bug Fixes|Known Issues|Contributors|Full Changelog)/{exit} {print}' \
docs/releases/v<version>.mdx | wc -w
```
Both must exit 0. Fix any errors before continuing. If either fails for reasons unrelated to your changes (missing dep, broken import), stop and surface that — do not silently bypass. `validate_release_notes.py` prints the first failing check (missing/renamed section, absent `compare/` link, tag mismatch) — read that line to localise the fix; it has no `--verbose` flag.
**`validate_release_notes.py` passing is not sufficient — it is not an MDX parser.** CI's
`validate` job additionally runs `mintlify validate` from `docs/`, and v0.22.0 failed it
after the notes passed the Python validator (see the escaping rule in step 3). Its error is
misleading: an unparseable `.mdx` surfaces as `"releases/v<version>" is referenced in the
docs.json navigation but the file does not exist` — the file exists, it just never parsed.
Pre-existing parse errors under `docs/plans/` and `docs/superpowers/` are reported but
non-fatal; leave them alone.
### 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
1. **Branch and commit.**
```bash
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 # confirm scope-clean — no drive-by edits
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.
2. **Read the most recent release PR body to match shape.**
```bash
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.
3. **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.
```bash
# Strip the YAML frontmatter (everything between the first two '---' lines)
awk '/^---$/{c++; next} c>=2' docs/releases/v<version>.mdx > /tmp/release-body.md
# Append the Release checklist section from the previous release PR
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
# Sanity-check the body before opening the PR
head -3 /tmp/release-body.md # should start with "# GAIA v<version> Release Notes"
tail -10 /tmp/release-body.md # should end with the checklist
```
If the awk pipeline produces an empty file, the previous PR didn't have a `## Release checklist` heading — fall back to copying the entire body and editing it manually before continuing.
4. **Open the PR.** Title: `Release v<version>`. Body: the file you just built.
```bash
gh pr create --repo amd/gaia \
--title "Release v<version>" \
--body-file /tmp/release-body.md \
--reviewer kovtcharov-amd
```
Surface the resulting PR URL.
### Gate 2 — wait for merge
Stop. Tell the user: **"PR opened at <url>. 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
1. **Sync local main to the merged commit, and capture the SHA from the release PR (not just the local HEAD).**
```bash
git checkout main && git pull
# Re-derive from the release PR — survives gate pauses across sessions/shells.
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.
2. **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.
```bash
grep -E '^__version__' src/gaia/version.py
```
3. **Re-run the release notes validator on the merged tree.**
```bash
python util/validate_release_notes.py docs/releases/v<version>.mdx --tag v<version>
```
4. **For a final: check the flagship agent was released from this commit.** `gaia init`, `gaia hub install gaia` and the Windows setup install the flagship from the Agent Hub and `hub/agents/gaia/npm/binaries.lock.json`, which only `release_agent_gaia.yml` (tag `agent-pkg-gaia-vX.Y.Z`) fills. `publish.yml` and `release_components.yml` run this same check on a final tag and fail it; run it first so a missing flagship release is found before the tag exists. (An RC skips it: it freezes its own flagship.)
```bash
git fetch --tags origin
python util/check_flagship_release.py --ref "$MERGED_SHA"
```
If it fails, release the flagship in this order. Skipping step 2 deadlocks: `release_agent_gaia.yml` refuses to run until the Terminal UI (`terminal-hub`) version the lock pins is on the hub, and it normally publishes only on the final tag, which this gate blocks.
1. Bump the flagship in a PR: edit `__version__` in `hub/agents/gaia/python/gaia_agent/__init__.py`, run `python hub/agents/gaia/python/packaging/stamp_version.py`, add a `hub/agents/gaia/npm/CHANGELOG.md` entry.
2. **Publish the Terminal UI (`terminal-hub`) first.** Dispatch **Publish Hub Components (R2)** (`release_components.yml`) on main with `dry_run` unchecked; a dispatch skips this gate. Its agent-ui and Windows-setup legs fail because there is no GitHub release or new flagship yet; they publish nothing. Skip this if `hub/components/terminal-hub/gaia-agent.yaml`'s version is already on the hub.
3. Tag `agent-pkg-gaia-v<flagship-version>` on the merged commit and approve `release_agent_gaia.yml`.
4. Land the `binaries.lock.json` it regenerated (its log prints it) and re-run Phase 3 on that commit. Code the binary is frozen from must not change between the agent tag and the core tag.
5. **Trigger Build Installers via `workflow_dispatch` against the merged SHA.** This builds the same artifacts the tag push will build, on the same code, *before* the tag exists.
```bash
gh workflow run "Build Installers" --repo amd/gaia --ref main
sleep 5
RUN_ID=$(gh run list --repo amd/gaia --workflow "Build Installers" --limit 1 --json databaseId -q '.[0].databaseId')
echo "Watching run $RUN_ID"
gh run watch "$RUN_ID" --repo amd/gaia
```
**AppImage smoke jobs (`appimage-distro-matrix`, `appimage-userns-restricted`) are the most flaky** — `appimage-userns-restricted` has a 300s `state: ready` poll (raised from 90s to cover a first-run model download). On real failure, **stop** and fix root cause; on transient flake (timeout-only, no logic error), `gh run rerun $RUN_ID --failed` is acceptable.
### Gate 3 — green run on the merged commit
Required before continuing:
- `__version__` matches the target.
- `validate_release_notes.py` passes.
- For a final, `check_flagship_release.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 3.5 — Release candidate (default for minor/major; ask for a patch)
**Goal:** put the exact build users will get in front of a tester before it becomes the default. An RC tag runs the same `publish.yml` with **no approval** (its `approve-publish` runs in the reviewer-less `publish-rc` environment), and stays off every stable channel:
| | Final `v<version>` | RC `v<version>-rcN` |
|---|---|---|
| GitHub Release | normal, becomes latest | pre-release, never latest |
| PyPI | `<version>` | `<version>rcN` — `pip install amd-gaia` ignores it without `--pre` |
| npm `@amd-gaia/agent-ui` | dist-tag `latest` | `<version>-rc.N` under dist-tag `next` |
| Agent Hub R2 (`release_components.yml`) | publishes | does not run — the catalog has no pre-release channel |
| Website | stable downloads | a collapsed "Try the release candidate" entry, hidden once the final ships |
| Flagship agent (`gaia-agent`) | the hub's, from `release_agent_gaia.yml` | frozen from the RC's commit by `rc-flagship`, attached to the pre-release; the RC core installs it from there and never from the hub |
| `release` branch, release-notes bot | run | skipped |
| Release notes | `docs/releases/v<version>.mdx` | the same file — no RC-specific notes |
`version.py` stays `<version>`; the pipeline stamps the RC suffix from the tag (`util/release_tag.py`), so the final can be tagged on the RC's commit with no further version bump. The terminal's `.pkg`/`.deb`/`.rpm` packages are not built for an RC (they need a numeric-only version); the RC carries the one-step Windows setup (`gaia-X.Y.Z-rc.N-win-x64-setup.exe`, built by `publish.yml`'s `rc-windows-setup` around that run's own desktop app build — never published to R2), the desktop app installers, the raw terminal binaries, and the wheel/sdist. A failed setup leaves it off the pre-release and turns the run red via `rc-windows-setup-missing`.
### Steps
1. **Dry-run the classification** — a malformed tag fails `validate` before anything builds, but catching it locally is cheaper:
```bash
python util/release_tag.py classify v<version>-rc1
python util/validate_release_notes.py docs/releases/v<version>.mdx --tag v<version>-rc1
```
`IS_RC=true`, `PEP440_VERSION=<version>rc1`, `NPM_VERSION=<version>-rc.1`. Anything else (`-rc.1`, `rc1` without the dash, `-rc0`, `-beta1`) is rejected.
2. **Tag the verified SHA from Gate 3 and push** — same confirmation rule as Phase 4: the SHA and the green pre-tag run go in the question.
```bash
git tag -a v<version>-rc1 <merged-sha> -m "Release candidate v<version>-rc1"
git rev-list -n1 v<version>-rc1 # MUST equal <merged-sha>
git push origin v<version>-rc1
```
3. **Monitor exactly as in Phase 5, minus the approval:** an RC publishes as soon as its builds and `ci-green` pass. Tagging `-rc(N+1)` does not cancel `-rcN`; if `-rcN` is still building, it stops itself right before publishing (red, "superseded by"), so only the newest RC reaches `next`. `redeploy-website-rc` asks the website to rebuild so the RC entry appears.
4. **Personal test pass** on a machine that does not already have the release installed (CLAUDE.md: test from the user's real initial state):
```bash
pip install amd-gaia==<version>rc1 && gaia -v # must print <version>rc1
npm view @amd-gaia/agent-ui dist-tags # latest unchanged, next = <version>-rc.1
gh release view v<version>-rc1 --repo amd/gaia # Pre-release, installers + gaia-agent-* attached
gaia init && gaia hub list # gaia agent version ends in +v<version>-rc1
```
Then install the desktop app from the pre-release and walk the golden paths the release notes advertise.
5. **Problems → fix forward, then `-rc2`.** Land the fix on `main` through a normal PR, re-run Phase 3 on the new merged SHA, and tag `v<version>-rc2` from step 2. Never move or delete an RC tag — each one is a published version on PyPI and npm.
### Gate 3.5 — RC passed
Ask: **"`v<version>-rcN` passed testing on `<sha>`. Tag the final `v<version>` on that same SHA?"** Phase 4 then tags that SHA (re-derive it with `git rev-list -n1 v<version>-rcN`), not necessarily the release PR's merge commit.
---
## Phase 4 — Tag and trigger the publish pipeline
**Goal:** push the annotated tag; do nothing else.
### Steps
1. **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.
```bash
git checkout main
git pull
# Re-derive from the release PR — the PR number was in the Gate 3 question.
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; }
```
2. **Tag and push.**
```bash
# Annotated, on the verified SHA — every prior release tag is annotated ("Release vX.Y.Z").
# A bare `git tag v<version>` creates a lightweight tag and breaks convention.
git tag -a v<version> <merged-sha> -m "Release v<version>"
git rev-list -n1 v<version> # MUST equal <merged-sha> before pushing
git push origin v<version>
```
3. **Confirm the publish workflow picked it up.**
```bash
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) + ci-green → approve-publish (manual gate for a final; none for an RC) → publish (publish-pypi + publish-npm) → post-publish-smoke → github-release`. Surface the run URL.
`ci-green` requires a passing run of `lint.yml`, `test_unit.yml` and `test_gaia_cli_linux.yml` on the tagged commit (`util/check_release_ci.py`). Most main commits have none (the next merge cancels their runs), so it dispatches the missing ones against the tag and waits up to ~80 min alongside the builds. A red one stops the release before anything publishes.
---
## Phase 5 — Monitor and approve
**Goal:** watch the run, distinguish flake from real failure, surface the manual approval gate to the human.
### Steps
1. **Watch the run.**
```bash
gh run watch <run-id> --repo amd/gaia
```
2. **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.
3. **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.
4. **Manual approval gate (finals only)** — 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 <url>. Claude cannot click this — please approve in browser when ready."** Then wait.
5. **After approval**, PyPI + npm + GitHub Release jobs run in parallel. Watch to completion.
6. **Context7 is not refreshed by the release.** GAIA's docs exceed the size Context7 refreshes automatically (its API answers HTTP 403 "exceeds the size limit for automatic refresh"), so the pipeline no longer asks. If Context7's copy of the docs is stale after a release, request a manual refresh with an issue on https://github.com/upstash/context7. It never blocks or reddens a release.
---
## Phase 6 — Post-release verification + announcement
**Goal:** confirm artifacts are live, draft the Discord announcement.
### Steps
1. **Smoke test the published wheel.**
```bash
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.
2. **Verify the GitHub release page.**
```bash
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.
3. **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`](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:
1. **Phase N complete.**
2. **What changed / what was verified** (1–3 bullets).
3. **Concrete artifact** (URL, SHA, file path, or fenced draft).
4. **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 the newest final tag (Phase 1 step 1's `PREV`) — never an `-rcN` tag, or the notes would cover only the changes since the candidate.
- Hotfix releases (`v0.15.4.1`) follow the same flow; the `validate_release_notes.py` check accepts the four-part form.
- Release candidates are `v<version>-rcN` only (Phase 3.5). There is no RC form of a four-part hotfix tag.
- 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. Every npm publish (`publish.yml` and the `release_agent_*.yml` agent releases) goes through `util/npm_publish_once.py`, so a re-run skips a version npm already holds with the same bytes and fails loudly if npm holds different bytes under it — that version can never be corrected; ship a new one.