Skip to content

feat(release): version-bump automation + shared release-readiness gate - #139

Merged
Mikola Lysenko (mikolalysenko) merged 1 commit into
mainfrom
feat/version-bump-automation
Jul 31, 2026
Merged

feat(release): version-bump automation + shared release-readiness gate#139
Mikola Lysenko (mikolalysenko) merged 1 commit into
mainfrom
feat/version-bump-automation

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Turns the version-bump chore into a one-command / one-click flow, and makes the release-readiness checks a shared, CI-enforced gate so a release can't start with unfinished version chores.

scripts/bump-version.sh <X.Y.Z> [--pr]

One command does the whole bump: validates the version (plain X.Y.Z, no downgrade, clean tree), rolls CHANGELOG.md's [Unreleased] notes into a dated ## [X.Y.Z] section (refuses if nobody wrote notes), stamps every packaging site via version-sync.sh, and with --pr pushes release/vX.Y.Z and opens the PR — body pre-filled with the rolled-over release notes and the post-merge playbook.

.github/workflows/version-bump.yml

The same flow as a workflow_dispatch (input: the new version), so a bump can be started from the GitHub UI alone. Documented caveat: a PR opened with GITHUB_TOKEN doesn't trigger pull_request CI (the same event suppression that shaped release.yml's one-workflow topology) — close/reopen the PR or push to its branch to kick checks; running the script locally is the preferred path and avoids this entirely. No PAT/App token introduced, keeping the zero-long-lived-credential posture.

scripts/release-lint.sh — one gate, two enforcement points

Check Ordinary PRs / pushes (CI release-readiness) Bump PRs (version changed vs base) Release workflow version job
Version coherence — version-sync.sh is a no-op across all packaging sites
CHANGELOG section exists and is non-empty
Tag v<X.Y.Z> absent (or at HEAD = release retry)
  • The coherence check on every PR means a hand-edited version in any single site (csproj, gemspec, pom, …) fails CI immediately instead of surfacing mid-release.
  • release.yml's version job now calls the script in place of its inline tag/CHANGELOG checks — the PR gate and the publish gate are the same code and cannot drift.
  • The non-empty-notes check is new (previously only the heading was required).

docs/releasing.md

The full operator playbook: bump → merge → dispatch Release (optional dry-run) → approve staged npm (platform packages first) → "Re-run failed jobs" semantics → pointer to the one-time registry setup checklist in #138.

Verified

  • Scratch-clone round-trip: bump 3.3.0 → 3.4.0 rolls the CHANGELOG correctly (empty [Unreleased] kept on top, 382 lines of notes under the new dated heading), full lint then passes end-to-end; a second bump correctly refuses on the now-empty [Unreleased].
  • Refusal paths: downgrade, same-version, vX.Y.Z shape, dirty tree, empty notes.
  • Drift detection: a hand-edited csproj version fails the lint with the file named, and the tree is restored afterwards.
  • On current main state: --sync-only passes (ordinary-PR CI stays green); the full gate correctly fails for 3.3.0, whose CHANGELOG section genuinely doesn't exist — the next bump supersedes that, but it means a hypothetical 3.3.0 re-release dispatch fails the gate exactly as the old inline check would have.
  • Found & fixed during testing: awk -v escape-processes its values, so -v re="\[3\.4\.0\]"-style patterns silently mangle and misread a populated section as empty — the section matchers use plain-string prefix matching instead.
  • shellcheck clean on all three scripts (now also linted in CI); YAML parses; all uses: SHA-pinned.

🤖 Generated with Claude Code


Note

Low Risk
Changes are release automation, CI gates, and docs only—no runtime product code paths.

Overview
Adds one-command / one-click version bumps and centralizes release checks so PR CI and the Release workflow cannot drift.

scripts/bump-version.sh validates the target version, rolls [Unreleased] into a dated ## [X.Y.Z] section (refuses empty notes), runs version-sync.sh across all packaging sites, and with --pr opens release/vX.Y.Z with notes and post-merge steps in the body.

scripts/release-lint.sh is the shared gate: version coherence (version-sync must be a no-op), non-empty CHANGELOG section, and optional remote tag collision check (tag at HEAD allowed for retries). CI’s new release-readiness job runs sync-only on ordinary PRs and the full gate on version-bump PRs; release.yml replaces inline tag/CHANGELOG steps with the same script. version-bump.yml dispatches the bump from Actions (with the documented GITHUB_TOKEN / pull_request CI caveat).

docs/releasing.md documents bump → merge → Release → npm 2FA approval and idempotent re-runs. CI also shellchecks the release scripts.

Reviewed by Cursor Bugbot for commit 365924c. Configure here.

scripts/bump-version.sh <X.Y.Z> [--pr] turns the whole version-bump chore
into one command: validates the version (shape, no downgrade, clean tree),
rolls CHANGELOG.md's [Unreleased] notes into a dated `## [X.Y.Z]` section
(refusing when no notes were written), stamps every packaging site via
version-sync.sh, and with --pr opens the release/vX.Y.Z PR whose body
carries the rolled-over notes and the post-merge playbook.

.github/workflows/version-bump.yml exposes the same flow as a
workflow_dispatch so a bump can be started from the GitHub UI. Documented
caveat: PRs opened with GITHUB_TOKEN don't trigger pull_request CI (the
same event suppression that shaped release.yml's topology) — close/reopen
the PR or push to its branch to kick checks; running the script locally is
the preferred path.

scripts/release-lint.sh is the single source of truth for the release
gate, shared by CI and the Release workflow so the two can't drift:
  - version coherence: version-sync.sh must be a no-op (catches a
    hand-edited version in any single packaging site);
  - CHANGELOG has a non-empty section for the version;
  - --tag-check: the tag doesn't exist at a commit other than HEAD
    (same-commit = release retry, allowed).

ci.yml gains a release-readiness job: coherence-only on ordinary
PRs/pushes, the full gate (incl. tag check) on PRs that change the
workspace version. release.yml's version job now calls the same script in
place of its inline tag/CHANGELOG checks. The full playbook lives in
docs/releasing.md.

Note: awk -v escape-processes its values, so the section matchers use
plain-string prefix matching instead of -v regexes — a -v pattern like
"\[3\.4\.0\]" silently mangles and misreads a populated section as empty
(caught by the scratch-clone round-trip test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Post-release lockfile false drift
    • Excluded npm/socket-patch/package-lock.json from the coherence check since lockfile changes from optional dependency resolution don't indicate version drift.

Create PR

Or push these changes by commenting:

@cursor push 3eaf9d1a98
Preview (3eaf9d1a98)
diff --git a/scripts/release-lint.sh b/scripts/release-lint.sh
--- a/scripts/release-lint.sh
+++ b/scripts/release-lint.sh
@@ -79,13 +79,24 @@
 else
   bash scripts/version-sync.sh "$VERSION" >/dev/null
   DRIFTED="$(git status --porcelain | awk '{print $2}')"
-  if [ -n "$DRIFTED" ]; then
-    fail "version-sync.sh $VERSION is not a no-op — these files carried a stale version: $(echo "$DRIFTED" | tr '\n' ' ')"
+  # Exclude npm/socket-patch/package-lock.json: version-sync refreshes it via
+  # npm install --package-lock-only, which re-resolves optional platform packages
+  # from the registry. After a release publishes those packages, the lockfile
+  # changes from hollow stubs to fully resolved entries (or vice versa when
+  # running on the bump commit before publish), but that's not version drift.
+  DRIFTED_VERSIONS="$(echo "$DRIFTED" | grep -v '^npm/socket-patch/package-lock\.json$' || true)"
+  if [ -n "$DRIFTED_VERSIONS" ]; then
+    fail "version-sync.sh $VERSION is not a no-op — these files carried a stale version: $(echo "$DRIFTED_VERSIONS" | tr '\n' ' ')"
     # The tree was clean before the sync, so restoring exactly the files the
     # sync touched leaves it as found.
     echo "$DRIFTED" | xargs git checkout --
   else
-    note "version coherence OK: every stamped site already carries $VERSION"
+    if [ -n "$DRIFTED" ]; then
+      note "version coherence OK: every stamped site already carries $VERSION (npm lockfile refreshed from registry, as expected)"
+      echo "$DRIFTED" | xargs git checkout --
+    else
+      note "version coherence OK: every stamped site already carries $VERSION"
+    fi
   fi
 fi

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 365924c. Configure here.

Comment thread scripts/release-lint.sh
Comment thread scripts/bump-version.sh
Comment thread scripts/release-lint.sh
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Jul 30, 2026
…ace)

Between a sibling thread's fork() and its exec(), the child briefly
inherits every open fd — including a write fd on the binary staged
moments ago — and exec'ing the file during that window fails with
"Text file busy". Retry the spawn on ErrorKind::ExecutableFileBusy
(10 attempts, 25 ms linear backoff, <=1.4 s worst case) instead of
failing a fully SHA-verified download; all other spawn errors still
fail immediately and the 10 s hang timeout applies per attempt. Same
dance Go's os/exec and cargo do.

Turns the previous commit's RED regression test green and deflakes the
coverage job (first bitten on PR #139: llvm-cov widens the race window,
which is why `test`/`test-release` never caught it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 570339d into main Jul 31, 2026
107 of 108 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the feat/version-bump-automation branch July 31, 2026 16:19
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Jul 31, 2026
…en self-update against the fork/exec fd race (#140)

* test(update): regression test for the ETXTBSY sanity-exec race (RED)

Reproduces the coverage-job flake deterministically: a write fd held open
on the staged binary while sanity_exec runs — the shape a sibling
thread's fork() leaves behind via fd inheritance — makes the exec fail
with "Text file busy" (Linux enforces ETXTBSY; the test is linux-gated).

Also tightens the existing strictness test to assert each rejection's
REASON instead of bare is_err(), which previously let an ETXTBSY spawn
failure masquerade as the expected rejection one line before the flake
surfaced.

This commit is intentionally pushed without the fix so CI demonstrates
the failure; the follow-up commit makes it pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(update): retry sanity-exec on ETXTBSY (fork/exec fd-inheritance race)

Between a sibling thread's fork() and its exec(), the child briefly
inherits every open fd — including a write fd on the binary staged
moments ago — and exec'ing the file during that window fails with
"Text file busy". Retry the spawn on ErrorKind::ExecutableFileBusy
(10 attempts, 25 ms linear backoff, <=1.4 s worst case) instead of
failing a fully SHA-verified download; all other spawn errors still
fail immediately and the 10 s hang timeout applies per attempt. Same
dance Go's os/exec and cargo do.

Turns the previous commit's RED regression test green and deflakes the
coverage job (first bitten on PR #139: llvm-cov widens the race window,
which is why `test`/`test-release` never caught it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants