Make the release guide and guards tell the truth before v0.15.0 - #808
Conversation
MIGRATING.md and the release tooling carried nine catalogued defects; this repairs the seven that live in the tree. The retro-labels on merged PRs and the Unreleased -> v0.15.0 promotion happen at tag time. - Re-file the two post-tag entries out of "# v0.14.0" into "# Unreleased": the #662 absent-expiry changes and the TimelineEventData pointer retype both landed in #703, after the tag. Proof: `git show go/v0.14.0:MIGRATING.md` contains neither heading. - Write the four entries the section was missing: #773 (the merge-safe Go reads return a transport failure verbatim -- errors.As and Retryable results move), #737 (four TS paginated methods now declare the ListResult they always returned), #735 (every generated Swift model has a public init -- recorded as NOT a break: no existing initializer changed shape, the 35 affected models were previously unconstructible so no consumer code exists against them), and the maxPages runtime cap (`1919e77f7`, a bare commit label-generated notes cannot list). - Fix the #650 miscount: `position` is conditional on the wire but was modeled before #723, so it is not one of "the seven" -- two of the seven new keys are conditional, plus `position`. Derived from the v0.14.0 and current Tool schemas and the bc3 partial's own `if`s. - Refresh the #604 table's Kotlin row to agree with the #750 entry and the KDoc it cites: the SerializationException lands in `decodeFailure`, the discriminator; `cause` mirrors it and is explicitly not one. - Rewrite the "# Not in this release" trailer: "Nothing is in flight ... merged at 9a819e4" was 53 commits stale. It now names the verification commit and the actual in-flight set, and dates the historical record below it. - Close the `make release` guard gap: it grepped seven of the ten files scripts/bump-version.sh writes, so a truncated bump could tag with the root package.json, typescript/src/client.ts or python/pyproject.toml constant stale. All three join the guard. Proven by mutating each file and watching `make release` refuse with the new message; restored by copy, verified with diff -q. - Release bodies now say that a change merged without a pull request appears only in MIGRATING.md, since generate_release_notes builds from merged PRs and structurally cannot list bare commits.
Sensitive Change Detection (shadow mode)This PR modifies control-plane files:
|
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Updates release documentation and pre-tag safeguards for v0.15.0.
Changes:
- Corrects and expands migration guidance.
- Adds missing version checks to
make release. - Documents the generated release-notes limitation.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
MIGRATING.md |
Corrects release history and adds missing migration entries. |
Makefile |
Expands pre-release version guards. |
.github/workflows/release-github.yml |
Identifies MIGRATING.md as authoritative. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c79f9ef96f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Seven fixes from the Copilot and Codex reviews, all taken: - The pyproject guard is an exact whole-line match (grep -qxF). The old regex left dots unescaped and the end unanchored, so a valid-PEP-440 "0.15+0" passed it and failed only in the Python release workflow, after other SDKs had published -- the exact post-tag failure class this PR exists to close. - typescript/package-lock.json (both SDK-version fields, via jq) and ruby/Gemfile.lock join the lockfile guards; bump-version.sh rewrites both, and neither was checked. All three new/changed guards proven by mutation: each refused with its message and exit 2, restored by copy, diff -q clean. - The trailer no longer claims every count in the guide was measured at 8fcb39a -- v0.13.0's totals state their own 9a819e4 baseline. The claim is scoped to the Unreleased section and the in-flight survey. - The #735 entry tells the two Swift shapes apart: updateGaugeNeedle was callable only as the nil-payload {} that bc3 400s; updateMyPreferences was not callable at all (outer requires the unconstructible payload). - The maxPages entry described sloppy-mode assignment wrong: [[Set]] on an inherited getter-only accessor creates no own property -- the assignment is silently ignored, not shadowed. - Three Ruby bare commits (2f21c9d, 3281530, 4785146) were consumer-visible -- crashes on mailto:/hostless server-supplied URLs became ApiError refusals -- and had entries nowhere. One combined entry records the class and the rescue that stops matching. - The release-body sentence no longer promises MIGRATING.md is a complete record of PR-less commits; it states the mechanism (the generated notes cannot see them) and points at the guide for consumer-visible changes.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8d527abd55
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex round 2: the request that returned the malformed Link or Location header was necessarily already sent — only the follow-up to the rejected target is prevented. Saying "before anything is sent" misled anyone reasoning about hooks or request counts.
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
Makefile:253
- This fixed-string search does not actually verify the root package version: a stale top-level
.versionstill passes if any nested object contains"version": "$(VERSION)". Since this guard decides whether tagging may proceed, read the root field withjq, as the lockfile checks already do.
@grep -qF '"version": "$(VERSION)"' package.json || \
{ echo "ERROR: Root package.json version does not match $(VERSION). Run 'make bump VERSION=$(VERSION)' first."; exit 1; }
Makefile:276
ruby/Gemfile.lockcurrently records the SDK version twice (the PATH spec and the CHECKSUMS entry).grep -qFsucceeds when either one matches, so an internally stale lockfile can still pass this pre-tag guard and fail only in the post-tag release. Require both tracked records to match.
@grep -qF 'basecamp-sdk ($(VERSION))' ruby/Gemfile.lock || \
{ echo "ERROR: ruby/Gemfile.lock records a stale SDK version. Run 'make bump VERSION=$(VERSION)' first."; exit 1; }
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd09a39fb2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- The #773 entry scoped the classification change to the four composites; Documents.Get installs markBodyReadFailures itself and Schedules.GetEntry delegates to getEntryWithBody, so direct getter callers see it too. The entry now names the getters and the composites built on them. - Both ruby Gemfile.lock guards anchor to the 4-space PATH-spec line with grep -qxF; the loose match could be satisfied by the version-bearing 2-space entry while the PATH spec stayed stale. Proven: mutating only the PATH-spec line now refuses with exit 2, and a clean tree passes the guard block. - The #735 entry claimed every Swift test file uses @testable import; the generator-only test files import BasecampGenerator plain. Narrowed to every test file that imports the SDK module.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9df7b6cdf6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex round 4 and Copilot round 2 converged on the same hole: grep -qF over package.json matches a "version" string anywhere in the document, so a stale top-level version passed while any nested metadata field carried the requested one. Proven literally: a crafted package.json with top-level 0.13.9 and a nested 0.14.0 satisfied the old grep and is refused by the new jq field read. typescript/package.json gets the same treatment -- same shape, same class. Copilot also wanted both version-bearing Gemfile.lock records checked, not just the PATH spec: a lockfile whose CHECKSUMS entry lags the PATH spec would pass the anchored guard and fail only post-tag. Both files now check both exact lines; a CHECKSUMS-only staleness is refused with its own message, proven by mutation with restore-by-copy.
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
Makefile:258
- This guard searches for the version text anywhere in the file rather than validating the exported constant. A stale
export const VERSIONplus a comment containing the requested value would pass and allow the wrong SDK version to be tagged. Match the complete declaration line, as the new Python guard does.
@grep -qF 'export const VERSION = "$(VERSION)"' typescript/src/client.ts || \
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac56b92a00
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Main moved by four while this PR was in review: #804, #807, #809, #810. Merged in; what each needed here, verified against the tree: - #809 and #810 wrote their own Unreleased entries when they merged (#805, #806) and carry `breaking` -- nothing to add. - #807 is CI-internal -- nothing to add. - #804 carries `breaking` but had no entry: resource-first discovery's second hop now rides the address-policed shared client, refusing special-use-space issuers non-retryably and dropping the caller's transport for that hop. Entry added beside #806's, with the variadic NewDiscoverer compile note and the remedies in policy order. - The trailer's verification commit moves to fa15fc1 and its in-flight list shrinks to what is actually in flight. Codex round 5's guard finding rides along: the pyproject check now parses the [project] table (awk section-scoped exact line) instead of matching a version assignment anywhere in the file. Proven with the literal bypass -- a stale [project].version plus an exact assignment in another table passed the old whole-file grep and is refused now; restore by copy, diff -q clean.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d2f3bb0bd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Copilot round 3 caught the release procedure's last unguarded step: the "# Unreleased" -> "# v(VERSION)" promotion was a hand edit nothing enforced, so a tag could ship with its notes still filed as unreleased. scripts/promote-migrating.sh now does the rewrite (exact-line, idempotent, refusing the both-headings and neither-heading states), bump-version.sh calls it as step 11, and make release guards both directions: the promoted heading must exist and "# Unreleased" must not. Proven: release refuses on today's tree; the script promotes a scratch copy correctly, is idempotent, and errors on both degenerate states. Codex round 6's two rides along: - The TS client guard is an exact whole-line match including the semicolon, so a comment carrying the assignment text cannot satisfy it while the real constant lags. - The #804 entry's remedy list dropped the address-class split in transcription: AllowLoopback re-admits loopback and nothing else, and Allow does not pierce the IANA tables -- for RFC 1918 the policy must be built without them, which is the implementation's own documented spelling. The entry now says so, and the #735 entry stops claiming UpdateGaugeNeedleRequest has a required member (its member is optional; the outer init exists because request models always got one).
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Copilot: delegating the promotion to a new script moved release-bump behavior out of the sensitive-change gate's sight -- bump-version.sh is listed in extra-patterns and the new script was not. It is now.
|
@codex review |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
MIGRATING.md:3925
- After
make bumppromotes the heading, this sentence points to a section that no longer exists:promote-migrating.shremoves the only exact# Unreleasedheading. Use release-neutral wording so the tagged v0.15.0 guide remains accurate.
Every change the `# Unreleased` section describes was merged by `fa15fc126`,
scripts/promote-migrating.sh:10
- This idempotence check searches the entire migration history, not the current top section. Once
# Unreleasedhas been promoted,make bump VERSION=<old release>will find that historical heading and report success after rolling all version files back; the Makefile guard uses the same global test. Restrict both checks to the first release heading so only the just-promoted section can satisfy them.
This issue also appears on line 19 of the same file.
if grep -qxF "# v$VERSION" "$FILE"; then
scripts/promote-migrating.sh:21
- After this promotion, nothing recreates
# Unreleased(the only repository references are this script and the guards). A later patch release with no migration-worthy changes therefore legitimately has neither heading—consistent with MIGRATING.md's “one section per release that breaks something” convention—butmake bumpwill abort here, andmake releasealso requires an otherwise-empty target heading. Handle the no-notes release case or automatically seed the next Unreleased section.
if ! grep -qxF "# Unreleased" "$FILE"; then
echo "ERROR: $FILE has neither '# Unreleased' nor '# v$VERSION' — nothing to promote." >&2
exit 1
|
Merging on Codex clean (round 8, "Didn't find any major issues" at the final head) + green CI (41 checks) + zero unresolved threads across eight review rounds. Copilot's last review covers everything except 370064c's one-line delta — the extra-patterns addition Copilot itself requested; it was re-requested and did not return within the usual window. |
|
Post-merge correction: Copilot's final-head review (here) returned 26 minutes before this merged, with three suppressed findings — including a genuine regression in the promotion script (a historical heading satisfied the global "already promoted" grep, so a backward bump would rewrite version files and make release would push the rollback before failing on the existing tag). The merge note above claimed the un-reviewed delta was one line; the review was in fact in, suppressed-block unread. All three findings are taken in #812, with the regression mutation-proven against the tagged script. |
* origin/main: deps(ts): bump the npm-dependencies group in /typescript with 3 updates (#825) deps(smithy): bump gradle-wrapper (#824) deps(kotlin): bump gradle-wrapper (#823) deps(go): bump github.com/oapi-codegen/runtime (#821) deps(conformance-ruby): bump webmock (#822) deps(ruby): bump webmock in /ruby in the bundler-dependencies group (#820) Bump version to 0.15.0 Make the release guide and guards tell the truth before v0.15.0 (#808) # Conflicts: # go/go.mod
…nt-feed-go-connector * origin/event-feed-foundations: deps(ts): bump the npm-dependencies group in /typescript with 3 updates (#825) deps(smithy): bump gradle-wrapper (#824) deps(kotlin): bump gradle-wrapper (#823) deps(go): bump github.com/oapi-codegen/runtime (#821) deps(conformance-ruby): bump webmock (#822) deps(ruby): bump webmock in /ruby in the bundler-dependencies group (#820) Bump version to 0.15.0 Make the release guide and guards tell the truth before v0.15.0 (#808)
…ent-feed-conformance-driver * origin/event-feed-go-connector: Event feed: the ring the buffer needed, the parser parity it claimed, and two honest declines deps(ts): bump the npm-dependencies group in /typescript with 3 updates (#825) deps(smithy): bump gradle-wrapper (#824) deps(kotlin): bump gradle-wrapper (#823) deps(go): bump github.com/oapi-codegen/runtime (#821) deps(conformance-ruby): bump webmock (#822) deps(ruby): bump webmock in /ruby in the bundler-dependencies group (#820) Bump version to 0.15.0 Make the release guide and guards tell the truth before v0.15.0 (#808)
…n it (#812) * Resolve the release heading from the top of the guide, not anywhere in it The tagged promote-migrating.sh matched "# v$VERSION" anywhere, so after v0.15.0's promotion a bump rolled backward to an old version found that version's HISTORICAL heading, reported "already promoted", and rewrote every version file backward -- and make release, using the same global grep, would have pushed the rollback to main before failing on the existing tag. A release with no migration-worthy changes also aborted mid-bump, after the version files were already rewritten. Copilot's final-head review of #808 flagged all of this 26 minutes before the merge and the suppressed block went unread -- the review discipline failed, not the reviewer. The script now resolves the CURRENT release heading as the first release heading in the file (the guide is newest-first; code blocks quoting old headings all sit below the newest section, pinned by a test case): - first heading "# Unreleased" -> promote; refuse if "# v$VERSION" already exists below (rollback). - first heading "# v$VERSION" -> idempotent no-op. - first heading an OLDER version -> the legitimate no-notes release: succeed without touching the file, say so. - first heading a NEWER version -> refuse: backward release. --check mode gives make release the same closed set of verdicts in place of the two global greps, and bump-version.sh calls the promotion as step 0, before any version file is touched, so every refusal aborts with a clean tree. scripts/test-promote-migrating.sh pins all of it (15 assertions; wired into check-targets and its own CI step, since check-targets membership alone is not CI coverage here). Mutation proof: the self-test run against the tagged script fails 5 assertions, the backward-bump regression case first among them. Also from that suppressed review: the trailer sentence naming "the # Unreleased section" now says "the newest release section above" -- promotion renames the heading, so the tagged guide pointed at a section that no longer existed. * Close the promotion's remaining fail-opens: order, placement, portability Round-one findings on #812, all taken: - The promote branch never compared the target against the released order: bump 0.9.0 with 0.15.0 released would mint "# v0.9.0" at the top and file today's notes behind history. The target must now be newer than the newest released section. - A misplaced "# Unreleased" below the first version heading rode through the idempotent and no-notes branches silently — its notes would miss the release. Both branches (both modes) refuse it. - The version comparator is pure component-wise arithmetic. Both bots claimed macOS's sort lacks -V; empirically Apple's sort 2.3 has it, but the portable spelling costs six lines and retires the question across BSD userlands -- and unlike a lexicographic fallback it orders 0.10.0 after 0.9.0, which a new test case pins. Self-test grows to 20 assertions; the three new failure cases run red against the pre-fix script (the backward-promote P1 first among them) and green after. * The version constant, not the headings, is the rollback authority Round two, all three taken: - Codex's P1: after a no-notes release the newest heading legitimately lags the SDK version, so heading comparisons cannot see a rollback landing between them -- packages at 0.16.0, newest heading v0.15.0, bump 0.15.5 sailed through. The script now reads the current version from go/pkg/basecamp/version.go (the constant make release re-verifies anyway) and refuses any target older than it, before the heading rules run. At bump time that constant still holds the pre-bump version; at release time the version guards have pinned it to the target, so the comparison degrades to a harmless equality. The self-test pins the authority via an env override so scratch cases control it. - A second "# Unreleased" heading is refused before any mutation, rather than promoting the first and leaving the release to fail late with the version files already rewritten. - test-promote-migrating is declared .PHONY like its sibling test goals, so a root path by that name cannot silently satisfy it. Self-test grows to 25 assertions; the four new ones fail against the pre-fix script (the rollback P1 first) and pass after. * Fail closed on a missing authority, and tell pending headings from history Round three, all three taken: - An unreadable or format-drifted version.go left CURRENT empty and current_blocks vacuously true -- the guard failed open into exactly the rollback it exists to refuse, and the self-test's override meant CI could never see it. An unresolvable current version is now fatal before anything is examined or mutated, pinned by a case that runs a copy of the script from outside the repo. - Promoting NEW notes to the version that already shipped (SDK released 0.16.0 without notes; fresh Unreleased; bump 0.16.0) filed them under a tag that already exists, and release would push main before failing on that tag. The promote branch now requires a strictly newer target; equality stays legal for the no-mutation paths. - A bump corrected to a higher version before committing stranded the first promotion: the second run called it a no-notes release and left the notes attributed to a version no tag would ever name. The release tag is the offline authority on what shipped: a newest heading with no tag is a PENDING promotion, which bump re-promotes to the corrected target and --check refuses instead of calling no-notes. Self-test grows to 30 assertions; the four new ones fail against the pre-fix script and pass after. * Tag absence proves nothing a shallow clone can act on Round four, all three taken -- and CI caught the P1 itself: the new spec-gates step runs in a default shallow checkout with no tag history, so `released` classified v0.15.0 as pending and the self-test failed three assertions there. Worse than the red CI: `make bump` from any --no-tags clone would have renamed real historical notes into the new release. - A local tag's absence now counts only when the checkout demonstrably knows release tags at all; a checkout that knows none fails closed with the fetch instruction, at the one point where the answer matters. The self-test pins both directions and is hermetic: it supplies the released-versions authority explicitly and never consults the repo's real tag state, so it passes identically in a tagless CI checkout. - The suppressed bypass rides along: a malformed current version ("dev") errored every arithmetic test in `newer`, which an `if` reads as false. CURRENT and the target are validated as X.Y.Z before any comparison; the self-test asserts the explicit diagnostic, since the pre-fix script exited 1 on one path only by the error landing in the blocking direction. * A present tag is proof; an absent one asks the remote Round five, both threads the same truth: one unrelated local v-tag is no evidence a missing tag never shipped -- shallow and filtered clones carry partial inventories, so the any-tag precondition still let a checkout holding v0.14.0 but not v0.15.0 re-promote v0.15.0's released notes. Absence is now confirmed against the authoritative source: a local hit is proof positive; a local miss asks git ls-remote --tags origin for exactly that ref, and when the remote cannot answer (offline, no origin) the question fails closed with the fetch instruction. The self-test's override remains the complete authority, so the suite stays hermetic and network-free; set-empty still models "cannot establish". * The remote tag is the only authority, and the self-test proves it live Round six, all four taken: - A local tag proves nothing either way: a release whose tag push failed leaves the tag behind with nothing published, so local presence is not release evidence -- released() now consults git ls-remote exclusively, failing closed when the remote cannot answer. - The authority paths were themselves untested: every self-test case ran the override, so a regression in the git path could re-promote history with CI green. The suite now builds a real repo with a file:// origin and exercises all four live paths -- remote-present, the local-only failed-push residue (correctly read as pending and carried forward), already-tagged --check refusal, and unreachable-remote fail-closed. - --check refuses a target that is already tagged, so re-running make release for a shipped no-notes version dies before pushing main. - The trailer binds to the v0.15.0 section by name: "the newest release section above" would silently re-target the next release's section while its fa15fc1 survey stayed a v0.15.0 fact. 40 assertions; the new ones fail against the pre-fix script three ways. * Heading judgments read the guide's prose, never its fences Copilot: every heading grep counted exact heading lines inside fenced code blocks -- the guide already quotes old version headings in its own derivation recipes, so a fence quoting "# Unreleased" read as a duplicate and one quoting the target as a rollback. All five judgments (first heading, duplicate target, duplicate Unreleased, misplaced Unreleased, newest released) now read through a fence-stripping pass; pinned by a fixture whose fences quote both headings and whose promotion must land anyway. Red: the case failed against the pre-fix script on the rollback misread. * Search captured prose, not a pipe a -q grep can kill Both bots: under pipefail, guide_prose | grep -q turns a successful early match into a failure when grep closes the pipe and awk dies on SIGPIPE writing the rest of a large guide -- an existing historical target could bypass the rollback check. The prose pass now runs once into a variable and every judgment searches it as a herestring; no producer process, nothing to SIGPIPE. Honest red account: the large-guide case passes on both sides HERE, because BSD grep reads through under -q; GNU grep -- CI's grep -- exits at first match, which is where the mechanism bites. The case ships as the Linux regression net, and the fix stands on the mechanism both reviewers named rather than a local reproduction. * One ordering authority: the newest shipped tag Round: both bots found the version-constant gates judging the wrong thing from opposite directions. A mistaken 0.17.0 bump could not be corrected downward to a still-forward 0.16.0 (the constant moved with the mistake and read the correction as a rollback), while hand-editing the constants back to an untagged 0.15.5 after a no-notes 0.16.0 release sailed through --check (CURRENT == VERSION, VERSION > TOP) and pushed the rollback to main. The constants are gone from the ordering entirely. The remote tag list is fetched once and is the single authority: the target must be strictly newer than the newest shipped release -- one gate subsuming rollback, re-release, and hand-edit -- and everything UNSHIPPED is deliberately correctable: a pending heading re-promotes to the new target in either direction. The guide-heading order check and the CURRENT checks fell out as redundant. Test suite migrated to the shipped semantics (46 assertions); the two new scenarios red-proved against the previous script: the downward correction was refused as a rollback, and the hand-edited --check passed. * Refuse abandoned sections, and keep the rewrites out of fences too Round: three more, all taken. An abandoned unshipped section below a fresh "# Unreleased" would be orphaned forever the moment promotion passed it -- every unshipped version heading below Unreleased now refuses with the fold-or-re-title instruction. Both awk rewrite passes track fence state, so a fenced "# Unreleased" example BEFORE the real section survives promotion (the judgments already read prose; the rewrites now match). And the SIGPIPE-net case was restaged onto the shipped-authority semantics -- its old target died at the ordering gate before reaching the check it claimed to cover. 50 assertions; the abandoned-section and leading-fence cases fail against the pre-fix script. * Widen the fence grammar, and make two cases prove what they claim Round: three test/parsing nits taken. The fence detector now recognizes tilde fences and up-to-three-space indents in all three passes (the judgments and both rewrites), pinned by an exotic-fence fixture. The remote-tag fixture snapshots the guide so a released-classification regression cannot pass by re-promoting. And the SIGPIPE-net case's shipped maximum drops below its target so the ordering gate cannot shadow the duplicate check it covers. * Give the fence pass CommonMark's closing rule, and every branch the orphan check Both bots converged on the same truth: a fence closes only with the same delimiter character, at least the opening run's length, and nothing but spaces after — so a ``` line inside a ```` block, or backticks inside a tilde fence, is content, and toggling on every delimiter run could hide the real Unreleased heading behind a phantom close. One fence automaton is now interpolated into the prose pass and both rewrites, so the judgments and the mutations cannot disagree about what is prose. The abandoned-section refusal also becomes a function called from every branch, not just Unreleased: an unshipped section can no longer hide below a promoted or pending target. The remote fixture ships a tag for each heading it documents, as history would. * Make the self-test seam an argument, not an environment variable The PROMOTE_MIGRATING_RELEASED override rode along into any bump or release that inherited it from the caller's shell: a stale exported list silently displaced the remote as the ordering authority, so a hand-edited rollback could pass --check while a newer tag sat on origin. The seam is now an explicit --released flag that only the self-test passes; the environment is ignored outright, pinned by a fixture-remote case that fails against the env-honoring version. Empty stays fail-closed, and the harness header no longer describes the long-gone current-version authority.
The release surface carried nine catalogued defects under the standing rule "don't tag before fixing". This PR repairs the seven that live in the tree; the other two happen at tag time (
# Unreleased→# v0.15.0promotion withmake bump; the version-specific release-body line). Doc + tooling only — no SDK behaviour. While it was in review,mainmoved by four merges (#804, #807, #809, #810); those are absorbed too.What this fixes
Two post-tag entries re-filed out of
# v0.14.0into# Unreleased: the AuthorizationInfo.ExpiresAt fabricates 0001-01-01T00:00:00Z, and named time wrappers are invisible to both timestamp guards #662 absent-expiry changes and theTimelineEventDataretype (both landed in ExpiresAt absence: zero-time sentinel, Expiry(), and wrapper-aware timestamp guards #703, after the tag —git show go/v0.14.0:MIGRATING.mdcontains neither heading).Five missing entries written (four from the original catalogue, one found by review):
Get/GetEntryand the composites built on them — return a transport failure verbatim instead of stamping it permanently malformed.ListResultthey always returned.updateMyPreferenceswas uncallable outright,updateGaugeNeedlecallable only as the nil-payload{}bc3 400s).1919e77f7(bare commit): maxPages enforced at runtime in TS/Ruby.ApiErrorclass (bare commits2f21c9de7,328153020,478514642, surfaced by Codex):mailto:/hostless server-supplied URLs in Link headers, redirects, and the download hop used to crash with rawURI/ArgumentErrorexceptions; all three now refuse withApiError. One combined entry records the class and the rescue that stops matching.Tool.name and Tool.enabled are @required but never emitted — affects GetTool, CreateTool and UpdateTool #650 recount re-derived from both schemas (two of the seven new keys conditional, plus
position, which was already modeled).Isolate the response decoder from the auth/transport phase in Kotlin and Swift BaseService #604's Kotlin row now names
decodeFailureas the discriminator, matching the KDoc.Trailer rewritten: verification commit and the true in-flight set (now
fa15fc126; the event-feed stack and two drafts).make releaseguard gap closed, then hardened by review: the target originally grepped seven of the ten filesscripts/bump-version.shwrites. Now: root and TSpackage.jsonread viajqfield access; TS client constant exact;python/pyproject.tomlparsed from its[project]table (awk section-scoped); bothGemfile.lockrecords (PATH spec + CHECKSUMS) exact whole-line matched in both lockfiles; TSpackage-lock.jsonboth fields viajq. Every guard mutation-proven, including the literal bypasses (nested JSON"version"field; aversion =assignment in a foreign TOML table) demonstrated against the old checks and refused by the new ones. Restore by copy,diff -qclean, real exit codes captured.Release bodies state the notes' blind spot: generated notes are built from merged PRs and cannot see bare commits; MIGRATING.md records consumer-visible changes regardless of how they merged.
Main-merge absorption (Judge the advertised OAuth issuer's address, not just its spelling #804/Quiet known-noise CodeQL alerts without losing coverage #807/Refuse redirects on the signed download hop in every SDK #809/Police the endpoints a discovered issuer names, not only the issuer #810): Refuse redirects on the signed download hop in every SDK #809/Police the endpoints a discovered issuer names, not only the issuer #810 wrote their own entries and carry
breaking; Quiet known-noise CodeQL alerts without losing coverage #807 is CI-internal; Judge the advertised OAuth issuer's address, not just its spelling #804 was breaking-labeled with no entry — its entry is added (address-policed discovery hop 2, the variadicNewDiscoverercompile note, remedies in policy order).Labels
breakingapplied to: #703, #716, #723, #725, #726, #754, #779, #782, #796 (joining #772; #804/#809/#810 arrived already labeled). Declined in writing: #749 (not a break), #781, #727, #751 (rationales in the thread history).Verification
LC_ALL=C make doc-constants-checkgreen (REAL_EXIT captured) after every MIGRATING edit.# Unreleasedentry cross-checked againstgit log go/v0.14.0..origin/main; Sweep stale Python modules, and ask TS and Ruby whether their clients reach what was generated #770/Check that an operation lands on the same service in every SDK #771 ruled out as unentried breaks by inspection.Not doing (declined in writing): a CHANGELOG — MIGRATING.md plus label-generated notes is the convention; a third surface is a third thing to drift.