vtmocanu

uzi-release

"Cuts and ships a uzi release for this repo (hosted on GitHub), and nothing else: confirms nothing wanted in the release is still unmerged and main's CI is green, then cuts the release (RC-first version bump, CHANGELOG, tag, GHCR publish, deploy verify) INLINE as the lead with the bundled release-cut/release-watch/release-verify scripts and presents the summary while it publishes in the background. Reviewing and merging open PRs, Renovate included, is the uzi-lander skill. Use when the user runs /uzi-release, or asks to cut or ship a uzi release, promote an RC, bump the uzi version, or deploy uzi. Triggers include uzi-release, release uzi, cut a uzi version, cut an RC, promote the RC, ship uzi, deploy uzi."

vtmocanu 92 4 Updated 2w ago

Resources

1
GitHub

Install

npx skillscat add vtmocanu/uzi/uzi-release

Install via the SkillsCat registry.

SKILL.md

uzi-release

Cut a uzi release, watch it publish, verify it, then summarize. This repo is hosted on GitHub: use gh (which infers the repo from the checkout, so a fork under any owner works unedited; never glab/tea). CI is GitHub Actions (.github/workflows/), images and the Helm chart publish to GHCR on a v* tag.

Boundary. Reviewing and merging PRs, one or the whole open set, Renovate included, is the uzi-lander skill (references/batch.md for the set, references/renovate.md for dependency PRs). This skill starts once what should ship is on main.

Do NOT bake step details that live in the repo into your head. Read the source of truth each run: .github/workflows/release.yml (and ci.yml, kind-smoke.yml, brew.yml), .claude/agents/release.md (the mechanics reference), the destructive-ops rules in CLAUDE.md, and deploy/README.md for deploy (still GitLab-worded in places, not the authority for publish mechanics).

Note on the deploy layer. Cluster/deploy verification needs cluster access you may not have; if so, say so plainly rather than guessing.

0. Guardrails first

  • NEVER docker compose -p uzi down -v and never glob uzi- containers (see CLAUDE.md "Destructive operations"). This skill touches git, gh, and (if you have deploy access) kubectl only.
  • Work on main in the repo root worktree; release-cut.sh makes its own throwaway worktree for a promote. Never check the root worktree out onto another branch.
  • The release commit goes direct to main under the admin role's ruleset bypass (uzi-lander references/merge-mechanics.md has the ruleset). If a push is refused, read the refusal and stop rather than forcing.

1. Preconditions: nothing pending, main green

  • Nothing wanted in this release is still open. gh pr list --state open, uzi run list for in-flight runs. Anything the user wants in the release gets landed first with uzi-lander (the whole set: its references/batch.md), then come back. Say which open PRs you are leaving out. When unsure what the user wants in, ask.
  • Exact main tip green. Fetch and fast-forward main. Watch all existing tip workflows to green with .agents/skills/uzi-lander/scripts/watch-run-ci.sh --sha "$(git rev-parse origin/main)" --branch main in the background; require successful ci.yml and kind-smoke.yml push runs on that exact SHA. Run python3 -I scripts/release-ci-coverage.py tip HEAD --wait 0 for the shared policy. A missing, queued, running, failed or cancelled run blocks the cut. Wait for the exact tip's ci.yml + KinD success BEFORE release-cut: pushing the release commit supersedes main CI and cancels a running parent, destroying inheritance evidence. release-cut.sh repeats this precondition for verbs that write main. For --promote-only, validate candidate coverage instead; the current main tip need not be green. Candidate validation accepts at most two strictly metadata hops to green main-push CI and smoke. Do not substitute evidence from an older commit.

2. Cut the release inline with the deterministic scripts — do NOT dispatch a subagent

Once step 1 holds (nothing pending, main green on the tip), cut the release inline as the lead, driving three bundled scripts. Do NOT dispatch the release subagent for this: a background subagent spawns a watch and then idles, so every wait becomes a manual re-ping (the "agent keeps dying" ping-pong), while the steps that actually need judgment — flake-vs-defect, whether to rerun, authoring the CHANGELOG — are the lead's regardless. .claude/agents/release.md stays the MECHANICS reference (what each step proves, the signing traps, deploy verify), but the driver is the lead + scripts, not that agent.

What the lead decides first (from what merged since the previous stable tag):

  • The version X.Y.Z. Patch (0.46.0 → 0.46.1) for a maintenance/deps/tooling batch with no user-facing feature or behavior change; minor (0.46.0 → 0.47.0) when a product feature ships. Patch tags are in this repo's history (v0.42.1, v0.38.1), so a small batch does not force a minor. Model B: chart version == appVersion == the tag.
  • RC-first (PRD #1265): which verb. A release is cut as a CANDIDATE vX.Y.Z-rc.1 by default and PROMOTED to stable later, from the candidate's own commit, in lockstep with the next candidate. When an RC is already in flight for a lower base, release-cut.sh REFUSES with the facts (which RC, since when, whether [Unreleased] is empty) and exits 3. That refusal is the lead's cue to ask the user which of four: promote the in-flight RC and cut the next candidate (--promote, the usual answer once a candidate has been dogfooded), promote it alone (release-cut B --promote-only, when main carries work the user does not want in a candidate yet), cut another candidate for the same base (release-cut B, when the candidate was not good enough; it folds [Unreleased] into the open ## [B] section per ### subsection and refuses, CHANGELOG untouched, on text outside a subsection, a non-Keep-a-Changelog subsection name, a duplicate subsection in the result, or a line lost, invented or moved; a ### repeated inside [Unreleased] is collapsed with a NOTE), or abandon it (--skip-promote). --stable (the old one-step model) is only for a docs-only release and is refused while an RC is in flight.
    • RC dogfood install (PRD #1378): brew install vtmocanu/tap/uzi-cli-rc builds the newest candidate from source; it conflicts_with the stable uzi-cli by design (one active uzi), so uninstall stable first if present. Switch back with brew uninstall uzi-cli-rc && brew install uzi-cli. Stable users on uzi-cli never see a candidate.
    • Stale in-flight RC, and the stable must contain current main (not the old candidate). When the in-flight RC is many merges behind main and you want the stable to ship everything on main: you cannot --stable (refused while an RC is in flight), and a plain --promote tags the OLD rc commit, so neither gives you the tip. The route is rc-at-tip, PUBLISH it, then promote: (1) author the oracle-flagged shipping merges into the open ## [B] section and COMMIT them first (the oracle reads the changelog at the committed ref, and release-cut refuses a dirty tree); (2) release-cut.sh B cuts vB-rc.(N+1) at the tip; push main; tag immediately once the metadata-only release diff is reviewed; (3) tag that RC and PUSH it (git tag -a vB-rc.(N+1) HEAD, then ! git push origin vB-rc.(N+1)), and wait for it to publish (release-watch.sh then release-verify.sh). Do NOT keep it local-only: the rc cut's worker-tag-autobump has already pinned workers.image.tag to that RC, so an unpushed tag would leave the stable chart pointing at an agent-* image release.yml never built (--promote REFUSES an in-flight RC that is not on origin). The redundant RC publish is the price of the invariant. (4) release-cut.sh <B+1> --promote sees zero delta since the new RC and takes the promote-only branch: it tags vB from the tip and cuts NO next candidate. Before promotion, require green candidate coverage, directly or through at most two strictly classified metadata hops. Push vB, watch, verify. Wart: promote-only after an on-main RC cut leaves main's Chart.yaml at B-rc.(N+1) (behind the stable). Harmless (nothing publishes off main until the next tag; ArgoCD tracks the newest published chart = vB), and the next feature cut clears it.
    • The stable must be the candidate exactly, and main has moved on. release-cut.sh B --promote-only (B = the in-flight base, not a next version): it tags vB from the RC commit and stops. No next candidate, no commit on main, no main push; work merged after the RC is not in vB and ships with the next plain release-cut <next>, whose coverage window starts at vB. It keeps --promote's published-RC refusal and takes no --changelog-file/--no-commit/--prev-tag. Push vB only, watch, verify once. Same wart as above, plus main's ## [B] date stays at the RC-cut date until the next cut of a new base syncs it from the vB tag. Prefer --promote when the next candidate is wanted anyway: a plain --promote with shipping work on main and an empty [Unreleased] fails the RC half and rolls the stable tag back.
  • A draft ## [X.Y.Z] - <date> CHANGELOG section (write it to a file for release-cut.sh --changelog-file), folded from [Unreleased] (keep an empty [Unreleased] on top), Keep-a-Changelog subsections (### Added/### Changed/### Fixed), no em dashes (user preference for user-facing content). The section is keyed by the STABLE base X.Y.Z even for a candidate (it is opened at rc.1 and reused by every later rc.N and the promotion, PRD #1265 D3). Every shipping merge since the previous STABLE v* tag MUST be cited by issue/PR number or short SHA (with gh authenticated, an issue the squash-merged PR closes also counts) — the oracle scripts/assert-changelog-covers-release.sh enforces it (shipping = api/**, agent/src/**, controller/**, web/src/**, deploy/chart/**, docs/**, excluding tests; docs(...)-typed, chore(release):, and non-shipping-only merges are exempt). release-cut.sh cites the dependency bumps check-changelog-entry.sh spares (scripts/lib/dependency-bump.sh) in one Routine dependency updates bullet under ### Changed; cite everything else. Enumerate them with git log --first-parent --oneline <prev-stable-tag>..HEAD. (If [Unreleased] already carries the entries, omit --changelog-file and release-cut.sh folds it in.)

The three scripts (.agents/skills/uzi-release/scripts/), in order:

  1. release-cut.sh <X.Y.Z> [--promote|--promote-only|--skip-promote|--stable] [--changelog-file FILE] — the whole prep, deterministic. It reads the LOCAL main and refuses one that is behind origin/main: fast-forward first. To rehearse a verb you have not run live, clone the repo to a scratch dir, git remote set-url origin <the GitHub URL>, git checkout -B main <commit>, and cut there: every tag stays in the clone. A <commit> behind the live origin/main trips the behind refusal, so to rehearse a past state point origin at a scratch git clone --bare of that state instead. It DERIVES the coverage base (the previous stable tag) itself, so there is no <prev-tag> positional (--prev-tag TAG overrides for a repair). Default cuts vX.Y.Z-rc.1 (folds [Unreleased]), the next rc.(N+1) for the in-flight base (folds [Unreleased] into the open section, guarded), or refuses (exit 3, with the facts) when a lower RC is in flight. --promote creates the stable vB tag LOCALLY from a throwaway release/B worktree at the RC commit (metadata-only edits, a diff allowlist checked BEFORE any tag exists, worktree+branch removed after), then cuts the next vX.Y.Z-rc.1 on main. --promote-only runs that promote half alone and exits: X.Y.Z = the in-flight base, nothing is committed on main, and the rest of this step does not apply (review git show vB, push the tag, go to script 3). Every other verb bumps Chart.yaml, runs worker-tag-autobump.sh (rolls the fleet IFF the agent runtime surface changed — the pin may legitimately STAY at a PUBLISHED RC tag inside a stable chart, D11, do not hand-normalize it; an unpublished pin is refused by the release guards), changelog-links.sh, commits chore(release): <tag> on main, and verifies with the oracle + changelog-links --check. On a verify failure it undoes the commit (reset --soft, edits kept staged) and exits 1. It does NOT push and does NOT tag the RC. Review git show HEAD, then git push origin main.
  2. Review the release diff, push main, then tag the candidate immediately with git tag -a vX.Y.Z-rc.1 -m vX.Y.Z-rc.1 HEAD (use the exact tag printed by release-cut), and push it under the user's existing authorization or hand them ! git push origin vX.Y.Z-rc.1. Do not wait for the release commit's own full CI. release.yml enforces successful main-push CI and smoke on the tagged SHA or through at most two strict metadata hops; a known failure on the tagged SHA blocks publication. It validates the tagged chart and bundled changelog, and requires the real tagged KinD smoke before chart publication. Full main CI still runs on the release commit. Never use a skip-ci marker, which suppresses tag workflows too. Promote push order: stable tag, main, candidate tag. A promote-only cut pushes just the stable tag; candidate coverage is required but current-main coverage is not.
  3. release-watch.sh <tag>, in the BACKGROUND — it watches release.yml AND brew.yml for every tag (PRD #1378: an RC publishes the opt-in uzi-cli-rc formula, a stable publishes uzi-cli). It AUTO-RERUNS a transient publish failure (any non-gate job) up to --max-reruns (default 2); gate failures (assert-*/prep) are NOT auto-rerun. For a KinD flake causing red assert-tag-smoke, first run gh run rerun <kind-run> --failed on the failed tag KinD run and wait for success, then run gh run rerun <release-run> --failed on release.yml. Then release-verify.sh <tag> proves the publish: five image tags + chart on GHCR, the cosign "Pushing signature to:" lines, and the channel — a stable Release is marked latest; an RC Release is flagged pre-release with releases/latest unchanged (the previous stable). On a promote, run verify TWICE: once for the stable tag, once for the new RC. After a stable, also confirm what verify does not: gh release list shows vB as Latest with no vB-rc.* entry left (release.yml prunes them), the deployment's chart revision reaches B (it lags a reconcile or two even after a hard refresh; re-check before calling it stuck), and an unchanged worker pin rolled no workers.

Manual hotfix compatibility. An off-main stable patch has no main-push CI. Follow deploy/README.md's Hotfix procedure: run affected local gates and check:release-metadata, then annotate the patch tag with the exact standalone line CI-Coverage: hotfix-uncovered. The exception requires the immediately previous stable patch on the first-parent chain and rejects a tag targeting main. In general, a failed main-push run on the tagged SHA or any visited coverage-chain commit vetoes publication. Report UNVERIFIED hotfix, no full CI evidence; tagged metadata, worker-image checks and real tagged smoke still apply. Never present the exception as CI-covered.

Run every watcher in the BACKGROUND, never a foreground blocking gh run watch. A foreground blocking watch pins the turn for the 5-10 min CI/publish window and locks the user out of the session the whole time; that background-vs-foreground split is the one real reason watches must not block the lead. Use a run_in_background command (or a background waiter) and react to its completion notification.

A tag push needs the user's go-ahead. It publishes unattended, so without an explicit in-session authorization the harness classifier blocks git push origin v* (Blocked by classifier, from a subagent and the lead alike); with one, the lead pushes the tag itself. That is a harness permission-mode block, distinct from the protect-release-tags ruleset (which would instead come back as a git/remote rejection). If blocked, the annotated tag already exists locally (shared across worktrees via .git), so hand the user one command: ! git push origin vX.Y.Z. The admin role then bypasses the ruleset's creation restriction (remote: Bypassed rule violations … [new tag]). Do NOT route around it with gh api to create the ref — that defeats the same intent.

Present the step-3 summary once the tag is pushed — the publish runs unattended in the background (release-watch.sh) while you report what shipped, and you fold the verified tags (or a block) into the summary when release-verify.sh confirms.

3. Final summary (always end with this)

Report to the user, concisely:

  • One line up top: version X.Y.Z and its status — cut / CI-green / tag pushed / published to GHCR (images + chart) / blocked on <what> (e.g. the tag push awaiting the user's ! git push); CI coverage proven at publish (or explicitly UNVERIFIED for an annotated hotfix exception); deploy state (or "deploy verification needs cluster access I don't have"). Since the publish runs in the background (step 2's release-watch.sh), the summary may go out while it is still publishing — say so, and update once release-verify.sh confirms.
  • What shipped, by issue/PR: one table over the release's CHANGELOG section (every shipping merge since the previous stable tag, as the oracle enforces): PR | issue | what it delivered in 1-2 sentences.
  • Behavior changes to flag: anything that changes existing behavior, plus post-deploy caveats.
  • Left out: open PRs deliberately not in this release, and why.
  • Follow-ups: release-mechanics gaps this cut surfaced; offer to file issues.

Categories