Skip to content

ci(release): fall back to generated notes, and add the changelog rotation script - #237

Merged
Nic-dorman merged 6 commits into
mainfrom
nic/v2-1107-release-notes-fallback
Oct 27, 2026
Merged

Nic-dorman merged 6 commits into
mainfrom
nic/v2-1107-release-notes-fallback

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Every ant release since 0.3.6 has shipped with empty notes. #183 made the release workflow ship the CHANGELOG.md section matching the tag's base version. Since #187 (V2-1142), a missing section publishes a placeholder instead of failing. But nothing at the train cut ever rotates [Unreleased] into ## [X.Y.Z]. So all nine tags from ant-cli-v0.3.7 to today's ant-cli-v0.3.12-rc.1 (five of them stable) shipped "No changelog entry was recorded for …".

This PR fixes the process in two places, and makes AGENTS.md the guide for every coding agent. The content backfill for 0.3.7–0.3.12 is a separate PR.

1. scripts/rotate-changelog.sh X.Y.Z [date] [changelog]: the rotation step the cut tooling never had. For the RM: please wire it into the rc.1 cut, on the rc branch, before tagging. It moves the [Unreleased] entries unchanged under a new ## [X.Y.Z] - <date> and keeps an empty [Unreleased] heading above it.

  • It accepts X.Y.Z-rc.N and drops the suffix.
  • It is idempotent: an rc.2 re-cut finds [X.Y.Z] already filled and does nothing.
  • An empty hand-written ## [X.Y.Z] heading is replaced, not duplicated.
  • It never fails on an empty [Unreleased]. It warns instead, and the workflow fallback below covers that release.
  • It exits 2 only when both [X.Y.Z] and [Unreleased] have entries (entries landed after the rotation), so a human places them. Per V2-1142, the cut tooling should report that, not abort on it.
  • It writes the rotated changelog to a temporary file next to it and renames that over the original, keeping its mode. An interrupted run leaves the old file whole.

2. Workflow fallback. With no matching section, the notes are now GitHub's generated list of PRs merged since the previous stable ant-cli-v* tag (releases/generate-notes). Before, they were the placeholder. A missed rotation now ships the right list of changes rather than none. The ::warning:: annotation stays, and the placeholder remains only as the last resort: no previous stable tag, an empty result, or a failed API call. A failed call's output is discarded. On an HTTP error gh api --jq prints the raw JSON error body and exits non-zero, and that body must never be published as the notes. The previous tag is the highest stable tag below the base version, so X.Y.Z-rc.N and X.Y.Z both list the whole train. Nothing about the success path changes. The logic lives in scripts/release-notes.sh, which the step calls, so scripts/test-release-notes.sh can test it against fake gh and git. A new Release scripts job in ci.yml runs that test and scripts/test-rotate-changelog.sh on every PR.

3. AGENTS.md is the agent guide, and CLAUDE.md imports it. CLAUDE.md's guide moves into a new root AGENTS.md unchanged. The Claude Code E2E skill section is labelled Claude-only. The changelog rule is added: user-facing PRs add entries under [Unreleased], and only the cut writes versioned headings. CLAUDE.md is now just @AGENTS.md, which Claude Code expands, so Claude loads the same text and there is one copy to keep current. Agents that read only AGENTS.md, such as Codex and OpenCode, now get the whole guide.

This settles the open question an earlier version of this PR asked reviewers, about where the changelog rule should live. The answer is option B, done here rather than in a follow-up: an AGENTS.md holding only this rule would have given those agents one rule and none of the build, architecture or PR guidance. saorsa-transport, saorsa-core and evmlib already keep a full AGENTS.md.

Linear issue

Closes V2-1107

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Compatibility

  • Wire: none
  • Storage: none
  • API: none. Only the release workflow's notes step (now scripts/release-notes.sh), the rotation script, their tests and CI job, and the agent guide (AGENTS.md, with CLAUDE.md importing it) change.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

Review fixes (2026-10-27), on main at 5e01699:

  • scripts/test-release-notes.sh, 10 checks, and scripts/test-rotate-changelog.sh, 10 checks, pass on Linux (dev2, bash 5.2) and in the new Release scripts CI job on 94b01ec (10 passed, 0 failed each).
    • Run against the old logic, with the step's || true restored, the failed-API test fails, and the published notes are the prefix plus {"message":"Not Found","documentation_url":…,"status":"404"}. All 10 pass on the fix.
    • Run against the old rotate-changelog.sh (cat "$tmp" > "$changelog"), the rename test fails, because the inode is unchanged. All 10 pass on the fix.
    • release-notes.sh gives byte-identical notes to the old step's extraction for 0.3.6, 0.3.5 and 0.3.6-rc.1 on main's CHANGELOG.md.
  • shellcheck is clean on all four scripts. actionlint with shellcheck finds nothing in ci.yml or in the changed step. Its 20 info and style notes on other steps of ant-cli-release.yml are the same 20 it reports on main's copy.
  • AGENTS.md holds every line of main's CLAUDE.md guide in order, checked line by line. The only change is the E2E heading's "(Claude Code)" label.

Earlier evidence:

New dependency

none

ADR

n/a

Mitigation / rollback

Revert. The workflow goes back to placeholder notes, and the script is unused until the cut tooling calls it.

🤖 Generated with Claude Code

Nic-dorman and others added 2 commits October 22, 2026 13:26
…tion script

The release workflow ships the CHANGELOG section matching the tag's base
version, but nothing at the train cut ever rotates `[Unreleased]` into that
section, so every tag from ant-cli-v0.3.7 to ant-cli-v0.3.12-rc.1 shipped the
"No changelog entry was recorded" placeholder.

- scripts/rotate-changelog.sh X.Y.Z moves `[Unreleased]` into
  `## [X.Y.Z] - <date>`, for the cut tooling to run on the rc branch. It is
  idempotent across rc re-cuts and never fails on an empty `[Unreleased]`.
- With no matching section, the workflow now publishes GitHub's generated list
  of PRs merged since the previous stable ant-cli tag instead of the
  placeholder, still with the `::warning::` annotation. The placeholder is kept
  only for when that API call fails.
- CLAUDE.md says where entries go and who writes the versioned heading.

Refs V2-1107.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sees it

CLAUDE.md imports it with @AGENTS.md, so Claude Code still loads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

REQUEST_CHANGES

The missing-section fallback can publish GitHub's API error response as the release notes instead of using the placeholder. The inline comment gives the failure path and minimum fix.

Coverage: 2/2 review seats: sol-1 (gpt-6.1-sol, xhigh) and glm-1 (z-ai/glm-5.3). The seats disagreed; I verified the error-handling defect in the reviewed workflow and GitHub CLI behavior.

Verification: bash -n scripts/rotate-changelog.sh passed. A local rotation and idempotent re-run passed. On this PR head, all listed checks passed except Security Audit. Cargo.lock is unchanged from the PR base, so that audit failure is inherited, not introduced by this PR; update the branch and rerun checks. No end-to-end release-tag run was performed.

Comment thread .github/workflows/ant-cli-release.yml Outdated
if [ -n "$previous_tag" ]; then
generated=$(gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
-f tag_name="$tag" -f previous_tag_name="$previous_tag" \
--jq .body || true)

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 — failed API requests can be published as release notes. When gh api receives a JSON HTTP error response, --jq .body does not filter it; gh writes the raw error JSON to stdout and exits non-zero. The trailing || true suppresses that exit but preserves stdout in generated. The non-empty check at line 402 then selects this branch and publishes the error payload instead of the placeholder. Capture success explicitly and set generated='' on failure (for example, if generated=$(gh api ...); then :; else generated=''; fi). Add a regression test with a mocked gh that emits a JSON error body and exits non-zero; assert the placeholder path is used.

@MaidSafe-Hermes

Copy link
Copy Markdown

Review posted on the exact head e2e3179c8731ccd9bb65ee5a852b9eb88f3932eb: formal REQUEST_CHANGES. The inline P2 finding explains how a failed gh api request can leave its JSON error body in generated, which then gets published instead of the placeholder; it requests explicit failure handling and a regression test.

I also noted one non-blocking hardening point: write the rotated changelog atomically so an interrupted copy cannot leave it truncated. For the AGENTS.md question, I recommend option A in this PR; a full shared agent guide can be a separate follow-up. The Security Audit failure predates this PR's changes (Cargo.lock is unchanged from its base); the other listed checks passed.

Nic-dorman and others added 4 commits October 27, 2026 14:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iting it

rotate-changelog.sh wrote the rotated text back with
`cat "$tmp" > "$changelog"`, so an interrupted run could leave
CHANGELOG.md truncated. It now writes a temporary file next to the
changelog, gives it the changelog's mode with `cp -p`, and renames it
over the original. An interruption leaves the old file whole.

scripts/test-rotate-changelog.sh covers:
- the rotation, including an rc suffix;
- an idempotent re-run;
- exit 2 when both sections have entries;
- an empty hand-written heading;
- an empty [Unreleased];
- bad input;
- the rename and the kept mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an HTTP error `gh api ... --jq .body` skips the filter, prints the
raw JSON error body and exits non-zero. The step's `|| true` kept that
body in `generated`, which then passed the non-empty check and would
have been published as the release notes instead of the placeholder.
Its output now counts only when the call succeeds.

The notes logic moves from the workflow step into
scripts/release-notes.sh, unchanged otherwise, so it can be tested:
the changelog section, else the generated notes since the previous
stable tag, else the placeholder. scripts/test-release-notes.sh runs it
against fake `git` and `gh`, including a failed call that prints a JSON
error body.

A new `Release scripts` job in ci.yml runs that test and the rotation
test on every PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md's guide moves into AGENTS.md unchanged, next to the changelog
rule, so agents that read only AGENTS.md (Codex, OpenCode and others)
get the whole guide and not just that one rule. CLAUDE.md is now
`@AGENTS.md`, which Claude Code expands, so Claude still loads the same
text and there is a single copy to keep current. The Claude Code E2E
skill section is labelled as Claude-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman

Copy link
Copy Markdown
Member Author

Addressed in 94b01ec. The branch is updated with main by a merge, not a rebase, so the reviewed commits are unchanged.

P2: failed API requests can be published as release notes (ant-cli-release.yml:400). You're right. I reproduced it with a 404: gh api … --jq .body printed {"message":"Not Found",…} and exited 1, and the || true kept that output.

  • The output of gh api now counts only when the call succeeds: if ! generated=$(gh api …); then generated=""; fi. A failed call gets the placeholder.
  • To make it testable, the notes logic moved out of the workflow step into scripts/release-notes.sh, and the step now calls it. The logic is otherwise unchanged: the changelog section, else the generated notes since the previous stable tag, else the placeholder.
  • scripts/test-release-notes.sh runs the script with fake git and gh on PATH. Its 10 checks:
    • a stable tag ships its own section, and an rc tag its base version's, without calling the API;
    • 0.3.1 doesn't take 0.3.11's section;
    • a missing section, or an empty hand-written heading, gets the generated notes, called with previous_tag_name=ant-cli-v0.3.11 for 0.3.12-rc.1;
    • gh printing a JSON error body and exiting 1 gets the placeholder, and neither Not Found nor documentation_url appears in the notes;
    • an empty generated body gets the placeholder;
    • no previous stable release gets the placeholder without an API call;
    • bad arguments and a missing changelog exit 1.
  • A new Release scripts job in ci.yml runs it on every PR, along with the rotation tests below.

With the step's original || true restored, the failed-API check fails and the notes contain the error JSON. All 10 checks pass on the fix. For 0.3.6, 0.3.5 and 0.3.6-rc.1 on main's CHANGELOG.md, the script's notes are byte-identical to the old step's extraction.

Non-blocking: write the rotated changelog atomically. Done. rotate-changelog.sh writes a temporary file next to the changelog, copies its mode with cp -p, then mvs it over the original. scripts/test-rotate-changelog.sh has 10 checks:

  • rotation, including an rc suffix;
  • an idempotent re-run;
  • exit 2 when both sections have entries;
  • an empty hand-written heading;
  • an empty [Unreleased];
  • bad version, bad date, and no [Unreleased] heading;
  • the file being replaced by a rename (new inode), with mode 640 kept and no temporary file left over.

Against the old cat "$tmp" > "$changelog", the rename check fails, because the inode is unchanged.

AGENTS.md: thanks for the recommendation. We went with B, in this PR instead of a follow-up.

  • CLAUDE.md's guide moved into AGENTS.md unchanged, which I checked line by line. The Claude Code E2E skill section is labelled Claude-only, and the changelog rule sits alongside the rest.
  • CLAUDE.md is now @AGENTS.md, so Claude Code loads the same text.
  • Agents that read only AGENTS.md now get the whole guide, not one rule, and there's a single copy to keep current. That matches saorsa-transport, saorsa-core and evmlib.

Verification (Linux, dev2, bash 5.2):

  • Both test scripts pass, 10/10 each.
  • shellcheck is clean on all four scripts.
  • actionlint with shellcheck finds nothing in ci.yml or the changed step. The 20 info and style notes it reports on other steps of ant-cli-release.yml are the same 20 it reports on main's copy.

CI on 94b01ec: all jobs pass except Security Audit. It fails only on RUSTSEC-2026-0387 (crypto-bigint 0.5.5), which is inherited from main and tracked as V2-1743. This PR changes no Cargo.toml or Cargo.lock. The new Release scripts job passes: test-release-notes.sh 10 passed, 0 failed, and test-rotate-changelog.sh 10 passed, 0 failed.

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

Focused re-review of the prior blockers on 94b01ec046cf974c47916c3bccf69c797be73104 (not a fresh full-panel review).

Coverage and adjudication: 2/2 seats returned: sol-1 (gpt-6.1-sol xhigh) was uncertain pending evidence on the audit failure; glm-1 (z-ai/glm-5.3) approved but asked to verify the tag output and API permission. I checked those points against the exact workflow and GitHub's REST permission requirements: the meta step writes tag to $GITHUB_OUTPUT, and this workflow grants contents: write, which the generate-release-notes endpoint requires. The Security Audit log identifies crypto-bigint 0.5.5 (RUSTSEC-2026-0387); the PR-base, PR-head and current-main Cargo.lock hashes are identical. The audit objection is therefore resolved as inherited, not PR-introduced. The seats differed initially; I resolved their stated uncertainties with this direct evidence rather than treating that as consensus.

The release-notes fix now discards captured output when gh api exits non-zero, and its regression test supplies a JSON error body and checks that placeholder notes are used. The changelog script stages beside the source and renames into place; tests cover repeat-run, failure-without-change and mode preservation. CLAUDE.md imports the full shared AGENTS.md guide. I found no remaining blocker in these changes.

Verification: bash -n passed; release-notes tests passed 10/10; changelog-rotation tests passed 10/10; the exact-head Release scripts CI job passed. Required checks linear-link and pr-template passed. The non-required Security Audit remains red for the inherited advisory above. No end-to-end tag release was run. This approval is a code-review verdict, not merge authorization.

@Nic-dorman
Nic-dorman merged commit 27461b2 into main Oct 27, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants