Skip to content

Release train PR 1: release.py core (offline stamp, rc versions, CHANGELOG sync/fold, blocker gate) - #643

Merged
Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
feat/release-train
Oct 5, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
feat/release-train

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Review complete on f43c3e36; ready to merge as-is from this review. 339 successful checks, 6 skipped, and all six workflows successful. Bugbot is clear on this commit; no unresolved review threads or new actionable findings. Four release-lint assertions now handle both terminal stderr and GitHub Actions stdout annotations. The complete Python suite passes with Actions settings: 214 passed and one platform-specific skip. Independent source review and 48 additional changelog scenarios are clear.

Release train PR 1: release.py core, offline stamp, rc-aware release-lint

Summary

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: the X.Y.Z / X.Y.Z-rc.N grammar 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-check
    • next-version: derived from git tags and burned release/* branches, never from main's own version.
    • changelog cut|promote|sync-main|check
    • sync-main
    • notes
    • blockers: the §3.5 release-blocker gate.
  • scripts/version-sync.sh is now a thin wrapper over release.py stamp. The networked npm install --package-lock-only is 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:
    • accepts X.Y.Z-rc.N;
    • check 2 is now the offline stamp --check plus npm-lock-check;
    • check 3 is rc-aware;
    • new --stable-only and --tag-exists flags.
  • ci.yml release-readiness: a same-repo release-sync PR into main runs release-lint.sh --tag-exists. Every other PR behaves as before.
  • Deleted: version-bump.yml and scripts/bump-version.sh. Every reference is fixed (release.yml comments, docs/releasing.md, the CHANGELOG header, the ci.yml shellcheck list).
  • Tests: scripts/tests/test_release.py plus fixtures, which include a recorded #454/#456 PR-merge close. CI's existing unittest discover already runs them.

Design decisions (maintainer decisions D1–D4 applied)

  • D2: version and CHANGELOG reach main on every rc.
    • sync-main brings 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].
    • At promotion, changelog promote --rc K folds 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.
    • Blocks are matched exactly and counted as a multiset everywhere:
      • the fold keeps every occurrence;
      • removal takes one occurrence per shipped block;
      • rc sections on main are charged against the stable's blocks before [Unreleased] is touched.
    • So next-version and changelog cut give byte-identical results whether or not the release-sync PR merged.
    • Version selection reads only tags and release/* branches. An unmerged sync PR never changes what gets cut.
    • release-lint accepts main at an rc.
  • D4: APPROVED_MAJORS = (5,), so the first train cut is 5.0.0-rc.1. A breaking heading that would open an unapproved major is refused, and a major is never skipped.
  • D3: blocker gate.
    • mikolalysenko is a routine actor, so he is never trusted to clear a blocker.
    • Config lists are parsed strictly, and an empty or degenerate config fails closed.
    • A PR-merge auto-close (commit_id: null) resolves only through a PR merged by the close actor whose merge commit is in the base.
    • since comes from merge-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.
    • A missing or renamed label, a vanished label, and a deleted or transferred issue all block. Any API error blocks.
  • D1 (App + tag ruleset) is implemented in PR 3. Until the ruleset (S9) is live, the 35-day lookback bounds what a forged tag can hide. S9 is a hard prerequisite for any live gate run.
  • CRLF: a Windows core.autocrlf checkout works. CHANGELOG transforms keep CRLF, and every stamped file keeps its own line endings, so stamp --check is a byte no-op there.
  • No half-synced trees: sync-main computes 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 status is clean):

PASS  version-sync.sh 4.0.0 -> no diff (Synced version to 4.0.0)
PASS  release-lint.sh passes on main at 4.0.0
PASS  stamp 5.0.0-rc.1 x2 offline: identical bytes (18 files changed, digest 4c0128e8f32d3674)
PASS  stamp --check 5.0.0-rc.1 is a no-op after the stamp
PASS  stamp diff is inside the release allowlist
PASS  release-lint.sh passes at 5.0.0-rc.1: release-lint: all checks passed for 5.0.0-rc.1
PASS  release-lint --stable-only refuses 5.0.0-rc.1
PASS  cargo build --locked -j4 -p socket-patch-cli on the stamped tree (socket-patch 5.0.0-rc.1)
PASS  npm install --no-save --ignore-scripts in npm/socket-patch (added 4 packages in 4s)
PASS  npm install --no-save left the stamped files as stamped
PASS  5.0.0-rc.1 < 5.0.0
PASS  next-version on this branch's CHANGELOG + real tags = 5.0.0-rc.1
PASS  sync-main --check: main is in sync with the real tags (no train tags yet)
PASS  tree restored, git status 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 in test_release.py, all passing.

Accept-list items covered by scripts/tests/test_release.py:

  • Versions and the main CHANGELOG snapshot:
    • version selection on the main CHANGELOG snapshot gives 5.0.0-rc.1 (test_first_train_on_the_main_snapshot_is_5_0_0_rc_1);
    • rc.2 while rc.1 is pending, with the sync PR merged or not;
    • after v5.0.0 with the sync PR unmerged, 5.0.1-rc.1 or 5.1.0-rc.1;
    • burned branches are skipped;
    • an unapproved major is refused, and a major is never skipped.
  • Blocker fixtures:
    • a missing or renamed label, and case variants of the label;
    • a label that vanished without an unlabeled event, and a deleted, converted or transferred issue;
    • an empty, unset or malformed actor config, and the newline-, space- and @-separated forms;
    • since comes from the merge-base, a forged high tag cannot move it past the lookback, and a since after the base fails closed;
    • a PR-merge close resolves through the merge commit only, and a manual close after an unrelated mentioning PR does not;
    • an open issue untouched for 200 days blocks;
    • closed by a commit that is not in the base blocks, and closed by an ancestor commit passes;
    • closed by a trusted human after t blocks;
    • unlabelled or closed by a routine actor (mik included) blocks;
    • a trusted unlabel passes;
    • an API error blocks.
  • D2 scenarios:
    • an rc syncs to main, and release-lint passes on that rc;
    • a stable fold with an abandoned later rc gives the same end state whether or not the sync PR merged;
    • sync-main is idempotent;
    • byte-identical cuts with the sync PR merged or unmerged, including:
      • a repeated identical entry (also repeated across folded rcs, and re-added after a promotion);
      • abandoned later rcs;
      • subsection order left over from shipped history;
    • a hotfix promoted beside a train rc of the same core;
    • CRLF CHANGELOG transforms and a CRLF stamp;
    • sync-main writes nothing when the stamp refuses;
    • release-lint (no flags and --tag-exists) on a sync-mained rc tree;
    • hermetic stamp tests (baselines 4.0.0 and an rc);
    • npm dependency drift caught offline.

What PRs 2–4 do next

  • PR 2, release-qa.yml + smoke:
    • one workflow_dispatch run at release/v<V> builds the 14 archives and 15 npm tarballs once, smokes those bytes, and calls ci.yml plus the 9 compat workflows through workflow_call;
    • adds release_smoke.py (Tier 0/1), the verify-qa verdict, and the Tier 0 step in the ci.yml e2e-build leg;
    • must pass probes P1 and P2 first.
  • PR 3, release.yml rc path:
    • plan (blocker gate, green-SHA window, version), then cut (a signed createCommitOnBranch stamp commit with a tree assertion), then QA with reruns and a fallback, then publish in env publish (the App mints the tag, then crates → npm next → GitHub prerelease), then verify-channels and report;
    • adds the Latest-release audit;
    • deletes publish-*.yml and dispatch-publish.sh;
    • needs P3, S8 and S9 before merge, and S3 and S4 at merge.
  • PR 4, stable + hotfix + docs:
    • the approve environment gate;
    • stable plan/cut (rc commit + stamp + changelog promote), re-finding the rc's full-QA run;
    • hotfix mode with waive_soak;
    • cancelling parked runs;
    • docs/releasing.md runbook, ROUTINE.md and the CHANGELOG header.
    • After that, the daily release-train routine maintains the rolling release-sync PR.

Phase 0 manual checklist (DESIGN.md §6)

Setup

Must-run probes

  • P1 On scratch release/v0.0.0-rc.1, GITHUB_TOKEN can POST /git/refs and createCommitOnBranch, and the result is a verified commit that ruleset 14265462 accepts.
  • P2 release-qa.yml runs ci + 9 compat as one run. Check that:
    • the job count is accepted;
    • no concurrency group stalls;
    • e2e-docker and hosted-e2e (force) execute;
    • artifact names don't collide;
    • rerun-failed-jobs works.
    • Also measure p95 wall time.
  • P3 With immutable releases and the tag ruleset on:
    • the App token can create a scratch v0.0.0-rc.1 tag that GITHUB_TOKEN cannot;
    • a draft created on that existing tag accepts uploads;
    • publishing the draft keeps the tag at its SHA.

🤖 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-only and 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 stdlib scripts/release.py (semver including X.Y.Z-rc.N, offline stamp across Cargo/npm locks, CHANGELOG cut/promote/sync-main, next-version, release-blocker gate, and related helpers), with scripts/version-sync.sh delegating to stamp instead of running networked npm install --package-lock-only.

scripts/release-lint.sh and CI/release workflows are updated for rc versions, offline coherence (stamp --check + npm-lock-check), --stable-only on the legacy Release workflow, and a dedicated release-sync → main path using --tag-exists. The Version Bump workflow and bump-version.sh are removed; docs and the CHANGELOG header describe release.py and 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.

…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>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@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.

Stale Bugbot comment from a previous run.

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 3, 2026 04:18
@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: Ready for review at 056d955 (marked ready from draft).

  • CI: 97/97 non-skipped checks green on the head SHA (3 skipped by path filters); mergeable with main.
  • Bugbot: reviewed 056d955, no issues found; no unresolved review threads.
  • Reviewer focus: this deletes version-bump.yml / scripts/bump-version.sh and makes version-sync.sh offline, and the blocker gate (D3) assumes the S9 tag ruleset before any live run.

Generated by Claude Code

@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

Review updated for f43c3e366c16ddd37afa0b37f25aace41509d350: Ready to merge as-is from this review. 339 successful checks, 6 skipped, and all six workflows successful. Bugbot is clear on this commit; no unresolved review threads or new actionable findings.

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 GITHUB_ACTIONS=true (214 passed, 1 existing macOS-specific skip). All 82 release tests passed in that environment, and the five affected tests also passed with ordinary terminal output. Independent review covered the offline stamp, version/ref selection, blocker gate, workflow integration, and changelog transforms; 48 additional modeled scenarios confirmed identical next-cut bytes across partial sync merges, repeated entries, and abandoned release candidates.

The tested files match this commit, the diff is clean, and it merges cleanly with main 045d7ec. No remaining actionable finding from the review. Ready for review has been restored. GitHub still requires the normal human approval before merge.

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.

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review

Please review the test correction on f43c3e366c16ddd37afa0b37f25aace41509d350.

@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.

✅ 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.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 5490c42 into main Oct 5, 2026
388 of 389 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the feat/release-train branch October 5, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants