Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions .claude/skills/prepare-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
name: prepare-release
description: Prepare a wave-level genblaze release — scope version bumps and dependency-floor drift with tools/prepare_release.py, apply them, gate with make pre-release, and open a release PR. Cannot tag or publish. Use before cutting a new CHANGELOG wave (as opposed to /release-check, which gates one already-decided package version).
allowed-tools: Bash(git add:*) Bash(git commit:*) Bash(git checkout:*) Bash(git switch:*) Bash(git push:*) Bash(git status:*) Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(python3:*) Bash(make pre-release:*) Bash(make ts-types:*) Bash(gh pr:*) Read Grep Edit
---

# Prepare a release wave — $ARGUMENTS

**Safety boundary**: the `allowed-tools` list above deliberately has no `git tag`,
`gh release`, `twine upload`, or `make post-release`. This skill cannot tag or publish
anything — without a `git tag` step there is no tag to push, and without `gh release`
or `twine` there is nothing to trigger `.github/workflows/release.yml`. It goes as far
as a merge-ready release PR and stops. Tagging, releasing, and post-release
verification are human commands, emitted (not run) at the end.

This is a thin driver over [`tools/prepare_release.py`](../../../tools/prepare_release.py),
which does the actual bookkeeping (dynamic package discovery, version bumps, dependency
floor sync, reserved-core-version guard — see its module docstring). For what a wave
release actually IS (versioning policy, the publish pipeline graph, tag-naming
convention) see [RELEASING.md](../../../RELEASING.md) — this skill does not restate
it. For gating one already-decided package version one at a time, use `/release-check`
instead; this skill operates at the wave level across every package at once.

## Step 1 — Scope

```bash
python3 tools/prepare_release.py --check
```

Read the report: which packages changed since the last tag, what version bumps and
dependency-floor updates it computes, and any warnings (e.g. a connector with no
entry in `libs/meta/pyproject.toml`). Exit 0 means nothing to prepare — stop here.
Exit 1 means proceed to Step 2. Exit 2 is a hard error (bad `--set`/`--bump`, a git
failure, or the reserved-core-version guard tripping) — resolve it before continuing.

Before doing anything else, confirm:
- You're on `main` (`git status`), up to date with `origin/main`, and the working
tree is clean.
- `git log` shows the commit you're about to release from is the one you intend.

## Step 2 — Apply, then cut the CHANGELOG

```bash
python3 tools/prepare_release.py --apply
```

This writes every version bump and dependency-floor update computed in Step 1. Pass
`--set <pkg>=<version>` or `--bump <pkg>=minor|major` to override the default patch
bump for any package (e.g. `--set core=0.4.0` for a deliberate minor/major, or to
route around the reserved-version guard intentionally).

Then, by hand (this is not scriptable — it's an editorial decision about wave naming
and prose):

1. Cut `CHANGELOG.md`'s `[Unreleased]` section to `## [X.Y.Z] - <today>`, pasting in
the `### Released package versions` list the script just emitted. Fold in any
entries that were mis-sectioned under the wrong package heading.
2. Leave `[Unreleased]` empty — `changelog-gate` in `release.yml` fails the release
otherwise.
3. **The tag name is `v` + that heading, exactly** (e.g. heading `## [0.5.0]` → tag
`v0.5.0`) — `validate-version` enforces this and a mismatch is the single most
common reason a release run fails. Do not confuse the wave name with any one
package's version (see RELEASING.md's versioning policy).

If `libs/spec` changed this wave, `prepare_release.py` will have flagged it — run:

```bash
make ts-types
```

and commit the regenerated `libs/spec/ts/genblaze.d.ts` in the same PR.

## Step 3 — Gate

```bash
make pre-release
```

This runs the same lint/typecheck/ts-types/pypi-metadata/pin-parity/test/release-smoke
gates the release workflow runs (see RELEASING.md's Pre-release checklist) — a clean
run here is a strong signal `validate-version`, `changelog-gate`, and `release-smoke`
will all be green once tagged. Delegate any single-package specifics (entry-point
sanity, per-connector doc freshness) to `/release-check`.

**macOS prerequisites** — verify these are already satisfied before running the
gate; this skill has no permission to install anything, and one-time machine setup
(cert installation) should never be automated by a skill:
- An activated venv so `python`/`pip` resolve (`make pre-release` shells out to
`python -m build`, `mypy`, etc.)
- `mypy>=1.8` and `pip install build twine` already done in that venv
- If the PyPI-metadata/pin-parity checks fail with an SSL error, run
`/Applications/Python 3.x/Install Certificates.command` once (a one-time,
machine-level change — do this yourself, don't script it), or set
`export SSL_CERT_FILE=$(python3 -m certifi)` for the session.

If `make pre-release` can't run locally for environment reasons (missing Node for
`ts-types`, no local PyPI network access, etc.), fall back to the `workflow_dispatch`
dry-run instead of forcing it: push the branch, then from the Actions tab → Release
workflow → "Run workflow" with `dry_run: true`. This exercises the identical
pipeline against TestPyPI without a local toolchain.

## Step 4 — Open the release PR

```bash
git switch -c release/<wave>
git add -A
git commit -m "chore(release): bump versions and cut CHANGELOG <wave> wave"
git push -u origin release/<wave>
gh pr create --title "chore(release): <wave> wave" --body "..."
```

Never commit to `main` directly. This is a normal PR — it needs review and a green
CI run like anything else before it merges.

**Conditional follow-ups** (only if the wave warrants them, not mandatory steps):
- Breaking changes this wave? Write `MIGRATING-<wave>.md`.
- Large surface touched? Consider running `/verify-docs` and `/security-review`
before opening the PR.

## After the PR merges

Once CI is green on `main`, here are the human commands to run — this skill does
not run them for you:

```bash
git tag -a v<wave> -m "Release <wave>"
git push origin v<wave>
gh release create v<wave> --title "<wave>" --notes-from-tag
# after the Release workflow finishes publishing:
make post-release VERSION=<umbrella-version>
```

`<umbrella-version>` is `libs/meta/pyproject.toml`'s version, which may differ from
the wave name (see RELEASING.md — e.g. wave `0.3.0` shipped umbrella `0.4.0`).
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `tools/prepare_release.py`: deterministic wave-level release-prep engine.
Discovers every package dynamically (no hardcoded version literals or
connector list), bumps versions for what changed since the last tag, and
— independent of whether the affected package itself changed — resyncs
`cli`'s and the `genblaze` umbrella's `genblaze-core`/`genblaze-s3`/
connector-extra dependency floors to match, forcing a republish when a
pin-only change would otherwise ship without a version bump (the drift
class `tools/check_pin_parity.py` guards against). Refuses to bump core
onto the reserved `raise_on_failure` default-flip version. `--check`/
`--apply` modes, `--set`/`--bump` overrides. New `.claude/skills/
prepare-release/SKILL.md` drives it end-to-end through `make pre-release`
and a release PR — it cannot tag or publish (see the skill's safety
boundary).

## [0.5.0] - 2026-07-16

Correctness and security hardening across the pipeline, provenance, streaming,
Expand Down
9 changes: 7 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ Before cutting a release, verify on `main`:

1. **Versions are bumped.** Every package whose code changed since the
last release has its `pyproject.toml` `version` updated. The umbrella
`libs/meta/pyproject.toml` is bumped to reflect this release.
`libs/meta/pyproject.toml` is bumped to reflect this release, along with
its (and `cli`'s) `genblaze-core`/`genblaze-s3`/connector-extra floors.
`python3 tools/prepare_release.py --check`/`--apply` automates this
step deterministically — see `.claude/skills/prepare-release/SKILL.md`
for the end-to-end driver.
2. **CHANGELOG is cut.** The `[Unreleased]` section is empty; a new
`## [X.Y.Z] - YYYY-MM-DD` section lists every package version change
under "Released package versions".
under "Released package versions" (the exact text `prepare_release.py`
emits).
3. **TS types are current.** `make ts-types` produces no diff. (CI's
`ts-types-check` job already enforces this on every push, but the
release workflow re-runs it as a defense in depth.)
Expand Down
81 changes: 81 additions & 0 deletions docs/exec-plans/active/release-prep-automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!-- created: 2026-07-16 -->
# Wave Release-Prep Automation

Adds a deterministic engine for the bookkeeping half of cutting a genblaze release
wave (RELEASING.md), plus a thin skill driver over it. Motivated by a concrete
failure mode from this session: a hand-edited release-prep pass bumped
`genblaze-core` but silently missed updating `cli`'s `genblaze-core` dependency
floor to match — invisible until a much later CI run. That's exactly the drift class
`tools/check_pin_parity.py` already guards against for connectors (`genblaze-s3` in
0.3.0, `genblaze-langsmith` + `genblaze-cli` in 0.3.2); this closes the same gap one
step earlier, before a human even has to remember to run the guard.

## Goals & success criteria

- No hardcoded package list or version literal anywhere in the new script — package
set comes from fixed structural roots (`libs/core`, `cli`, `libs/meta`,
`libs/spec`) plus a glob over `libs/connectors/*`.
- `cli`'s and the `genblaze` umbrella's `genblaze-core`/`genblaze-s3`/connector-extra
dependency floors are always resynced to the FINAL decided version of the package
they point at, whether or not that package changed this wave — the existing
`test_cli_core_dependency_floor_matches_local_core` /
`test_umbrella_core_dependency_floor_matches_local_core` /
`test_umbrella_s3_dependency_floor_matches_local_s3` /
`test_umbrella_connector_extra_floors_match_local_versions` invariants in
`cli/tests/test_cli.py` stay green regardless of which packages moved.
- Refuses to bump `genblaze-core` onto the version reserved for the
`raise_on_failure` default flip (`_RAISE_ON_FAILURE_DEFAULT_FLIP_VERSION` in
`libs/core/genblaze_core/pipeline/pipeline.py`), read from source rather than
duplicated as a literal in the script.
- Idempotent: re-running `--apply` (or hand-bumping a package outside the tool) is a
safe no-op for anything already past what was published at the last tag.
- The driving skill (`.claude/skills/prepare-release/SKILL.md`) cannot tag or
publish — enforced via its `allowed-tools` frontmatter, not prose.

## Key decisions

- **Text-level pyproject/package.json surgery, not full TOML re-serialization.**
Targeted regex line rewrites preserve comments and formatting; matches how the
rest of `tools/` (e.g. `validate-version` in `release.yml`) already treats these
files as text, not just data.
- **Floor rewriting preserves the existing upper-bound cap verbatim** (e.g. the
`<0.4` in `genblaze-core>=0.3.6,<0.4`) — only the floor number is substituted, so
the script never hardcodes that cap and stays correct once it eventually moves.
- **"Already bumped" is detected by comparing on-disk version to what was published
at the last tag**, not by tracking script-internal state across runs — this is
what makes re-running safe and also what let a live smoke-test (`--check` against
`main`, last tag `v0.4.0`) correctly report the already-prepped 0.5.0 wave as
clean except for one genuine, previously invisible gap (see Verification).
- **A pin that needs to change forces a version bump on the file that carries it**,
even with no other code change to that package — this is the actual fix for the
motivating bug, not a side effect.
- **New connectors with no existing floor line in `libs/meta/pyproject.toml` are not
auto-wired** (extra + bundle membership is an editorial call) — surfaced as a
warning instead.
- Design was red-teamed via a fork sub-agent before implementation (idempotency
heuristic, reserved-version guard blind spot on `--check`, skill `allowed-tools`
vs. its own prescribed macOS prereqs, over-generalized TOML editor) and again by
a 3-reviewer panel over the finished diff before push.

## Verification

- `pytest tools/tests/ -v` — 86 passed (34 new for `prepare_release.py`, no
regressions in `check_pin_parity`/`check_pypi_metadata`/`release_import_smoke`
tests).
- `python3 tools/prepare_release.py --check` against the live repo (last tag
`v0.4.0`) correctly recognized every package the 0.5.0 wave-prep commit
(`afe1dd5`) already bumped as "no action needed," and surfaced exactly one real,
previously invisible gap: `genblaze-nvidia`'s only change since `v0.4.0` is a
README relative-link fix that was never version-bumped, so the broken-link
README is still what's live on PyPI for that package.
- `ruff check` / `ruff format --check` clean on both new files.

## Follow-ups (not in this change)

- Whether to fold the `genblaze-nvidia` README gap the smoke test surfaced into a
future patch wave is a release-scheduling decision, not something this PR
resolves.
- Each connector's own `genblaze-core>=X,<0.4` floor (as opposed to `cli`'s and the
umbrella's) is intentionally left wide/unsynced per existing convention (see the
0.5.0 CHANGELOG: "All other connectors unchanged — their existing floor already
admits 0.3.6") — not a gap, a deliberate scope boundary.
Loading
Loading