Skip to content

Make the release guide and guards tell the truth before v0.15.0 - #808

Merged
jeremy merged 9 commits into
mainfrom
release-repair
Aug 22, 2026
Merged

Make the release guide and guards tell the truth before v0.15.0#808
jeremy merged 9 commits into
mainfrom
release-repair

Conversation

@jeremy

@jeremy jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member

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.0 promotion with make bump; the version-specific release-body line). Doc + tooling only — no SDK behaviour. While it was in review, main moved by four merges (#804, #807, #809, #810); those are absorbed too.

What this fixes

  1. Two post-tag entries re-filed out of # v0.14.0 into # 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 the TimelineEventData retype (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.md contains neither heading).

  2. Five missing entries written (four from the original catalogue, one found by review):

  3. 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).

  4. Isolate the response decoder from the auth/transport phase in Kotlin and Swift BaseService #604's Kotlin row now names decodeFailure as the discriminator, matching the KDoc.

  5. Trailer rewritten: verification commit and the true in-flight set (now fa15fc126; the event-feed stack and two drafts).

  6. make release guard gap closed, then hardened by review: the target originally grepped seven of the ten files scripts/bump-version.sh writes. Now: root and TS package.json read via jq field access; TS client constant exact; python/pyproject.toml parsed from its [project] table (awk section-scoped); both Gemfile.lock records (PATH spec + CHECKSUMS) exact whole-line matched in both lockfiles; TS package-lock.json both fields via jq. Every guard mutation-proven, including the literal bypasses (nested JSON "version" field; a version = assignment in a foreign TOML table) demonstrated against the old checks and refused by the new ones. Restore by copy, diff -q clean, real exit codes captured.

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

  8. 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 variadic NewDiscoverer compile note, remedies in policy order).

Labels

breaking applied 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

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.

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.
Copilot AI balanced review requested due to automatic review settings August 22, 2026 05:57
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/release-github.yml
  • .github/workflows/sensitive-change-gate.yml
  • scripts/bump-version.sh

Shadow mode — this check is informational only. When activated, changes to these paths will require approval from a maintainer.

@github-actions github-actions Bot added the github-actions Pull requests that update GitHub Actions label Aug 22, 2026
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI 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.

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.

Comment thread Makefile
Comment thread MIGRATING.md Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread MIGRATING.md Outdated
Comment thread MIGRATING.md Outdated
Comment thread MIGRATING.md Outdated
Comment thread Makefile Outdated
Comment thread .github/workflows/release-github.yml Outdated
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.
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread MIGRATING.md Outdated
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.
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI 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.

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 .version still passes if any nested object contains "version": "$(VERSION)". Since this guard decides whether tagging may proceed, read the root field with jq, 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.lock currently records the SDK version twice (the PATH spec and the CHECKSUMS entry). grep -qF succeeds 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; }

Comment thread MIGRATING.md Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread Makefile Outdated
Comment thread MIGRATING.md Outdated
Comment thread MIGRATING.md Outdated
- 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.
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread Makefile Outdated
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.
@jeremy
jeremy requested a balanced review from Copilot August 22, 2026 07:07
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI 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.

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 VERSION plus 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 || \

Comment thread MIGRATING.md

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread Makefile Outdated
jeremy added 2 commits August 22, 2026 01:43
* origin/main:
  Police the endpoints a discovered issuer names, not only the issuer (#810)
  Refuse redirects on the signed download hop in every SDK (#809)
  Quiet known-noise CodeQL alerts without losing coverage (#807)
  Judge the advertised OAuth issuer's address, not just its spelling (#804)
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.
@jeremy
jeremy requested a balanced review from Copilot August 22, 2026 08:45
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread MIGRATING.md Outdated
Comment thread Makefile

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread MIGRATING.md Outdated
Comment thread Makefile Outdated
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).
@jeremy
jeremy requested a balanced review from Copilot August 22, 2026 09:13
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread scripts/bump-version.sh
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 70623b3f57

ℹ️ 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: 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.
@jeremy
jeremy requested a balanced review from Copilot August 22, 2026 09:24
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 370064cf40

ℹ️ 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 AI 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.

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 bump promotes the heading, this sentence points to a section that no longer exists: promote-migrating.sh removes the only exact # Unreleased heading. 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 # Unreleased has 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—but make bump will abort here, and make release also 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

@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

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.

@jeremy
jeremy merged commit ec3a669 into main Aug 22, 2026
46 checks passed
@jeremy
jeremy deleted the release-repair branch August 22, 2026 09:55
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

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.

jeremy added a commit that referenced this pull request Aug 24, 2026
* 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
jeremy added a commit that referenced this pull request Aug 24, 2026
…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)
jeremy added a commit that referenced this pull request Aug 24, 2026
…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)
jeremy added a commit that referenced this pull request Aug 29, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github-actions Pull requests that update GitHub Actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants