diff --git a/.cnb.yml b/.cnb.yml index 4ef6ec69..1a9abfdf 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -145,6 +145,11 @@ $: tag_name="$(git describe --tags --exact-match 2>/dev/null || true)" fi version="${tag_name#v}" + cargo_version="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')" + if [ -n "$tag_name" ] && [ "$version" != "$cargo_version" ]; then + echo "ERROR: tag ${tag_name} does not match Cargo.toml version ${cargo_version}" >&2 + exit 1 + fi commit_sha="${CNB_COMMIT:-$(git rev-parse HEAD)}" { echo "# ${tag_name:-CNB release}" diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index ac8138ac..96c8b303 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -38,14 +38,15 @@ not enumerate. ## 2. Version pins are in sync -- [ ] `Cargo.toml` workspace `version` is bumped. -- [ ] All per-crate `crates/*/Cargo.toml` path-dependency `version = "..."` - pins match the new workspace version. -- [ ] `npm/codewhale/package.json` `version` AND `codewhaleBinaryVersion` - are both bumped. +- [ ] Run `./scripts/release/prepare-release.sh X.Y.Z` — it bumps the + workspace version, every per-crate dependency pin, + `npm/codewhale/package.json` (`version` + `codewhaleBinaryVersion`), + the README install-tag examples, refreshes `Cargo.lock`, regenerates + `crates/tui/CHANGELOG.md` and `web/lib/facts.generated.ts`, and ends + by running `check-versions.sh`. Write the CHANGELOG entry **before** + running it. - [ ] `npm/deepseek-tui/package.json` remains private/compatibility-only and is **not** bumped or published. -- [ ] `Cargo.lock` is refreshed (`cargo update --workspace --offline`). - [ ] `./scripts/release/check-versions.sh` reports `Version state OK: workspace=X.Y.Z, npm=X.Y.Z, lockfile in sync.` - [ ] `./scripts/release/check-ohos-deps.sh` reports that the OpenHarmony diff --git a/docs/RELEASE_RUNBOOK.md b/docs/RELEASE_RUNBOOK.md index 0f511f19..a96996cd 100644 --- a/docs/RELEASE_RUNBOOK.md +++ b/docs/RELEASE_RUNBOOK.md @@ -113,9 +113,12 @@ Crate publishing to crates.io is **manual** — there is no automated `scripts/release/` from a developer workstation that has `cargo login` configured. -1. Update the workspace version in [Cargo.toml](../Cargo.toml). -2. Run `./scripts/release/check-versions.sh` and - `./scripts/release/publish-crates.sh dry-run` locally; both must be clean. +1. Write the CHANGELOG entry, then run + `./scripts/release/prepare-release.sh X.Y.Z` — it bumps every + version-bearing file (workspace + crate pins + npm wrapper + README + install tags), refreshes the lockfile and generated files, and runs + `check-versions.sh`. +2. Run `./scripts/release/publish-crates.sh dry-run` locally; it must be clean. 3. Tag the release as `vX.Y.Z` (typically by pushing the version bump to `main` and letting `auto-tag.yml` create the tag — see the npm wrapper release section below for the `RELEASE_TAG_PAT` requirement). diff --git a/scripts/release/check-versions.sh b/scripts/release/check-versions.sh index 6d48c038..23c4ec83 100755 --- a/scripts/release/check-versions.sh +++ b/scripts/release/check-versions.sh @@ -149,7 +149,24 @@ if grep -qF "hmbown.dev@gmail.com" SECURITY.md; then fail=1 fi -# 8) Cargo.lock in sync. +# 8) Generated web facts carry the workspace version. +facts_version="$(grep -oE '"version": "[0-9]+\.[0-9]+\.[0-9]+"' web/lib/facts.generated.ts | head -n1 | sed -E 's/.*"([0-9.]+)".*/\1/')" +if [[ "${facts_version}" != "${workspace_version}" ]]; then + echo "::error::web/lib/facts.generated.ts version (${facts_version}) does not match workspace (${workspace_version}). Run: node web/scripts/derive-facts.mjs" >&2 + fail=1 +fi + +# 9) README install-tag examples point at the current release. +for readme in README.md README.zh-CN.md README.ja-JP.md README.vi.md; do + stale_tags="$(grep -nE -- "--tag v[0-9]+\.[0-9]+\.[0-9]+" "${readme}" | grep -v -- "--tag v${workspace_version}" || true)" + if [[ -n "${stale_tags}" ]]; then + echo "::error::${readme} has install examples pinned to an old tag (want v${workspace_version}):" >&2 + echo "${stale_tags}" >&2 + fail=1 + fi +done + +# 10) Cargo.lock in sync. if ! cargo metadata --locked --format-version 1 --no-deps >/dev/null 2>&1; then echo "::error::Cargo.lock is out of sync with the manifests. Run 'cargo update -p codewhale-tui' or 'cargo build' and commit the result." >&2 fail=1 diff --git a/scripts/release/prepare-release.sh b/scripts/release/prepare-release.sh new file mode 100755 index 00000000..4181f094 --- /dev/null +++ b/scripts/release/prepare-release.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Bump every version-bearing file for a release in one shot. +# +# Usage: ./scripts/release/prepare-release.sh +# +# Touches: Cargo.toml (workspace version), crates/*/Cargo.toml (internal +# codewhale-* dependency pins), npm/codewhale/package.json (version + +# codewhaleBinaryVersion), README*.md install-tag examples, Cargo.lock, +# crates/tui/CHANGELOG.md (via sync-changelog.sh) and +# web/lib/facts.generated.ts (via derive-facts.mjs). +# +# It does NOT write the CHANGELOG entry — add the `## [X.Y.Z] - YYYY-MM-DD` +# section first (see docs/RELEASE_CHECKLIST.md), then run this script, then +# let check-versions.sh (run at the end here) confirm everything agrees. +set -euo pipefail + +new="${1:?usage: $0 }" +if ! [[ "${new}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "error: '${new}' is not a plain X.Y.Z version" >&2 + exit 1 +fi + +repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "${repo}" + +old="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')" +if [[ "${old}" == "${new}" ]]; then + echo "workspace is already at ${new}; nothing to bump" + exit 0 +fi +echo "Bumping ${old} -> ${new}" + +if ! grep -q "^## \[${new}\]" CHANGELOG.md; then + echo "warning: CHANGELOG.md has no '## [${new}]' entry yet — add it before tagging" >&2 +fi + +OLD_VERSION="${old}" NEW_VERSION="${new}" python3 - <<'PY' +import os, pathlib, re, sys + +old, new = os.environ["OLD_VERSION"], os.environ["NEW_VERSION"] +old_re = re.escape(old) + +def bump(path, pattern, repl, minimum): + p = pathlib.Path(path) + text = p.read_text() + out, n = re.subn(pattern, repl, text) + if n < minimum: + sys.exit(f"error: expected >= {minimum} replacement(s) in {path}, made {n}") + p.write_text(out) + print(f" {path}: {n} replacement(s)") + +# 1) Workspace version. +bump("Cargo.toml", rf'^version = "{old_re}"$', f'version = "{new}"', 1) + +# 2) Internal codewhale-* dependency pins in every crate manifest. +total = 0 +for manifest in sorted(pathlib.Path("crates").glob("*/Cargo.toml")): + text = manifest.read_text() + out, n = re.subn( + rf'(codewhale-[a-z0-9-]+\s*=\s*\{{[^}}]*version = "){old_re}(")', + rf"\g<1>{new}\g<2>", + text, + ) + if n: + manifest.write_text(out) + print(f" {manifest}: {n} pin(s)") + total += n +if total == 0: + sys.exit("error: no internal dependency pins were bumped — wrong old version?") + +# 3) npm wrapper. +bump( + "npm/codewhale/package.json", + rf'("(?:version|codewhaleBinaryVersion)": "){old_re}(")', + rf"\g<1>{new}\g<2>", + 2, +) + +# 4) README install-tag examples (all translations). +for readme in ["README.md", "README.zh-CN.md", "README.ja-JP.md", "README.vi.md"]: + bump(readme, rf"--tag v{old_re}\b", f"--tag v{new}", 1) +PY + +echo "Refreshing Cargo.lock..." +cargo update --workspace --offline >/dev/null + +echo "Regenerating crates/tui/CHANGELOG.md slice..." +./scripts/sync-changelog.sh + +echo "Regenerating web/lib/facts.generated.ts..." +node web/scripts/derive-facts.mjs + +echo "Validating..." +./scripts/release/check-versions.sh +echo "Done. Review 'git diff', commit, and follow docs/RELEASE_CHECKLIST.md."