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

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