Skip to content

Resolve the release heading from the top of the guide, not anywhere in it - #812

Open
jeremy wants to merge 5 commits into
mainfrom
release-guard-repair
Open

Resolve the release heading from the top of the guide, not anywhere in it#812
jeremy wants to merge 5 commits into
mainfrom
release-guard-repair

Conversation

@jeremy

@jeremy jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member

Repairs the release-guard regression #808 shipped, flagged by Copilot's final-head review (suppressed block) 26 minutes before the merge and not absorbed — the review discipline failed, not the reviewer. All three suppressed findings taken:

  1. The historical-heading bypass (the dangerous one). The tagged promote-migrating.sh matched # v$VERSION anywhere in MIGRATING.md, so after v0.15.0's promotion, make bump VERSION=0.14.0 found the historical v0.14.0 heading, reported "already promoted", and rewrote every version file backward; make release used the same global grep and would have pushed the rollback to main before failing on the existing tag. The script now resolves the current release heading as the first release heading in the file, with a closed verdict set: promote / idempotent no-op / no-notes forward release (succeed, untouched) / backward release (refuse) / rollback-duplicate (refuse). make release calls --check mode instead of the two global greps.

  2. The no-notes release aborted mid-bump. A release with nothing migration-worthy legitimately has neither heading (the guide's one-section-per-release-that-breaks-something convention); the old script errored on it — after the version files were already rewritten. The no-notes forward case now succeeds explicitly, and the promotion runs as bump step 0, before any file is touched, so every refusal aborts with a clean tree.

  3. The tagged trailer pointed at a removed heading. "Every change the # Unreleased section describes…" — promotion renames that heading, so the v0.15.0 guide referenced a section that no longer existed. Now release-neutral ("the newest release section above").

Proofs. scripts/test-promote-migrating.sh: 15 assertions covering every verdict, including a fence-quoted-heading case; wired into check-targets and its own CI step (check-targets membership alone is not CI coverage in this repo). Mutation proof: the self-test run against the tagged script fails 5 assertions — the backward-bump regression first among them — then passes 15/15 restored. make release VERSION=0.15.0 on this branch prints the idempotent verdict and proceeds to the expected dirty-tree stop, proving the --check wiring executes.


Summary by cubic

Resolve the current release heading from the top of MIGRATING.md and use the SDK’s version constant (and tag awareness) to prevent rollbacks. Old behavior matched any historical “# v” and let backward bumps and mid-bump aborts pass; new behavior treats the first release heading as current, validates versions, accounts for tagless clones, and returns explicit promote/check verdicts.

  • Key behavior changes

    • scripts/promote-migrating.sh: resolves the current release as the first heading; supports no-notes releases; adds --check for make release.
    • Uses the SDK version constant from go/pkg/basecamp/version.go as rollback authority; refuses any target older than it; fails closed if unreadable; validates both current and target as X.Y.Z.
    • Requires a strictly newer target when promoting notes (prevents tagging an already released version); compares the target against the newest released section to avoid filing notes behind history.
    • Distinguishes a pending (unreleased) top heading from history: bump re-promotes it to the corrected target; --check refuses pending.
    • Refuses multiple “# Unreleased” headings and any misplaced “# Unreleased” below the top section.
    • Detects tagless/shallow clones: if no release tags are visible, fails closed with a fetch instruction rather than misclassifying history as pending.
    • Uses a pure arithmetic semver comparator (no sort -V dependency).
  • Tooling and CI

    • scripts/bump-version.sh runs the promotion as step 0 to fail early before version files change.
    • Makefile uses ./scripts/promote-migrating.sh --check; adds .PHONY test-promote-migrating and includes it in check-targets.
    • .github/workflows/test.yml runs a hermetic self-test that pins tag visibility and current-version authority and covers all verdicts; MIGRATING.md wording now references “the newest release section above.”

Written for commit 90d6489. Summary will update on new commits.

Review in cubic

…n 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.
Copilot AI balanced review requested due to automatic review settings August 22, 2026 19:21
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@github-actions

Copy link
Copy Markdown
Contributor

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/test.yml
  • scripts/bump-version.sh
  • scripts/promote-migrating.sh

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

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

Hardens release-guide promotion and rollback checks.

Changes:

  • Resolves only the newest migration heading.
  • Adds no-notes handling and release validation.
  • Adds regression tests and CI coverage.

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 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
scripts/promote-migrating.sh Implements heading-state validation.
scripts/test-promote-migrating.sh Tests promotion and rollback cases.
scripts/bump-version.sh Runs migration validation before mutation.
Makefile Integrates checks into bump, release, and test targets.
.github/workflows/test.yml Runs the self-test in CI.
MIGRATING.md Makes trailer wording release-neutral.
Suppressed comments (1)

scripts/promote-migrating.sh:54

  • Comparing only with the newest documented heading loses release history in the newly accepted no-notes case. After v0.16.0 ships without notes while this file still starts at v0.15.0, both make bump VERSION=0.15.1 and the later --check accept 0.15.1 as forward, so the release target can again push a version rollback to main. Compare the bump against the currently recorded SDK version and the release check against existing release tags (or persist a no-notes release marker), and add a sequential no-notes-then-rollback regression case.
    if newer "$VERSION" "$TOP"; then
      # Legitimate: a release with nothing migration-worthy has no section,
      # per the guide's one-section-per-release-that-breaks-something rule.
      echo "$FILE: no '# Unreleased' section — v$VERSION ships without migration notes (newest documented: v$TOP)."

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/promote-migrating.sh

@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: 9f742e562f

ℹ️ 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 scripts/promote-migrating.sh Outdated
Comment thread scripts/promote-migrating.sh
Comment thread scripts/promote-migrating.sh
…lity

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.
@jeremy
jeremy requested a balanced review from Copilot August 22, 2026 19:40
@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: 9af0966b0f

ℹ️ 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 scripts/promote-migrating.sh

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

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

Makefile:306

  • Unlike the repository's other test/check goals (for example, Makefile:1289 and Makefile:1543), this new target is not declared phony. If a root path named test-promote-migrating exists, both the CI invocation and check-targets will treat it as up to date and silently skip this regression suite.
test-promote-migrating:

Comment thread scripts/promote-migrating.sh
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.
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Also taken from the suppressed block of the round-two review: test-promote-migrating is now declared .PHONY like its sibling test goals, so a root path by that name cannot silently satisfy the target.

@jeremy
jeremy requested a balanced review from Copilot August 22, 2026 19:56
@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 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread scripts/promote-migrating.sh

@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: 11c886d246

ℹ️ 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 scripts/promote-migrating.sh
Comment thread scripts/promote-migrating.sh
…story

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

jeremy commented Aug 24, 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 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

scripts/promote-migrating.sh:40

  • Checking only for an empty value does not fail closed on a malformed matched constant. For example, const Version = "dev" sets a nonempty CURRENT; each arithmetic test in newer returns an error status, which is treated as false while current_blocks runs in an if, so the rollback authority is bypassed. Validate CURRENT as the supported numeric three-component format before any comparison.
if [ -z "$CURRENT" ]; then
  # Fail closed: with no rollback authority, current_blocks would accept
  # every target — including the rollback it exists to refuse.
  echo "ERROR: cannot read the current SDK version from go/pkg/basecamp/version.go — refusing to run without the rollback authority." >&2
  exit 1
fi

Comment thread scripts/promote-migrating.sh 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: 4a980b04a4

ℹ️ 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 scripts/promote-migrating.sh
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.
@jeremy
jeremy requested a balanced review from Copilot August 24, 2026 08:14
@jeremy

jeremy commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ 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 1 comment.

Comment on lines +79 to +85
local any
any=$(git -C "$(dirname "$0")/.." tag -l 'v[0-9]*' 2>/dev/null | head -1)
if [ -z "$any" ]; then
echo "ERROR: no release tags visible in this checkout, so whether '# v$1' ever shipped cannot be established (shallow or --no-tags clone?). Run 'git fetch --tags' and retry." >&2
exit 1
fi
echo "$FILE already promoted to v$VERSION"
exit 0
fi
git -C "$(dirname "$0")/.." tag -l "v$1" 2>/dev/null | grep -qx "v$1"

@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: 90d6489509

ℹ️ 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 on lines +80 to 84
any=$(git -C "$(dirname "$0")/.." tag -l 'v[0-9]*' 2>/dev/null | head -1)
if [ -z "$any" ]; then
echo "ERROR: no release tags visible in this checkout, so whether '# v$1' ever shipped cannot be established (shallow or --no-tags clone?). Run 'git fetch --tags' and retry." >&2
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require a complete tag inventory before re-promoting

In a shallow or filtered clone with a nonempty but incomplete tag set, finding one unrelated v* tag is not proof that the missing v$TOP never shipped. For example, if only v0.14.0 is present while the historical top section is v0.15.0, bumping from 0.15.0 to 0.16.0 makes released 0.15.0 return false and rewrites the shipped 0.15.0 notes as 0.16.0 notes. Fresh evidence beyond the earlier tagless-checkout finding is that the final code fails closed only when zero tags are visible; require verified-complete tags or query the authoritative remote before treating absence as unreleased.

AGENTS.md reference: AGENTS.md:L149-L154

Useful? React with 👍 / 👎.

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