Release train PR 1: release.py core (offline stamp, rc versions, CHANGELOG sync/fold, blocker gate) - #643
Conversation
…ckers PR 1 of the weekly release train (docs/release-train/DESIGN.md §7). One stdlib-only Python file with argparse subcommands: - stamp <V> [--check]: offline, byte-deterministic version stamp of Cargo.toml (workspace version + =V core pin), Cargo.lock (source-less workspace-member entries, so --locked builds), the 15 npm manifests and npm/socket-patch/package-lock.json (JSON edit; platform entries for any other version are dropped instead of re-resolved over the network, which ends the #233/#235 lock-drift class). - semver: X.Y.Z and X.Y.Z-rc.N only, semver precedence. - next-version: from git tags + burned release/* branches + the [Unreleased] headings of sync-main(C) computed in memory, never from main's Cargo version. New majors need APPROVED_MAJORS (5 is pre-approved) and are never skipped; otherwise refused with an error. - changelog cut|promote|sync-main|check: rc sections reach main on every rc; promotion folds rc.1..rc.K into one [X.Y.Z] section and returns later abandoned rc blocks to [Unreleased]. Exact-match, deterministic, idempotent; newer [Unreleased] entries are never touched. - notes: release notes with a link to the open-P1 query, never titles. - blockers --base <sha>: the §3.5 release-blocker rule over REST (injectable transport); any API error blocks. Tests: scripts/tests/test_release.py with temp git repos driven through the train timeline and recorded REST shapes under scripts/tests/fixtures/release/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- scripts/version-sync.sh is now a thin wrapper over `release.py stamp` (same CLI contract; also stamps Cargo.lock's workspace entries). On the clean tree `version-sync.sh 4.0.0` is a byte no-op. - scripts/release-lint.sh: the grammar accepts X.Y.Z and X.Y.Z-rc.N; check 2 is `release.py stamp --check` (byte compare, offline, no clean-tree requirement, writes nothing); check 3 is `release.py changelog check` (rc sections; a stable fails while rc sections remain unfolded); new --stable-only and --tag-exists. - ci.yml release-readiness: the rolling `release-sync` PR (which moves main to the newest cut tag, rc or stable) runs `release-lint.sh --tag-exists`; every other PR keeps today's behavior. - release.yml (legacy pipeline until the train replaces it): lint with --stable-only so it can never publish an rc as Latest/npm latest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
version-bump.yml's unsigned push is rejected by the main ruleset and it never ran; the release train cuts versions with scripts/release.py instead. The CHANGELOG header and docs/releasing.md now point at docs/release-train/DESIGN.md (the full runbook rewrite is PR 4), the interim manual bump uses `release.py changelog cut` + version-sync.sh, and ci.yml stops shellchecking the deleted script. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
D1: GitHub App socket-patch-release + refs/tags/v* ruleset (App-only bypass); the App mints the tag in the publish job (PR 3). D2: version and CHANGELOG reach main on every rc via the release-sync PR, folded at promotion. D3: routines run as mikolalysenko (a routine actor, not an approver) until a bot exists; npm stable is direct OIDC; newest-line hotfixes only. D4: the first train release is 5.0.0 (pre-approved major). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CHANGELOG (sync-main / cut / promote):
- The fold no longer classifies rc sections by rc number. Every rc
section whose core shipped is replaced by its blocks (tag text) minus
[S]'s blocks, as a multiset shared with the [Unreleased] removal. A
train rc cut beside a same-core hotfix now returns its entries instead
of losing them, whichever was cut first. TagSource.promoted_from is gone.
- Matching counts occurrences: a shipped block removes one occurrence,
so a repeated entry ("- Updated dependencies.") survives an unmerged
sync PR and no longer changes the bump level.
- Returned blocks go before the blocks already in their subsection, and
a cut orders its ### subsections canonically (breaking, then Keep a
Changelog, then the rest). The next cut is now byte-identical with or
without the sync PR, including subsection order left by shipped history.
- CRLF CHANGELOGs round-trip, and every generated line uses CRLF.
Blocker gate:
- Fails closed unless GET /labels/release-blocker returns that exact
name. Label names compare case-insensitively. labeled events since L
are candidates, and a label that vanished without an unlabeled event
still blocks. Deleted, converted and transferred issues block.
- RELEASE_APPROVERS / RELEASE_ROUTINE_ACTORS split on commas and
whitespace and accept a leading @. A malformed login, an empty list, or
no trusted approver blocks with a config error, in cmd_blockers and in
evaluate_blockers.
- since = committer date of merge-base(L, base), clamped to the base
date, not a forgeable tag date. A since later than the base fails
closed.
- PR-merge closes (closed event with commit_id null, recorded from #454)
resolve through the closing PR's merge_commit_sha being in base.
- Malformed event shapes fail closed.
Lint / CI:
- release-lint check 2 also runs the new offline `release.py
npm-lock-check`: the wrapper lock's packages[""] must match
package.json, and every non-optional dependency needs a node_modules
entry. This restores the dependency-drift check the networked lock
refresh used to give.
- ci.yml takes the release-sync --tag-exists path only for a same-repo
release-sync branch into main.
- rel.Git ignores GIT_DIR/GIT_WORK_TREE and similar variables.
Tests:
- StampTests are hermetic: they run on a temp tree stamped to a fixed
baseline (4.0.0, and 5.0.0-rc.1 via a subclass), so the suite passes on
main after the release-sync PR.
- The temp repos ignore the git env and global config.
- New coverage: release-lint (plain and --tag-exists) on a sync-main'ed
tree at an rc, the CI gate step run with stubs, and a regression test
for each finding.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- §1: rc.2's section is synced to main and its unshipped blocks return to [Unreleased] when the stable is synced. - §2: the release.py list adds semver, npm-lock-check, next-version and changelog. - §3.1: U = [Unreleased] of sync-main(C) computed in memory (this replaces the pre-D2 "minus L's section" rule). - §3.4: hotfix cuts use --no-sync, and same-core train rcs return to [Unreleased]. - §3.5: the label check, case-insensitive names, labeled-event candidates, vanished labels and gone issues, the merge-base `since`, strict config parsing, and PR-merge close resolution. - §3.7: multiset fold, chronological returns, canonical cut order, CRLF, and the same-repo-only release-sync CI path. - §4: npm-lock-check and the ci.yml condition. - §5 I1 and PR 4: the stable tree is stamp + promote (fold), not a heading rename. - S5: the release-blocker label must exist before the gate can pass. - §7 PR 1: the accept list adds the new scenarios. - §8: APPROVED_MAJORS is kept (D4), and multiset counting is kept. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- promote keeps every occurrence when folding rcs, so [X.Y.Z] is the multiset sum of its rcs and sync-main charges them with the same count (a repeated entry no longer returns as unshipped or raises the bump); a later abandoned rc returns all of its blocks. - sync-main charges the rc sections on main against [S] before removing the rest of [S] from [Unreleased], so a newer identical entry keeps its place whether or not the release-sync PR merged. - blockers: since is never later than base - 35 days, so a forged high stable tag at the base cannot shrink the candidate window further; S9 is a hard prerequisite for live gate runs. - blockers: a PR-merge close resolves only through PRs merged by the close actor (merged_by.login, recorded for #456). - stamp keeps each file's line endings (CRLF checkouts check clean). - sync-main computes CHANGELOG and stamp before writing anything. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
Burn-down agent: Ready for review at
Generated by Claude Code |
|
Review updated for Four release-lint test assertions now inspect stdout and stderr together, preserving the failure-status and diagnostic-content checks. This handles GitHub Actions annotations correctly. Production code is unchanged by the correction. Validation: the full Python suite passed with The tested files match this commit, the diff is clean, and it merges cleanly with main CI note: a PDM 2.6.1 Ubuntu case initially hit API HTTP 504 before patching. One targeted retry passed all 34 supported/expected-refusal cases, with two explicitly unsupported PEP582 cases and no failures. The previously failing case now verifies patch bytes, repeat install/sync/rescan, rollback, and unchanged lock/manifest files; the fresh result artifact digest was verified. |
|
BugBot review Please review the test correction on |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f43c3e3. Configure here.
Release train PR 1:
release.pycore, offline stamp, rc-aware release-lintSummary
This is the first of the four PRs in
docs/release-train/DESIGN.md§7. It adds the tested core that the weekly release train is built on. No workflow publishes anything new yet.scripts/release.py(stdlib Python, one file). Subcommands:semver: theX.Y.Z/X.Y.Z-rc.Ngrammar and precedence.stamp: an offline, byte-deterministic version stamp of Cargo.toml, Cargo.lock, the 15 npm manifests and the npm wrapper lock.npm-lock-checknext-version: derived from git tags and burnedrelease/*branches, never from main's own version.changelog cut|promote|sync-main|checksync-mainnotesblockers: the §3.5 release-blocker gate.scripts/version-sync.shis now a thin wrapper overrelease.py stamp. The networkednpm install --package-lock-onlyis gone, which ends the fix(maven): send the official Maven CLI user agent to the maven2 registry; refresh npm wrapper lock for published 4.0.0 platform packages #233/test(e2e): demote the cargo production legs to canary status — free cargo tier is unpublished #235 lock-drift class.scripts/release-lint.sh:X.Y.Z-rc.N;stamp --checkplusnpm-lock-check;--stable-onlyand--tag-existsflags.ci.ymlrelease-readiness: a same-reporelease-syncPR intomainrunsrelease-lint.sh --tag-exists. Every other PR behaves as before.version-bump.ymlandscripts/bump-version.sh. Every reference is fixed (release.yml comments, docs/releasing.md, the CHANGELOG header, the ci.yml shellcheck list).scripts/tests/test_release.pyplus fixtures, which include a recorded#454/#456PR-merge close. CI's existingunittest discoveralready runs them.Design decisions (maintainer decisions D1–D4 applied)
sync-mainbrings main to the newest cut tag, rc or stable. It inserts each pending rc's## [X.Y.Z-rc.N]section from the tag and removes exactly those blocks from[Unreleased].changelog promote --rc Kfolds rc.1..rc.K into one## [X.Y.Z]section. When that stable is synced, every rc section of the core is dropped, and its blocks that did not ship go back to[Unreleased], oldest rc first and in their original chronological place. That covers an abandoned rc.K+1 and a train rc cut beside a hotfix.[Unreleased]is touched.next-versionandchangelog cutgive byte-identical results whether or not the release-sync PR merged.release/*branches. An unmerged sync PR never changes what gets cut.release-lintaccepts main at an rc.APPROVED_MAJORS = (5,), so the first train cut is5.0.0-rc.1. A breaking heading that would open an unapproved major is refused, and a major is never skipped.mikolalysenkois a routine actor, so he is never trusted to clear a blocker.commit_id: null) resolves only through a PR merged by the close actor whose merge commit is in the base.sincecomes frommerge-base(L, base)(never a tag's date) and is never later than base − 35 days, so a forged tag cannot shrink the window further.core.autocrlfcheckout works. CHANGELOG transforms keep CRLF, and every stamped file keeps its own line endings, sostamp --checkis a byte no-op there.sync-maincomputes the CHANGELOG and every stamped file before it writes any of them.Acceptance results (DESIGN.md §7 PR 1, run end to end on this branch)
End-to-end script on the worktree (the tree was restored afterwards and
git statusis clean):Unit tests (
python3 -B -m unittest discover -s scripts/tests -v, as CI runs them):Ran 215 tests in 766.415s — OK (skipped=1); 82 of them are intest_release.py, all passing.Accept-list items covered by
scripts/tests/test_release.py:5.0.0-rc.1(test_first_train_on_the_main_snapshot_is_5_0_0_rc_1);5.0.1-rc.1or5.1.0-rc.1;unlabeledevent, and a deleted, converted or transferred issue;@-separated forms;sincecomes from the merge-base, a forged high tag cannot move it past the lookback, and asinceafter the base fails closed;release-lintpasses on that rc;sync-mainis idempotent;sync-mainwrites nothing when the stamp refuses;release-lint(no flags and--tag-exists) on async-mained rc tree;What PRs 2–4 do next
release-qa.yml+ smoke:workflow_dispatchrun atrelease/v<V>builds the 14 archives and 15 npm tarballs once, smokes those bytes, and callsci.ymlplus the 9 compat workflows throughworkflow_call;release_smoke.py(Tier 0/1), theverify-qaverdict, and the Tier 0 step in the ci.ymle2e-buildleg;release.ymlrc path:plan(blocker gate, green-SHA window, version), thencut(a signedcreateCommitOnBranchstamp commit with a tree assertion), then QA with reruns and a fallback, thenpublishin envpublish(the App mints the tag, then crates → npmnext→ GitHub prerelease), thenverify-channelsandreport;publish-*.ymlanddispatch-publish.sh;approveenvironment gate;plan/cut(rc commit +stamp+changelog promote), re-finding the rc's full-QA run;waive_soak;docs/releasing.mdrunbook,ROUTINE.mdand the CHANGELOG header.release-trainroutine maintains the rollingrelease-syncPR.Phase 0 manual checklist (DESIGN.md §6)
Setup
mikolalysenkofor now (D3). SetRELEASE_ROUTINE_ACTORSto every account any Claude routine runs as (todaymikolalysenko). Add the bot when it exists, and remove mik only once no routine runs as him.socket-patch-release-approverswith at least 2 humans, none of them inRELEASE_ROUTINE_ACTORS. Mirror it inRELEASE_APPROVERS.release: reviewers = the team, branchmain, admin bypass off, self-review prevention off;publish: no reviewers, branchmain, admin bypass off;pypiandrubygems.SocketDev/socket-patch/release.yml/ envpublish. Do this when PR 3 merges; it is an atomic cutover for npm. Confirm the org policy allows direct OIDC publish.release-blocker(exact name; the gate fails closed withlabel:until it exists),release,release:trainandrelease:sync;NTFY_TOPICandSLACK_WEBHOOK_URL(optional);release-blockeronremove <purl>garbage-collects the beforeHash blobs of every other patch still in the manifest, so a later offline rollback of those patches fails missing_blob #559, scan/get --json drop the agent-mode apply failure: exit 1 with failed: 0, the patch shown as "added", and no error anywhere (e.g. a read-only global ~/.m2) #424, npm VEX attests not_affected while a bundled (inBundle) copy of the same package@version stays unpatched #325/npm agent-mode apply never patches an npm-aliased install (lp@npm:left-pad), yet VEX attests the package not_affected #356/In a yarn classic Plug'n'Play project,vexattests a hosted patch as not_affected while the copy .pnp.js loads is still unpatched #519/npm vendored vex and vendor --check pass while a second registry copy of the patched package@version in the same package-lock.json stays unwired and installs unpatched #588 and Perf regression: bun/hosted wall +110% (1169ae68, #472) #578/Perf regression: vlt/hosted wall +127% (1169ae68, #472) #579.e2e_npm,e2e_pypi,e2e_gemande2e_scan --ignoredby hand on main. Drop any suite that is chronically red because of the public proxy fromlive-e2e, and document why.socket-patch-release(D1):contents: writeonly, no webhooks;SocketDev/socket-patchonly;RELEASE_APP_IDandRELEASE_APP_PRIVATE_KEYstored as secrets of envpublish.refs/tags/v*:Must-run probes
release/v0.0.0-rc.1,GITHUB_TOKENcanPOST /git/refsandcreateCommitOnBranch, and the result is averifiedcommit that ruleset 14265462 accepts.release-qa.ymlruns ci + 9 compat as one run. Check that:e2e-dockerandhosted-e2e(force) execute;rerun-failed-jobsworks.v0.0.0-rc.1tag thatGITHUB_TOKENcannot;🤖 Generated with Claude Code
Note
Medium Risk
Touches release versioning, lockfile stamping, and CI release gates; mistakes could block releases or allow version drift, though legacy stable releases stay gated with
--stable-onlyand extensive tests are added.Overview
Release train PR 1 lays the tested foundation for the weekly train in
docs/release-train/DESIGN.md: a new stdlibscripts/release.py(semver includingX.Y.Z-rc.N, offlinestampacross Cargo/npm locks, CHANGELOG cut/promote/sync-main,next-version, release-blocker gate, and related helpers), withscripts/version-sync.shdelegating tostampinstead of running networkednpm install --package-lock-only.scripts/release-lint.shand CI/release workflows are updated for rc versions, offline coherence (stamp --check+npm-lock-check),--stable-onlyon the legacy Release workflow, and a dedicatedrelease-sync→ main path using--tag-exists. The Version Bump workflow andbump-version.share removed; docs and the CHANGELOG header describerelease.pyand the train instead.Unit tests and fixtures cover stamping, changelog sync/fold, version selection, and blocker scenarios.
Reviewed by Cursor Bugbot for commit f43c3e3. Configure here.
Generated by Claude Code
CI follow-up: one PDM 2.6.1 Ubuntu job initially stopped on an API HTTP 504 before patching. Its single targeted retry passed: 34 supported/expected-refusal cases and two documented unsupported PEP582 cases, with no failed checks. The fresh artifact digest and source revision were verified.