Repository navigation
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
Claude finished @astubbs's task in 5m 33s —— View job Review: publish the curated changelog section as the release bodyTask list
SummaryThe design is sound: fail loudly before anything is tagged/deployed/published, reject unsupported AsciiDoc rather than silently mangling it, no One thing worth a look before merge:
Smaller observations (non-blocking)
Verified against real data (via static trace, not execution — see caveat above)
Overall: well-scoped, well-tested-by-design change that fixes a real silent-failure bug, with clear docs updates in |
✅ Duplicate Code ReportTwo engines run in parallel for cross-validation. Each has its own thresholds tuned to its baseline - the real safety net is the per-engine "max increase vs base" check. ✅ PMD CPD
No new clones introduced by this PR. ✅ jscpd (language-agnostic)
No new clones introduced by this PR. Powered by astubbs/duplicate-code-cross-check |
|
Review response (@claude review of f7a731d)Replying here rather than in-thread because this review posted no inline comments and no review Fixed in 0d9fa67. 1.
|
| Site | Before | After |
|---|---|---|
| quarantine guard | [ "${{ inputs.dryRun }}" = "true" ] |
[ "$DRY_RUN" = "true" ] |
| tag-exists guard | refs/tags/v${{ inputs.releaseVersion }} |
refs/tags/v$RELEASE_VERSION |
| render notes (new here) | python3 bin/release-notes.py "${{ ... }}" |
python3 bin/release-notes.py "$RELEASE_VERSION" |
release:prepare |
-DreleaseVersion=${{ ... }} (also unquoted) |
-DreleaseVersion="$RELEASE_VERSION" |
| deploy / release | git checkout "v${{ ... }}", gh release create "v${{ ... }}" |
"v$RELEASE_VERSION" |
Hardening one instance and leaving four would have left the file no safer while implying otherwise.
The release:prepare arguments were additionally unquoted, so they gain word-splitting safety too.
if:/with: expressions keep ${{ }} - Actions evaluates those itself, never through a shell.
Verified mechanically, by parsing the YAML rather than reading it:
job-level env: {'RELEASE_VERSION': '${{ inputs.releaseVersion }}', ...}
OK - no run: block interpolates ${{ ... }}; every shell use is an env var
The PR body's checklist item has been rewritten, and a Script injection section now records what the
claim was, that it was false, and what makes it true. AGENTS.md documents the convention so new
steps inherit it.
Agreed on your framing that this was not a new exposure class and that workflow_dispatch already
requires write access - it is hygiene. The reason it was worth doing anyway is the false claim, not the
threat model.
2. RE_BOLD misfiring on bare asterisks. Valid - fixed, not just noted.
\*{1,2}([^*\n]+)\*{1,2}would misfire on ordinary text containing two unrelated single asterisks
(e.g. arithmetic like3 * 4 * 5) [...] an inherent limitation of a line-oriented regex converter.
You flagged this as non-blocking known fragility. I disagree that it should be left: this renderer's
entire thesis is reject rather than mangle, and silently turning 3 * 4 * 5 into 3 ** 4 ** 5 is a
mangle shipped straight to a release page. It also is not inherent - AsciiDoc bold is constrained,
and honouring those boundaries is a regex change, not a parser:
RE_BOLD = re.compile(r"(?<![\w*])(\*{1,2})(?![\s*])(.+?)(?<![\s*])\1(?![\w*])")The backreference also keeps *x* and **x** symmetric instead of pairing one delimiter with two.
New assertion bare asterisks in prose are not emphasis covers 3 * 4 * 5 and --forkCount 1 * cores
alongside a real *really* in the same line. Confirmed byte-identical output across all 29 real
CHANGELOG.adoc sections, so this is pure hardening with no behaviour change on current data.
3. Multi-line NOTE:: with a +-continued paragraph. Valid, deliberately not fixed.
a
+-continued second paragraph inside aNOTE::would fall outside the blockquote [...] and
render as an unquoted paragraph instead of erroring per the "reject, don't mangle" design principle.
Correct analysis, and I am leaving it. Unlike (2), the fix is not a boundary tweak: knowing "am I
inside an admonition" requires the block model this script deliberately does not have, and the case is
unreachable today (no NOTE:: lives inside any version section). Adding speculative block-tracking
machinery for a construct nobody has written would be the wrong trade. If a release section ever needs
a multi-paragraph admonition, the right move is to add +-inside-admonition to UNSUPPORTED at that
point - a rejection is cheap, correct, and one line. Flagging rather than silently accepting the gap.
4. The execution caveat - now discharged.
this sandbox declined to run
python3/bash[...] Please treat correctness claims below as
"verified by careful reading," not "verified by running it".
Thanks for stating this plainly rather than presenting the trace as verification. I ran everything:
$ bash bin/test-release-notes.sh -> exit 0, 23/23 ok
$ python3 bin/release-notes.py 0.6.0.0 -> exit 0, 5641 bytes
warning: the `== 0.6.0.0` heading still carries '(unreleased)' ...
$ python3 bin/release-notes.py 9.9.9.9 -> exit 2
error: no `== 9.9.9.9` section in the changelog ...
$ all 29 sections -> sections=29 all_rendered_ok=yes
Your hand-traced conclusions held up: no mismatch found between the renderer and the assertions, and
every section renders. The PR body's "4.9 KB" was wrong (measured 5.6 KB) and is corrected.
Unrelated: the red claude-review check is a broken gate, not a finding
claude-review is red for a reason that has nothing to do with this PR, and it will hit any PR
whose bot comments exceed the pipe buffer. bin/check-review-posted.sh:94:
if printf '%s\n' "$comment_bodies" | grep -qE "actions/runs/${run_id}([^0-9]|$)"; thenwith set -euo pipefail at line 56. grep -q exits the instant it matches; printf is then killed by
SIGPIPE (141); pipefail promotes 141 to the pipeline status, so a successful match is read as a
failure. Reproduced against this PR's real comment payload:
$ gh api .../issues/199/comments --paginate --jq '.[].body' | wc -c
136257
$ grep -c "actions/runs/30974467729" <payload> # the review IS there
1
$ printf '%s\n' "$B" | grep -qE "actions/runs/30974467729([^0-9]|$)"; echo $?
141 # 136 KB payload -> printf blocks, grep exits early, SIGPIPE
$ printf '%s\n' "hello actions/runs/30974467729 x" | grep -qE ...; echo $?
0 # small payload fits the 64 KB pipe buffer, no SIGPIPE
So it is size-dependent, which is why bin/test-check-review-posted.sh misses it - every fixture it
feeds is a few bytes. The emitted error also misdiagnoses itself, blaming the workflow-validation
guard; this PR does not touch claude-code-review.yml, and the review was posted and does cite run
30974467729.
I have not touched the gate. Fixing a CI gate from inside the PR it is failing is exactly the
gaming this repo forbids, even when the fix is legitimate - so it stays red and visible. Flagging for a
separate PR: the one-line fix is grep -q ... <<<"$comment_bodies" (or dropping -q), plus a fixture
larger than 64 KB in the gate's own test so the bug cannot come back.
|
Claude finished @astubbs's task in 3m 48s —— View job Review: follow-up on commit 0d9fa67 (env-binding + RE_BOLD fix)Task list
Both points from my previous review are addressed correctly1. Script injection ( 2. RE_BOLD = re.compile(r"(?<![\w*])(\*{1,2})(?![\s*])(.+?)(?<![\s*])\1(?![\w*])")correctly requires a non-word/non-
This is a real fix, not just a regex tweak for the failing case — I don't see a case in the current One very minor, non-blocking observation
Everything else checked
Response to the CI-gate tangentSeparately: the Overall: both prior findings were fixed correctly and verifiably from the diff alone, not just reworded. No new blocking issues found in this pass. |
Review response (follow-up review of 0d9fa67)Thanks for re-checking both fixes from the diff, and for repeating the execution caveat rather than Unconstrained
|
| Run | Payload | Byte offset of the run-id citation | Result |
|---|---|---|---|
| 30974467729 | 136,257 | 285 (early) | red - printf SIGPIPE, false failure |
| 30985467155 | 150,064 | 144,188 (late) | green |
When the citation sits late, grep -q drains nearly the whole stream before exiting, so printf
finishes writing and never takes SIGPIPE. When it sits early, printf is killed, pipefail promotes
141, and a successful match reads as a failure. Synthetic confirmation - same gate, same run id, early
citation in a >64 KB payload:
early-citation gate exit=1 (1 = false failure; the review IS cited)
So this is a latent, ordering-dependent flake that will bite whichever PR happens to have its review
comment early in a large payload - a green result is not evidence the gate is sound. Still deliberately
unfixed from inside this PR; it needs its own change (grep -q ... <<<"$comment_bodies", plus a
fixture larger than the 64 KB pipe buffer in bin/test-check-review-posted.sh, which only feeds a few
bytes today and is why this survived).
|
Claude finished @astubbs's task in 2m 1s —— View job Review: commit bb899c3 (test-only addition)Task list
Execution caveat (same as both prior review rounds on this PR): this sandbox again declined every Scope of this commit
This is precisely what the PR author's last comment said they'd do: rather than special-case Verified by hand-traceNew assertion ( expected to render as: Traced against
I also recounted the assertions in the file against the PR author's "24/24 ok" claim: 10 top-level ConclusionNo issues found. This commit is a well-targeted, low-risk addition: a single regression test that locks in behaviour already shown correct in the previous review round, with no changes to the renderer itself. The comment explaining why the pass-through is deliberately harmless (identity transform + CommonMark already treats intraword |
check-review-posted.sh:94 ran `printf | grep -q` under `set -o pipefail`. grep exits the instant it matches, printf then dies with EPIPE (141), and pipefail promotes that to the pipeline's status - so a comment citing the run made the check fail. It only bites when more than one pipe buffer (64 KiB) of comment data follows the match, which is why it looked intermittent. Observed on #198, #199, #204 and #210. On #210 the review comment sat 4.7 KB into the stream with a 127 KB similarity report behind it; the job log carries `printf: write error: Broken pipe` directly above the failure. The emitted error blamed the workflow-validation guard, which none of those PRs had tripped. The fix is the herestring this repo already prescribes: the same bug class is guarded against in bin/test-check-copyright-headers.sh, whose comment records it "seen live in CI". check-review-posted.sh shipped in that same commit without the guard. Two tests, both verified to fail against the old line: - functional: match found, then >64 KiB of further comments. The existing cases dance around this - case 5 buries the match but keeps it small, case 6 puts it last so nothing follows to fill the buffer. - structural: the checker may not pipe into grep -q or awk at all, mirroring the copyright scanner's guard, so the next instance of the class is caught rather than the next occurrence of this one. The error text is left alone: it misdiagnosed those four PRs only because of this bug, and becomes accurate again once the SIGPIPE path is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
#211 added a rule to bin/AGENTS.md - never pipe into `grep -q` under pipefail - while the repo broke it in four places. A rule shipped with known violations is not a rule. `writer | grep -q PATTERN` under `set -o pipefail` reports failure exactly when it MATCHES: grep exits on the first hit, the writer takes EPIPE (141), pipefail promotes that to the pipeline's status. It only fires once the writer still has more than one pipe buffer (64 KiB) to write, so it passes every small fixture and surfaces when real data grows. - check-review-posted.sh:94 - live. Reported "no review posted" on four PRs whose reviews had posted (#198, #199, #204, #210). - check-quarantine-owners.sh:98,110 - latent, and close. `git show` pipes a whole source file into `grep -q` inside an `if`. The largest file in the repo is 65,185 bytes against a 65,536-byte buffer: 351 bytes of headroom, on a file two open PRs are adding lines to. It would fail as "annotation missing", not as a pipe error. - quarantine-lane-report.sh:201 - the `||` makes a SIGPIPE take the wrong branch and silently retarget, rather than shielding it. All four become herestrings, which have no pipeline to fail. Adds bin/check-shell-sigpipe.sh, run in CI beside the copyright self-test (seconds, no JDK) and granted to the reviewer. Verified both directions: clean on this tree, exit 1 when the old line is reinstated. It skips itself, since its failure message necessarily contains the anti-pattern as the "wrong" half of a worked example. shellcheck does NOT detect this - run against the known-bad line, it passed clean. Hence a bespoke grep rather than adopting a linter. Note check-review-posted.sh:94 is also fixed in #210, which adds the functional regression test for it. Conflict expected and cheap; this PR fixes it because it is this PR that states the rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
`writer | grep -q` under `set -o pipefail` reports failure exactly when it MATCHES: grep exits on the first hit, the writer takes EPIPE (141), pipefail promotes that to the pipeline's status. It only fires once the writer still has more than one pipe buffer (64 KiB) to write, so it passes every small fixture and surfaces when real data grows. - check-review-posted.sh:94 - live. Reported "no review posted" on four PRs whose reviews had posted (#198, #199, #204, #210). - check-quarantine-owners.sh:98,110 - latent, and close. `git show` pipes a whole source file into `grep -q` inside an `if`. The largest file in the repo is 65,185 bytes against a 65,536-byte buffer: 351 bytes of headroom, on a file two open PRs are adding lines to. It would fail as "annotation missing", not as a pipe error, sending the reader nowhere near the cause. - quarantine-lane-report.sh:201 - the `||` makes a SIGPIPE take the wrong branch and silently retarget, rather than shielding it. All four become herestrings, which have no pipeline to fail. Kept separate from the guard that enforces this, so the fixes can be reviewed - and reverted - on their own. shellcheck does NOT detect this: run against the known-bad line, it passed clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
`writer | grep -q` under `set -o pipefail` reports failure exactly when it MATCHES: grep exits on the first hit, the writer takes EPIPE (141), pipefail promotes that to the pipeline's status. It only fires once the writer still has more than one pipe buffer (64 KiB) to write, so it passes every small fixture and surfaces when real data grows. - check-review-posted.sh:94 - live. Reported "no review posted" on four PRs whose reviews had posted (#198, #199, #204, #210). - check-quarantine-owners.sh:98,110 - latent, and close. `git show` pipes a whole source file into `grep -q` inside an `if`. The largest file in the repo is 65,185 bytes against a 65,536-byte buffer: 351 bytes of headroom, on a file two open PRs are adding lines to. It would fail as "annotation missing", not as a pipe error, sending the reader nowhere near the cause. - quarantine-lane-report.sh:201 - the `||` makes a SIGPIPE take the wrong branch and silently retarget, rather than shielding it. All four become herestrings, which have no pipeline to fail. Kept separate from the guard that enforces this, so the fixes can be reviewed - and reverted - on their own. shellcheck does NOT detect this: run against the known-bad line, it passed clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
Merged master (4f76680) - and what it changed for this PRMerged Conflicts: two, both add-add, both resolved by keeping BOTH sides
Master's bug list is left verbatim - it is #198's territory, and bullets 1 and 2 describing the Both new guards pass, and they genuinely inspect this PR's filesChecked why they pass rather than trusting the exit code:
Pleasing to see #198 changed my input, and the pipeline handled it correctly
The only difference in the rendered release body is #198's correction flowing straight through: - The Kafka client stays on **3.9.1** - Kafka 4.x ... deferred to keep the release low-risk.
+ The Kafka client moves 3.9.1 -> **3.9.2**, a patch bump within the same 3.9 line - no API or
+ behaviour change for callers. Kafka 4.x ... deferred to keep the release low-risk.That is the whole point of this PR working: a changelog correction becomes the release page text with One thing master's new rules say I should do, that I am deliberately NOT doing here
I am not adding the grant in this PR, because
Worth pairing with a sweep for any other ungranted verification script, since the same gap is what (Not touching |
|
Claude finished @astubbs's task in 1m 58s —— View job Review: merge commit
|
Issue-reference gate (#114): six references qualified - 6c96df3
All six mean this fork's release tracking issue, so all six became Verified with the gate itself rather than by eye, run against a locally computed diff vs (Before the fix, the same command listed all six.) Worth noting the gate reads added lines in No The other three new commits, checked rather than assumed
Full state after the merge and this fixEverything in my previous comment still stands, including the one deliberate omission: the |
|
Claude finished @astubbs's task in 1m 59s —— View job Review: commit
|
…he sweep tool a mode that could have caught it (#258) * docs(upstream-map): track the 28 issues closed by the 2023 admin sweep Upstream ran two bulk clearouts before going quiet, and neither was a triage: 2023-06-15 `eddyv` closed 35 unmerged PRs with "Closing - Stale." (34 of them ours), and 2023-07-07 `johnbyrnejb` closed 28 issues with "Closing Issue" - every one marked COMPLETED rather than "not planned". That state reason is why the cohort was invisible. GitHub renders `completed` as resolved, so the issues read as done at a glance, and `upstream-sweep.sh` searches `updated:>=last_swept`, so anything last touched in 2023 can never appear in a sweep. None of the 28 were in this manifest; the mirroring that produced the other 78 fork mirrors only ever sampled *open* upstream issues. All 28 were re-read in full - bodies plus all 58 comments - and each verified against fork source rather than trusted from its thread. That mattered: four issue bodies point at PRs that "fix" them (#372->#390, #319->#270, #203->#345, #191->#346) and every one of those PRs was itself swept unmerged. Result: 2 already fixed (#41 offset-scan removal, #319 shutdown CME), 6 partly addressed, 20 fully open. Grouped into work items rather than 28 near-duplicate entries, since several want designing together - per-topic handlers with separate consume/produce types, seek with subscription changes, retry expiry with stall detection. #57 folded into the existing log-noise entry. Swept PR head commits were checked and are all still reachable, so the drafts are recoverable; entries cite branch and SHA, not just the PR number. Upstream-Issue: confluentinc#154 Forwarded: no Applied-Upstream: no * docs(upstream-map): mirror the whole 2023 cohort, not the parts we rate The point of mirroring is a clean, known cutoff - "every issue upstream closed administratively is accounted for here" - not a curated pick of the good ones. A set filtered by our own value judgement is not a cutoff: the next person cannot tell "not mirrored because it was junk" from "not mirrored because it was missed", and that ambiguity is worth far more than the cost of carrying a few items nobody will ever action. Drops the decline recommendations from #53, #199, #246 and #57 in favour of neutral statements of the trade-off, and renames sweep-2023-decline-candidates to sweep-2023-small-items (status wontfix -> none). The facts that informed those recommendations are kept - they are useful to whoever picks the issue up - but they are now framed as input to that person's decision rather than as our verdict delivered in advance. Fork issues can always be closed later on their merits, which leaves a visible record; pruning before mirroring does not. Records the principle in the section header so a later pass does not re-prune. Upstream-Issue: confluentinc#154 Forwarded: no Applied-Upstream: no * docs(upstream-map): link the created fork mirrors back to their entries The 28 mirrors are live as #227-254 (label `upstream-admin-closed`), with confluentinc#41 -> #233 and confluentinc#319 -> #252 created and immediately closed as completed, carrying the code evidence that they are genuinely fixed. Records the fork numbers on each entry so the mapping is queryable rather than re-derived, which is the reason this file exists. Adds `fork.fork_issues` (the plural of the existing `fork_issue`) because several entries deliberately group upstream items that want designing together, and documents it in the schema block. Verified: all 28 mirrors are referenced, none missing, no strays. Also snapshots each upstream title verbatim into its mirror body header, dated 2026-08-07 - the fork titles may drift as these are worked, and the original wording is what makes an old thread findable. Upstream-Issue: confluentinc#154 Forwarded: no Applied-Upstream: no * fix(tooling): give upstream-sweep a mode that can see closures it never saw The default sweep is a window search - `updated:>=$since` - so it structurally cannot see an item whose last activity predates the window, and `last_swept` only moves forward, so the blind spot grows. That is why upstream's 2023 administrative closures went unnoticed for three years: 28 issues closed with "Closing Issue" and 35 unmerged PRs closed as "Closing - Stale.", all marked COMPLETED, none of it triage, none of it visible to us. Adds `--audit`: no time window, asks "which closed upstream items are neither tracked in the manifest nor mirrored in the fork?", and flags days where an implausible number of things closed at once. Run against the real repo it rediscovers both sweeps from scratch and reports zero unaccounted, which is also an end-to-end check that the new fork_issues linkage is correct. Bots are excluded from the PR analysis. Dependabot self-closes superseded bumps in batches that look exactly like a sweep - 2022-08-16, 2022-10-20, 2023-11-03 and 2024-01-25 are all dependabot - and unfiltered they buried the two real sweeps in noise. Filtering them surfaced two human PRs that had been sitting inside those batches (confluentinc#508, confluentinc#650), now recorded for review. The report says outright that a bulk day is not proof of a sweep, because a release triage looks identical from here, and that stateReason COMPLETED is not evidence of a fix - that assumption is what made this cohort invisible. Known remaining hole, recorded not fixed: a PR closed alone on a quiet day is still invisible, since detection keys on bulk. Upstream-Issue: confluentinc#154 Forwarded: no Applied-Upstream: no * fix(tooling): teach the audit about Discussions, a content type we never checked Discussions were invisible to everything: not in the manifest, not mirrored, not in any sweep mode. 74 of them upstream, and nothing we run would ever have mentioned one. They were NOT swept - no bulk closure, and the largest day (2024-04-02, six threads) is all answered=true housekeeping. So the failure mode is different from the 2023 cohort: not administrative closure, just questions nobody answered, in a place no issue search reaches. Handling differs accordingly - these want answering or converting, not mirroring wholesale. The audit now lists zero-reply discussions, excluding release-announcement threads by title: those legitimately have no replies, and counting them as neglect would put 9 false positives at the top of the report. Two finds justify the check on their own. Discussion 542 (zero replies) is a field report of a transactional consumer stuck in rebalance because it cannot acquire the produce lock, with the reporter suspecting the revoke-time flush - the same lock lifecycle as bug-producing-lock-double-release, and they raised the timeout without effect, which is evidence about the mechanism rather than the duration. Discussion 883 (zero replies) is the same complaint as fork mirror #187 but with a runnable reproducer, which #187 lacks. Upstream-Issue: confluentinc#154 Forwarded: no Applied-Upstream: no * docs(inflight): record the open obligation to account for every upstream item Two notes, both about what is NOT done. Discussions are not a queue to be converted. There is no pipeline turning threads into issues, and most should never become one. The rule is a judgement: if reading a discussion makes us think there is an issue, we raise one - a normal fork issue on its own merits, citing the discussion as where it came from. It exists because we believe the problem is real, not because a thread existed. That also keeps `upstream-mirror` meaning exactly "an upstream issue we carry", which is what makes the 2023 cutoff verifiable. The wider obligation is now written down: every upstream issue, PR and discussion must be accounted for - carried, declined, or genuinely resolved upstream - not sampled. The 2023 cohort was found only because it was a *bulk* event; `--audit` keys on bulk closures and zero-reply threads, and both are proxies that miss whole shapes of problem (a PR closed alone on a quiet day, a discussion with one dismissive reply, an issue marked COMPLETED with no linked PR). The audit narrows the field; only reading discharges the obligation. Also records what has been ruled out, so it is not re-investigated: wiki disabled, no advisories, all open-milestone issues already mirrored, the orphan branches accounted for (v0.6.x-dev is the lambda-actor work already captured via swept PRs; 0.5.3.x's regression fix IS on master as a908e16), and "upstream pushed today" being a pushed_at artefact rather than new activity. Left open: project boards need a read:project scope we do not have, and 169 forks are unexamined. * fix(upstream-map): use a group the schema actually allows Two new entries used `group: batching-ordering`, which is not in the GROUPS allow-list in scripts/upstream-map.py, so `upstream-map.py validate` failed on them. Caught in review on #258. My mistake was validating the wrong way: I checked the file with an ad-hoc yaml.safe_load plus a duplicate-id check, which passes happily on a group the schema rejects, instead of running the project's own validator that AGENTS.md tells contributors to run before committing manifest changes. Reassigned both to `features` rather than widening GROUPS. The rest of this cohort's feature entries already use `features`, so this keeps the cohort internally consistent, and the ordering/batching distinction is not lost - it lives where it belongs, on the issues themselves via the `area/batching-ordering` label (#244, #236). Adding a vocabulary term to a shared schema as a side effect of a mirroring PR is the larger, less reversible change. Also drops the redundant 345 from sweep-2023-broker-disconnect-commit's `related` list, where it already appears in `prs`. Verified: `upstream-map.py validate` reports OK on 27 entries, and `refs` and `table` both still render. * docs(upstream): graduate the durable parts of the sweep record out of inflight The three inflight notes stay - they hold undischarged work - but they were also carrying permanent content, and inflight is transient by its own charter. Moved to durable homes, each citing the manifest rather than restating it: - docs/upstream.md gains the upstream-admin-closed cohort note (with the "do not trust 2023-era closure states" rule), the discussions non-mirroring policy decided 2026-08-07, documentation of --audit and its known blind spots, and the ruled-out upstream surfaces (wiki, advisories, milestones, orphan branches, pushed_at). - docs/solutions/ gains the transferable lesson: a closure state is a rendering choice not a triage, "fixed by #N" must be checked against the merge bit, windowed watchers need a no-window audit, and bots must be filtered before hunting bulk events. - upstream-pr-analysis.adoc is corrected: it claimed issues were never bulk-closed, which the 2023-07-07 sweep disproves, and the PR sweep is now confirmed rather than suspected. The inflight notes now hold only open work, pointing at the new homes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(upstream): name the analysis doc's role (the plan) beside the manifest (the state) docs/upstream.md's opening enumerated everything it owns except the editorial analysis, and neither source-of-truth reference was a clickable link. Now the two roles are named explicitly: the .adoc is the plan, the manifest is the state tracker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(upstream): make the audit's discussion query actually paginate, and correct the sweep cohort Address PR review feedback (#258). - upstream-sweep.sh: `gh --paginate` only substitutes a variable named `endCursor`; calling it `$cursor` meant the cursor was never fed back and gh re-requested page 1 forever (reproduced: 469 identical pages at first:2). It only looked healthy because 74 discussions fit in one page of 100. The failure was also swallowed by `2>/dev/null || true`, which would have printed a clean "(none)" for a broken query - the one answer this mode must never invent. Fail loudly instead. - upstream-map.yaml: the sweep cohort listed 36 PRs against a documented count of 35. Upstream closed exactly 35 unmerged PRs on 2023-06-15; confluentinc#66 (closed 2022-10-19) was carried in by mistake. Listing a ref here marks it accounted for, so it was hiding an unrelated PR from every future audit. Recorded PR 258 and moved the entry to `pr-open` per the lifecycle rule. - next-upstream-coverage-completeness.md: dropped the cached counts - inflight notes must not record what a command can answer, and the "~100 partially mirrored" line already contradicted docs/upstream.md, which says all 78 open upstream issues are mirrored with backlinks. Kept only what the audit cannot say: what nobody has read yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(agents): let the manifest cache frozen upstream-issue facts, and say why The rule said "the manifest tracks upstream PRs only", so mirroring the 2023 sweep cohort into it read as a violation. The rule is what is out of date. An archived upstream's closed issue numbers and closure events are frozen. Caching them locally is a read-path optimisation, not a second tracker: grepping one file is instant, while the same answer from the mirrors costs dozens of API round-trips and burns rate limit shared across agents. The usual objection to duplication - the copy silently diverges - needs a source that can still move, and this one cannot. So the boundary is redrawn where it actually bites: the mirror owns an issue's *live state* and remains the only place you update it; the manifest may cache what is frozen (number, cohort, owning mirror). Cache what is frozen; never mirror what is moving. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(upstream): put the manifest-caching rule in the doc that owns it The previous commit wrote the rule into AGENTS.md, which is the one place it does not belong. AGENTS.md's own preamble calls itself a router - "if it only matters once you are already in a topic... it goes in that topic's doc" - and docs/upstream.md line 7 already claims ownership explicitly: "AGENTS.md carries only the pointer and the one-line rule that manifest upkeep is the agent's job." The rule was in fact already stated twice before this branch (AGENTS.md:442 and docs/upstream.md:23), so growing the AGENTS.md copy deepened a fragmentation that was already there. Now stated once, where an agent editing the manifest will actually be looking, with AGENTS.md left holding the pointer and the one binding rule whose failure is silent (keep the fork side in sync). Net effect on the router: one line shorter than master, despite covering more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(agents): make a pointer say who owns the rule, not just what is nearby Diagnosed from a live miss. A reviewer cited AGENTS.md's "the manifest tracks upstream PRs only", so that section was read and treated as authoritative - while docs/upstream.md had been stating the same rule, and claiming ownership of it, the whole time. The duplicate was then grown in the router rather than fixed at source. Nothing in the reading path revealed the mistake. AGENTS.md stated a complete-looking rule and followed it with a pointer listing four adjacent mechanics ("the manifest schema, the mirrors, the commit trailers and the upstream sweep"), which reads as *further detail*, not as *this is a summary and that doc wins*. The contract was written down - docs/upstream.md:7 - but only on the side nobody enters from, since AGENTS.md is what loads every session. The anti-duplication rule already existed here ("Never state a fact twice"). What was missing is the half that makes it discoverable: a cross-reference must name the owner and say the owner wins, so a reader can tell a stub from the whole rule before editing the copy. Applied to the rule itself and to the upstream pointer that failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e body
A GitHub Release cut by this pipeline had no readable body. The workflow did try:
an inline awk compared each line to the literal string "== <releaseVersion>", but
the section is headed "== 0.6.0.0 (unreleased)", so it matched nothing - and the
step then fell back to --generate-notes, which is indistinguishable, on the release
page, from the curated notes having silently vanished. Nothing failed.
Replaces the one-liner with bin/release-notes.py, and removes the fallback:
- Extraction tolerates the headings the file actually uses ("v0.5.2.2", a
"(unreleased)" suffix - warned about, not silently accepted) and will not let a
version prefix match a longer version (0.6.0.1 vs 0.6.0.10).
- AsciiDoc -> Markdown for a bounded subset (headings, bullets, ordered lists,
link/URL macros, bold, admonitions, "+" continuations, "//" comments), with
monospace spans masked so `bz.stub.parallelconsumer.*` is not read as an
emphasis marker, and relative link: targets absolutised at the released tag so
they do not 404 off github.com. Anything outside the subset that would render as
garbage (source blocks, tables, anchors, block attributes, includes, xrefs) is an
error, not a silent pass. No asciidoctor/pandoc: the release must not be able to
fail because a gem would not install.
- Missing, empty or unconvertible section = non-zero exit. release.yml renders the
notes BEFORE it commits, tags, deploys or publishes anything, so that failure is
cheap; a dry run rehearses the render and prints the body to the job summary.
Tested by bin/test-release-notes.sh (contract cases on synthetic changelogs, plus
every version section of the real CHANGELOG.adoc). No CI job is added for it:
bin/check-all.sh --with-tests globs bin/test-*.sh and repo-hygiene.yml runs that on
every PR, so the suite is already swept - the original branch added a dedicated
maven.yml job because check-all.sh did not exist yet, and re-adding one now would
be the hand-maintained list that script exists to abolish.
Docs go to docs/releasing.md and docs/inflight/release-0.6.0.0.md rather than
AGENTS.md, which no longer carries the Releasing or CI sections this originally
edited. Every reference to #197 is written qualified: the fork's numbers sit
inside confluentinc's range, confluentinc#197 is a different issue, and
.github/scripts/issue-ref-gate.js now fails a bare one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g them into shell
The PR body claimed the release version "reaches the script as an argv argument, not
interpolated into a shell string". That was false: `${{ inputs.releaseVersion }}` is
substituted textually into the `run:` block before bash parses it, so an input carrying
shell metacharacters is injected as code regardless of the quotes around it. A security
claim that does not hold is worse than no claim, so make the claim true rather than
retract it.
release.yml now binds releaseVersion/developmentVersion/dryRun to
RELEASE_VERSION/DEVELOPMENT_VERSION/DRY_RUN once at job level, and every shell step reads
the environment variable. This covers the file's pre-existing instances too (the tag
guard, release:prepare, git checkout, gh release create), not just the render step this
PR added - leaving four unhardened and one hardened would have been incoherent. `if:` and
`with:` expressions still use `${{ }}`: those are evaluated by Actions, never by a shell.
The maven args were also unquoted, so they gained word-splitting safety as well.
Also fix a real mangle the reviewer flagged in the renderer: RE_BOLD ignored AsciiDoc's
constrained-emphasis boundaries, so ordinary prose containing two bare asterisks
("3 * 4 * 5") was rewritten into broken emphasis - precisely the mangled output this
renderer promises never to ship. The regex now honours the boundaries and pairs its
delimiters with a backreference. Verified byte-identical output across all 29 real
CHANGELOG.adoc sections.
Tests: bin/test-release-notes.sh gains a bare-asterisk assertion (23 pass, exit 0).
Docs: docs/releasing.md records the env-binding convention so new steps inherit it -
AGENTS.md no longer carries the Releasing section this originally edited.
… through The follow-up review noted RE_BOLD applies constrained-emphasis boundaries to `**` as well as `*`, though AsciiDoc's double-asterisk form is unconstrained, so an intraword span like `un**bel**ievable` is not matched. Accurate about the regex, but it has no effect on output: `**x**` is already the Markdown spelling, so "converting" it is the identity transform, and CommonMark renders intraword `**` as strong regardless. Checked every intraword form (`foo**bar**baz`, `re**start**`, `a**b**`, `**a**b`) - matched and unmatched produce byte-identical output. An exhaustive sweep of 19607 generated strings against an implementation that treats `**` as properly unconstrained found no difference on any input of that shape. So there is nothing to fix, and the pass-through is asserted rather than left to chance. 24 assertions, exit 0; all 29 real CHANGELOG.adoc sections still render byte-identically.
… not a blank release page
The emptiness check ran on the RAW section lines, so "the section has lines in it" was
being used as a proxy for "the section renders to something". Those are different tests
and only the second one is the promise. A section holding nothing but `//` comments - or
nothing but a `+` list continuation - passed the first, and every one of its lines was
then dropped during conversion:
$ printf '== 0.6.0.0\n\n// a comment\n' > c.adoc
$ python3 bin/release-notes.py 0.6.0.0 --changelog c.adoc | wc -c
1
$ echo $?
0
One byte, exit 0, and release.yml would hand that to `gh release create --notes-file`.
That is the blank release page of #197 reached from the other side, in the script
written to make it impossible. Emptiness is now judged on the converted output, in
`render`, which is the only place where it is a true statement.
Four smaller mangles found by the same adversarial pass over the renderer:
- An odd number of backticks on a line leaves a monospace span open, so `convert_inline`
masks the remainder as code and silently stops converting emphasis in it. Rejected now,
with the same "fail rather than ship mangled markup" contract as the AsciiDoc table.
- `'''` (thematic break) and `<<<` (page break) passed through into the body, where
Markdown renders both as literal punctuation. Rejected.
- `check_supported` reported one line once per pattern that matched it, so `|===` was
reported twice - as a table and as a table cell. First match wins now, and the patterns
are explicitly allowed to overlap.
- argparse exits 2 on a usage error, which is this script's "no section for that version".
A release operator hitting a mistyped flag would have gone looking for a missing
changelog section. Usage errors are exit 1, matching what the docstring always claimed.
Plus one deliberate behaviour change, wired up by release.yml in the next commit: `--strict`
promotes the "heading still carries `(unreleased)`" warning to a refusal (exit 4). The suffix
never reaches the body - the heading is not rendered - so this is about not tagging a changelog
that still calls the version unreleased, and a warning in a 30-minute job log is not a check.
Without the flag it stays a warning, so a rehearsal before the freeze is still possible.
bin/test-release-notes.sh gains eight assertions, six of them verified red against the previous
renderer. The empty-body one counts BYTES rather than comparing to "": `$(...)` strips the
trailing newline, so the one-byte body this commit is about compares equal to empty and the
obvious spelling of the assertion passes over the bug.
32 assertions, exit 0; all 29 real CHANGELOG.adoc sections still render byte-identically.
…till says (unreleased) The instruction to drop the `(unreleased)` suffix before dispatching lived in a docs/inflight/ note, which is deleted when its work lands - so 0.6.0.1 would rediscover it - and the renderer only warned, into a job log nobody reads at minute thirty of a release. That is a documented invariant with no check behind it, which this repo has decided is not an invariant at all. release.yml now passes --strict whenever dryRun is false, so a real release stops at the render step - before anything is committed, tagged, deployed or published - rather than tagging a CHANGELOG.adoc that still calls the version unreleased. A dry run deliberately does not pass it: rehearsing the body BEFORE the heading is frozen is the point of rehearsing, and a flag that blocked that would just be turned off. Also states, where a reader will hit it: - docs/releasing.md gains the frozen-heading rule and corrects "empty" to "renders to nothing" - the two differ, and the difference is the bug the previous commit fixed. - docs/inflight/release-0.6.0.0.md drops its explanation of why the suffix is tolerated and points at docs/releasing.md as the owner, keeping only the part that is transient: that 0.6.0.0 has not had it done yet. - The render step's comment now says why bin/test-release-notes.sh is re-run here when repo-hygiene.yml already ran it on the same master SHA. A reader could not previously tell deliberate belt-and-braces from a leftover.
…g the renderer for its stderr Three defects in the self-test harness, none of which could fail the suite - which is what made them worth finding. **Without python3 the suite exited 1.** bin/check-all.sh maps 1 to FAIL and 2 to CANNOT, and repo-hygiene.yml's macOS lane maps anything but 2 to "self-test FAILS on macOS" - so a box without an interpreter reported this as a broken gate rather than as coverage that did not run. That is the attribution failure the macOS lane's own step comment spends a paragraph on. It now probes the way bin/check-docs-data.sh and bin/check-upstream-map.sh do and exits 2, with one difference: it asserts `sys.version_info[0] >= 3` rather than accepting any `python`, because the renderer's `print(..., file=...)` is a syntax error under Python 2, so the house probe would select an interpreter that cannot run the thing under test. **Fixture paths used $RANDOM, inside `$(...)`.** bash before 5.1 does not reseed $RANDOM in a subshell and `changelog()` is only ever called in a command substitution, so on the macOS bash-3.2 lane every fixture would land on one path and silently overwrite the last - while THREE_SECTIONS, written first, is still read by asserts two thirds of the way down the file. On bash 5 it is a birthday collision across 15 draws instead. mktemp has neither problem. **Part B spawned three processes per failing section to learn one thing.** The first re-run discarded stdout, stderr and its exit status - dead code producing nothing any later line consumed; the second existed only to recover the stderr render_status had thrown away. One invocation now keeps its own stderr in a file. Also gitignores __pycache__: bin/release-notes.py is the first module here anything might import rather than run, and CPython drops bytecode beside the source when it does. 32 assertions, exit 0. No assertion was changed or removed.
… one workflow that breaks it
The rule this branch introduced - a dispatch input reaches a `run:` block as an environment
variable, never as `${{ }}` - was written only into docs/releasing.md, which nobody editing a
non-release workflow opens. It was already stale on arrival:
mutation-full-sweep.yml interpolates the free-text `threads` dispatch input straight into its
`run:` line, on the self-hosted runner, two lines below the same step correctly env-binding
PIT_TARGET_CLASSES and PIT_TARGET_TESTS. A convention with one live violation and no reader is
not a convention.
So: bin/AGENTS.md's Workflows section - which already owns "one version per action", the same
shape of rule - states it in two lines and names docs/releasing.md as the owner, and the
`threads` input joins its two neighbours in `env:`. bin/CLAUDE.md bridges that file into a
Claude Code session the moment anything under bin/ is touched, which a topic doc does not do.
Nothing enforces it yet; a bin/check-workflow-run-interpolation.sh scanning `run:` bodies for
`${{ inputs.* }}` and `${{ github.event.* }}` is the shape that would, in the mould of
bin/check-action-versions.sh, and is left for its own change since it needs a self-test.
Sibling scan for the same defect class, since a fix that removes today's instance invites
tomorrow's: `${{ }}` inside a `run:` body across .github/workflows/ leaves maven.yml's
`run: ${{ matrix.cmd }}` (workflow-file data, not user input) and quarantine-lane.yml's
`${{ steps.run.outcome }}` (an Actions enum). Neither is attacker-controlled; both left alone.
… every relative link
`bin/release-notes.py v0.5.2.2` is supported and tested - historic sections are headed
`== v0.5.2.2`, so the extractor normalises the prefix away and both spellings find the same
section. The default git ref for relative links did not normalise it:
$ python3 bin/release-notes.py v0.5.2.2 --changelog c.adoc
- See [the doc](https://github.com/astubbs/parallel-consumer/blob/vv0.5.2.2/docs/x.md).
`vv0.5.2.2` is not a tag. The notes render perfectly and every `link:docs/...` in them 404s -
a failure that is invisible at render time and only shows up when a reader clicks, which is
precisely the class this script exists to close. The two spellings now produce byte-identical
output, asserted by comparing them to each other rather than to a hardcoded URL, so the case
cannot rot when the repo URL changes.
Found by adversarially probing the renderer rather than by a test failing; verified red
against the previous commit. 33 assertions, exit 0.
… in Markdown The comment claimed a blank line "keeps the block with its item". It does not: CommonMark ends a list at an unindented paragraph, so the continued block trails the list rather than attaching to the item. The output still reads correctly - checked against both `+` uses in CHANGELOG.adoc - and preserving the attachment would mean the line converter carrying list state, which is not worth it for two sites. So the behaviour stands and the comment now says what it is, rather than what would be nicer.
🟢 Throughput — OKThis branch measured about 13% faster than master, on the one test this measures. That is INSIDE this test's own run-to-run spread of about 17%, so read it as a reading and not as a result - re-running the same commit moves it by about as much.
Allowable range 🟢 ≥ 0.70 · 🟡 0.50–0.70 (about a 30% loss) · 🔴 < 0.50 (about a 50% loss) What the numbers mean, and what they cannot tell youThe one that gets misread. Why a shape and not a rate. A rate depends on which runner you drew. A shape does not: every test here processes a fixed number of records, so a runner twice as slow doubles the subject and the controls together and leaves their ratio alone. That is the whole trick, and it is why the reported rate is shown last and labelled as this machine only. Reading the comparison. By conservation, not by correction. Every test in this lane processes a fixed number of records, so within one run the ratio of one test's time to another's is invariant under machine speed — a runner twice as slow doubles both terms and leaves the ratio alone. There is no machine-index correction to be wrong, because nothing needed correcting. Per-method times, not class times. A class time is Reference is the median of 10 recent What this still cannot do. It removes machine-to-machine variance. It does not remove this test's own run-to-run variance, measured at about 30% on a single unchanged commit while its controls stayed within 5%. That is a property of the test, not of the comparison, and no arithmetic here can touch it — which is why the reference is a median and the bounds are deliberately coarse. 🟡 means look at this; only 🔴 is outside the measured spread. Runs used: 43ed239, b1a6dbd, a37d148, e8bd2cb, 1743297, 4bc6e7a, b62c310, c381310, c79424a, 9c67c89 Since the previous push: ratio 1.203 -> 1.127, share 1.526 -> 1.628, rate 85731 -> 94150 (+9.8%). One push of difference sits inside this test's measured spread - read it as movement, not as a result. Updated for |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #199 +/- ##
============================================
+ Coverage 82.79% 83.26% +0.47%
- Complexity 1596 1604 +8
============================================
Files 96 96
Lines 5475 5475
Branches 554 554
============================================
+ Hits 4533 4559 +26
+ Misses 745 723 -22
+ Partials 197 193 -4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… port bin/release-notes.py landed the same day the Node-default ruling did (bin/lib/source-patterns.mjs, #403), which says the repo already chose Node over Python. The gate matches only sh|bash, so nothing flagged the .py; the .sh beside it carries a shell-justified: line. Owner's ruling, 2026-09-02: let it ride, port later. This line is where that decision lives, so the next sweep finds a decision rather than an oversight. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CMeraeBL2ycXVHEaTHM86W
…landed on both sides #201 squash-merged while this branch carried the same two release notes with a fresher MDC state. Both hunks were the same fact from two directions: master had dropped the load-factor bullet as "now #201" but still carried the stale "MDC is not captured" claim this branch had already corrected (#205 landed it). Resolved to the state that is true now that both have merged - the triage list is recorded as landed in the blockers note, and the release note's "bugs found while triaging" section, which had nothing open left in it, is removed along with its heading, which nothing cites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CMeraeBL2ycXVHEaTHM86W
…ely ignores Thirty-nine items are in flight - 32 open PRs plus seven with no PR yet - and the ordering had been re-derived from scratch three times in one session, differently each time. This writes it down once. Ordered by value to the release, explicitly not by merge mechanics: conflicts, red checks and stale bases are not inputs to it. Records the two hard constraints (#199 cannot be applied after the tag, #207 decides what already deployed v6 readers will tolerate forever), the one item with schedule risk (the Connect PoC is not written yet), and the three open decisions the order cannot settle on its own. Per the directory's rules it names no PR titles or states - `gh pr list` owns those - and gives the command to re-check coverage instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hecklist #476 vetted every open note and touched five this branch also edits. Three conflicted and each is resolved on the side that reflects the later decision: - release-0600-blockers.md: deleted here, modified there. Kept deleted; the two corrections the sweep made to it are folded into the burn-down's tag-day checks. One of them is only half right and is recorded as such: release.yml does build a notes file from the changelog (#72), but its exact heading match fails on the "(unreleased)" suffix and falls back to generated notes, which is what #199 fixes. The sweep's "the MDC gap is real" is refuted on the tree - MdcPropagation captures and restores the caller's context since #205 - so the burn-down keeps that correction. - release-experimental-module-records.md: the 2026-09-07 deferral wins over the sweep's vetted marker, per the per-note contract (a deferred note carries the state, not the marker). - release-when-is-v6-good-enough.md: the sweep's dated "the date passed" paragraph and its stale vetted marker ("the question is still unanswered") are dropped; the rewritten note records the decisions and names the failure mode in its own words. Also carried from the sweep: the citation to the poisoned-transaction wedge note is repointed to the sibling #476 merged it into; the burn-down now points at the sweep's own "what gates v6" list in process-candidate-ranking.md and records where the two disagree (the sweep reads the poisoned pair as not gating, and the batchSize validate() bound as the cheapest real fix, which could ride in tier 1); and the #44 exception is narrowed - #466 bounded the revoke wait, so what #408 still owns is declining rather than waiting. Claude-Session: 460f7df9-dcc2-4b00-a9f9-62f3a2c6d5e4 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
56 commits of master, none of which touched this PR's mechanism: master changed neither `.github/workflows/release.yml`, `bin/release-notes.py`, `bin/test-release-notes.sh` nor `.github/workflows/mutation-full-sweep.yml` since the branch's last master merge, so the renderer and the workflow step that calls it merge unchanged. Master's `release.yml` still carries the inline `awk`/`sed` converter this PR replaces. Master also changed six of the files this PR touches. Four auto-merged: `docs/releasing.md` and `bin/AGENTS.md` (#457's job batching, #442's integration sharding), `.gitignore` (#378, #440), and `docs/inflight/release-0.6.0.0.md` (#448, #476). Two conflicted. `docs/inflight/release-0600-blockers.md` - the #197 triage bullet, inside the `post-merge: checked` markers. This branch (2026-09-01, 2026-09-03) rewrote it in post-merge terms as "all four landed"; master's grooming sweep (#476, 2026-09-08, correcting on 2026-09-07) rewrote the same bullet to say the empty-release-body item **was already false when written** - #72 gave the workflow a `--notes-file` built from the `CHANGELOG.adoc` section on 2026-07-29, with `--generate-notes` only as a fallback. Master's correction is right and is the later decision, so it is kept; master's claim that MDC is still open is not - #205 merged 2026-08-27, which is why this branch corrected it, and that correction is kept. Resolved as one bullet carrying both later facts, and it now states what #199 actually does (replace the inline converter, and fail the release on a missing or unrenderable section) rather than the retired claim that it restores a body that was never absent. `docs/refactoring.md` - two unrelated new sections appended at the same point. Both kept: this branch's "`bin/release-notes.py` is Python in a Node-default `bin/`" and master's "JUnit tag resolution is implemented twice". No content from either side dropped. Neither `--ours` nor `--theirs` was used. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
🧪🔒 Quarantine Lane ReportThe quarantine lane is empty - no Any earlier row on this PR asking for a Lane: non-gating; rules: see the Quarantine Audit check. No quarantined test changed outcome since the previous push. Updated for |
… named, and five stale lines are corrected Owner's decisions, 2026-09-09: the merge queue is closed as of today, with later finds 0.6.0.x unless data loss on a default configuration; the poisoned-transaction wedge is the second named exception beside #44, and the release-note draft now carries it; the gate-latch warning #487 argued for is v6-sized and joins tier 1 as the last item; the upstream flat-counter reporters are not asked. Corrections from the owner's read of the note: the release page body is posted by hand on the day with gh release edit, because release.yml's exact heading match misses the unreleased heading on master - so #199 follows the tag rather than gating it, and the two lines that said the workflow already publishes the curated section are fixed; the #468 line no longer asks the reader to check a PR body for two by-key removals that #468 dismissed and #492 fixed; the vetting sweep's opening claim that the quarantine registry is non-empty is struck as the sweep's dated reading; and the disposition list names the three deferred bug notes it omitted. Claude-Session: 460f7df9-dcc2-4b00-a9f9-62f3a2c6d5e4 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…d the notes and README now say so
The opening paragraph of the 0.6.0.0 section claimed "no method signature
changed, but two identifiers did", and the coordinates bullet under Breaking
said "no signature changed, so nothing else in your code moves". Both were
false, and the seven bullets beneath them said so: a renamed exception, a
removed method, a new exception type on the commit failure surface, changed
protected signatures on the controller, a changed stream contract, and
identity equality on RecordContext. The owner caught it on the PR.
The claim was copied from src/docs/README_TEMPLATE.adoc, which carried it
twice - in the fork summary ("the API is source-compatible") and in the
Upgrading section ("Nothing else changes. The API is source-compatible, so
beyond the import lines no source edit is needed"). Both were written when
the rename was the only change and were never revisited as the breaking
changes landed. Same defect, four sites; all four are corrected here, and
README.adoc is regenerated from the template with the asciidoc-template
plugin rather than hand-edited.
What the text now says, in all four places: for most users the upgrade is the
pom and the imports, the committed offset format is unchanged so a consumer
group upgrades in place, and the API changes are the short list under
Breaking - named in the README so a reader knows what to look for before
following the link.
The rendered release-page Markdown was regenerated from the corrected section
with #199's converter in strict mode.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
…e the release Owner review on #498, five changes: - The opening no longer teases three roadmap items in a clause. A new "What comes next" section lists the queue by state - implemented and on an open PR (fencing recovery, virtual threads, self-tuning concurrency, global rate limiting, Streams and Connect previews, the multi-language sidecar and in-process clients, the commit-failure seam, the health check, residence time, the dashboard, offset density, the direct-pull engine, the docs site, the API gate); designed but not built (the dead-letter queue, batch failure attribution, the poll-path error seam, micro-batching, bounded buffers, the Java 17 baseline); and the 1.0 API settlement. The list is drawn from docs/data/roadmap.yaml and the open PR list, and says so; previews are marked as previews per the announcement note's rule. - Breaking is rewritten as nested bullets: one line of consequence per change, one sub-bullet per thing a reader must do or know, no paragraphs. Same content, about half the words. - The Fixes subsection "Records lost or duplicated with nothing in the logs" is "Priority 1: data loss and duplicates". - The intake-stall limitation no longer says "silently": #497 adds a WARN when the gate has stayed latched with nothing retiring, and is in tier 1 for this release. An AsciiDoc comment beside the bullet records the tag-day dependency: if #497 has not merged, the WARN sentence comes out and "silently" goes back. - A "The size of this release" section quantifies the gap from 0.5.3.2, upstream's last published release: merged PRs, main and test Java lines added and removed with rename detection, and new files, main against test. These are figures a command can produce, which docs/merge-checklist.md warns against; they are here because the section is frozen at the tag and the release is the one place a point-in-time number is the point. The commands are in a comment beside them, and recomputing them is a tag-day check named in the PR body. Lines over 120 columns from the rewrite were rewrapped. The rendered Markdown was regenerated with #199's converter in strict mode and its self-test passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
…ighten Breaking, name what comes next, and size the release Owner review on #498, in one commit. THE SOURCE-COMPATIBILITY CLAIM WAS FALSE, in four places. The opening paragraph said "no method signature changed, but two identifiers did", and the coordinates bullet under Breaking said "no signature changed, so nothing else in your code moves" - while the bullets beneath them listed a renamed exception, a removed method, a new exception type on the commit failure surface, changed protected signatures on the controller, a changed stream contract, and identity equality on RecordContext. The claim had been copied from src/docs/README_TEMPLATE.adoc, which carried it twice (the fork summary and the Upgrading section), written when the rename was the only change and never revisited. All four sites now say: for most users the upgrade is the pom and the imports, it is not source-compatible beyond that, the offset format is unchanged so a consumer group upgrades in place, and the API changes are the short list under Breaking - which the README names, so a reader knows what to look for before following the link. README.adoc is regenerated from the template with the asciidoc-template plugin. WHAT COMES NEXT replaces a one-clause teaser. A new section lists the queue by state, drawn from docs/data/roadmap.yaml and the open PR list: implemented and on an open PR (fencing recovery and the two transactional fixes it unlocks, virtual threads, self-tuning concurrency, global rate limiting, the Streams and Connect previews, the multi-language sidecar and in-process clients, the commit-failure seam, the health check, residence time, the dashboard, offset density, the direct-pull engine, the docs site, the API gate); designed but not built (the dead-letter queue, batch failure attribution, the poll-path error seam, micro-batching, bounded buffers, the Java 17 baseline); and the 1.0 API settlement. Previews are marked as previews, per the announcement note's rule. BREAKING is nested bullets: one line of consequence per change, one sub-bullet per thing a reader must do or know. Same content, half the words. THE PRIORITY-1 FIXES SUBSECTION is named "Priority 1: correctness", for the property rather than the failure. THE INTAKE-STALL LIMITATION no longer says "silently": #497 adds a WARN when the gate has stayed latched with nothing retiring, and is in tier 1 for this release. An AsciiDoc comment beside the bullet records the tag-day dependency - if #497 has not merged, the WARN sentence comes out and "silently" goes back. THE SIZE OF THIS RELEASE is a new section quantifying the gap from 0.5.3.2, upstream's last published release: merged PRs, main and test Java lines added and removed with rename detection, and new files, main against test. These are figures a command can produce, which docs/merge-checklist.md warns against; they are here because the section is frozen at the tag and the release is the one place a point-in-time number is the point. The commands are in a comment beside them, and recomputing them is a tag-day check named in the PR body. The rendered Markdown was regenerated with #199's converter in strict mode and its self-test passes; every line of the section is within 120 columns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
…es (#498) The release-time rewrite of CHANGELOG.adoc's `== 0.6.0.0` section, so that it is the published release notes rather than the working text it had been since the fork. docs/releasing.md says the section for the release being cut is generated at release time, replacing what is there; there is no generator in bin/, so this is that generation, done by hand from the first-parent commit log since the fork point (every fix, fix!, feat, feat! and deps body read in full) and from the release documents: the v6 burn-down note on #475, docs/inflight/release-0.6.0.0.md, docs/data/roadmap.yaml and the open PR list. It is the one deliberate exception to "a PR never adds a changelog entry", which exists so this rewrite can be written as a set. What the section now says, in order: the fork and its coordinates, the stability-release argument, and that upgrading is the pom and the imports for most users but is not source-compatible beyond that; the size of the release since 0.5.3.2, upstream's last published release; Breaking, as nested bullets - the coordinates and Java packages, the commit-budget exception, JStream blocking until close, the metadata-policy default, the batchSize bound, the exception rename and removed listener getter, identity equality for RecordContext, the two controller narrowings, the Mutiny Java 17 floor; Fixes in three subsections - priority 1 correctness, consumption stopped after a rebalance (the confluentinc#857 story: the mechanisms closed, the detector lines demoted to timing proxies, the one unattributed chaos-only arm), other fixes; Known limitations, stated so the release claims no more than it can show; What comes next, by state - implemented and on an open PR, designed and not yet built, toward 1.0; Dependencies re-read against the poms; Examples; and Build & CI, the lanes that say how the library is tested and analysed, with Fray named as the next concurrency lane. Claims removed or corrected from the old text: the "source-compatible" claim, which the Breaking list itself contradicted, removed here and from src/docs/README_TEMPLATE.adoc in two places, README.adoc regenerated; the wrong upstream attribution on the null-epoch fix that #217 asked to be dropped; "upstream's last release 0.5.3.3" corrected to 0.5.3.2 published; the Reactor version, which said 3.8.6 while the pom says 3.8.7; the self-hosted lane described as per-PR; the quarantine state; two counts restated as shape; two upstream bullets folded into the fork entries that carry them. The heading loses its "(unreleased)" suffix, so release.yml's exact heading match now finds the section. The release page body is still posted by hand on the day from the converter's Markdown, per the burn-down's tier 3; #199 follows the tag. Tag-day checks this leaves: #497 must be merged, or the WARN sentence in the intake-stall limitation comes out and "silently" goes back (an AsciiDoc comment beside the bullet says the same); and the figures under "The size of this release" are recomputed with the commands in the comment beside them. Also touched: docs/inflight/release-0.6.0.0.md gains the settled release-condition wording at the same insertion point #475 amends it, whichever merges second keeps the settled paragraph; docs/releasing.md no longer says the section's generation is undecided. Co-authored-by: Claude Fable 5.1 (1M context) <noreply@anthropic.com>
Brings in the v6 changelog finalisation (#498), the batchSize bound, the gate-latch warning and the Lincheck timeout change. The changelog heading is now `== 0.6.0.0` on master, so this branch's strict-mode refusal of an `(unreleased)` heading no longer fires on a real run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
|
Successor: #501 converts |
… is its section verbatim The file was renamed in the previous commit with its AsciiDoc contents intact; this commit converts them. CHANGELOG.adoc used almost nothing of AsciiDoc: headings, `url[text]` link macros, single-star bold, `**` nested bullets, three NOTE admonitions, one continuation and a vestigial toc/ifndef preamble from when the README included it. GitHub release bodies are Markdown, so the file's format was the only reason a converter had to exist between the two - #199's 412-line parser and its 397-line self-test. Converting the file once removes the need for both. The new content is every version section of the old file rendered by that converter in strict mode (all eleven render, including the finalised 0.6.0.0 section, whose body is byte-identical to the one #498 was going to post by hand), under `## <version>` headings, with the preamble converted by hand. The 0.6.0.0 text is unchanged in content. release.yml gains an "Extract the release notes" step before release:prepare that pulls the `## <version>` section with awk and fails the run if it is missing or empty - the strict-mode guarantee #199 had, in six lines, with no fallback to --generate-notes. The GitHub release step posts that file. A dry run rehearses the extraction and puts the body in the job summary. What encoded the AsciiDoc shape changes with it: the changelog citation gate reads `###` section headings and `-` bullets (its tests updated), the package-rename guard's comment names the `### Breaking` bullet, and the live docs name the heading syntax as it is now. docs/releasing.md says what the workflow now does with the file. Supersedes #199, whose converter did its one job here and whose release.yml changes are replaced by the six-line extract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
… is its section verbatim The file was renamed in the previous commit with its AsciiDoc contents intact; this commit converts them. CHANGELOG.adoc used almost nothing of AsciiDoc: headings, `url[text]` link macros, single-star bold, `**` nested bullets, three NOTE admonitions, one continuation and a vestigial toc/ifndef preamble from when the README included it. GitHub release bodies are Markdown, so the file's format was the only reason a converter had to exist between the two - #199's 412-line parser and its 397-line self-test. Converting the file once removes the need for both. The new content is every version section of the old file rendered by that converter in strict mode (all eleven render, including the finalised 0.6.0.0 section, whose body is byte-identical to the one #498 was going to post by hand), under `## <version>` headings, with the preamble converted by hand. The 0.6.0.0 text is unchanged in content. release.yml gains an "Extract the release notes" step before release:prepare that pulls the `## <version>` section with awk and fails the run if it is missing or empty - the strict-mode guarantee #199 had, in six lines, with no fallback to --generate-notes. The GitHub release step posts that file. A dry run rehearses the extraction and puts the body in the job summary. What encoded the AsciiDoc shape changes with it: the changelog citation gate reads `###` section headings and `-` bullets (its tests updated), the package-rename guard's comment names the `### Breaking` bullet, and the live docs name the heading syntax as it is now. docs/releasing.md says what the workflow now does with the file. Supersedes #199, whose converter did its one job here and whose release.yml changes are replaced by the six-line extract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
… is its section verbatim The file was renamed in the previous commit with its AsciiDoc contents intact; this commit converts them. CHANGELOG.adoc used almost nothing of AsciiDoc: headings, `url[text]` link macros, single-star bold, `**` nested bullets, three NOTE admonitions, one continuation and a vestigial toc/ifndef preamble from when the README included it. GitHub release bodies are Markdown, so the file's format was the only reason a converter had to exist between the two - #199's 412-line parser and its 397-line self-test. Converting the file once removes the need for both. The new content is every version section of the old file rendered by that converter (all twenty-nine render - the eleven unprefixed ones in strict mode - including the finalised 0.6.0.0 section, whose body is byte-identical to the one #498 was going to post by hand), under `## <version>` headings, with the preamble converted by hand. The 0.6.0.0 text is unchanged in content. release.yml gains an "Extract the release notes" step before release:prepare that pulls the `## <version>` section with awk and fails the run if it is missing or empty - the strict-mode guarantee #199 had, in six lines, with no fallback to --generate-notes. The GitHub release step posts that file. A dry run rehearses the extraction and puts the body in the job summary. What encoded the AsciiDoc shape changes with it: the changelog citation gate reads `###` section headings and `-` bullets (its tests updated), the package-rename guard's comment names the `### Breaking` bullet, and the live docs name the heading syntax as it is now. docs/releasing.md says what the workflow now does with the file. Supersedes #199, whose converter did its one job here and whose release.yml changes are replaced by the six-line extract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
…ter dropped The version sections were rendered by #199's converter, which does not emit AsciiDoc line comments - so every `//` block inside a version section was silently lost in the conversion. Only the preamble's one comment survived, because the preamble was converted by hand. The automated review found one of the four. Grepping the defect class rather than the symptom - `grep -n '^//'` against `3e66041e3^:CHANGELOG.adoc`, which is the last byte-identical copy - finds twelve comment lines in six blocks: - The preamble's `git log --pretty` recipe. Already present, hand-converted. - The `// only show TOC if this is the root document` note. Correctly gone: it annotated the `ifndef::github_name[]` / `toc::[]` directives, which have no Markdown equivalent and were themselves dropped on purpose. - Regenerate at the tag, above `### Breaking`: the exact commands that recompute the size-of-this-release figures. This is the one the review flagged. - TAG-DAY, in Known limitations: what to do to the latched-gate bullet if #497 had not merged before the tag. - There is no 0.5.3.4 release, at the end of the 0.6.0.0 section: why a version number is missing from the file. - The upstream release-tag URL under `## v0.4.0.0`, a source note on a pre-fork section. The last four are restored as HTML comments, in the positions they held in the AsciiDoc. Nothing rendered changes: comment syntax is the only difference, and content parity is unmoved at 29 `## ` headings, 58 `### ` headings and 342 bullets. The TAG-DAY block is indented two spaces so it sits inside the list item it annotates. A `<!--` at column zero between two bullets is an HTML block, which ends the list and starts a new one - a gap AsciiDoc line comments do not create, and one that would have shown up in the release body. DECISION FOR THE MAINTAINER, deliberately not taken here. Three of these now live inside the `## 0.6.0.0` section, and this PR makes that section the release body verbatim - so they ship into the v0.6.0.0 release notes. They do not render, but they are readable in the body source, which the old converter route never exposed. Regenerate-at-the-tag and TAG-DAY are the maintainer-only two; moving them to docs/releasing.md instead is a one-line change if you would rather the release body carried none of them. Restoring in place is the lossless default and is what the review suggested first. `awk` extraction rehearsed over the modified file: 0.6.0.0 is 631 lines, 0.5.3.3 is 7, v0.4.0.0 is 15 - all non-empty, so the fail-loud guard is not tripped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
… is its section verbatim The file was renamed in the previous commit with its AsciiDoc contents intact; this commit converts them. CHANGELOG.adoc used almost nothing of AsciiDoc: headings, `url[text]` link macros, single-star bold, `**` nested bullets, three NOTE admonitions, one continuation, a handful of `//` line comments and a vestigial toc/ifndef preamble from when the README included it. GitHub release bodies are Markdown, so the file's format was the only reason a converter had to exist between the two - #199's 412-line parser and its 397-line self-test. Converting the file once removes the need for both. The new content is every version section of the old file rendered by that converter (all twenty-nine render - the eleven unprefixed ones in strict mode - including the finalised 0.6.0.0 section, whose body is byte-identical to the one #498 was going to post by hand), under `## <version>` headings that keep their original text, with the preamble converted by hand. The 0.6.0.0 text is unchanged in content. Parity against the old file: 29 version sections, 58 section headings, 5 sub-headings, 342 bullets in both of the old file's bullet syntaxes, 75 nested bullets. The converter does not emit AsciiDoc line comments, so the four `//` blocks inside version sections are restored by hand as HTML comments in the positions they held: the recompute commands for the size-of-this-release figures above `### Breaking`; the tag-day note on the latched-gate bullet in Known limitations, indented two spaces so it stays inside the list item (a `<!--` at column zero between two bullets is an HTML block and would split the list); the "there is no 0.5.3.4 release" note at the end of the 0.6.0.0 section; and the upstream release-tag URL under `## v0.4.0.0`. The one comment not carried over annotated the toc/ifndef directives, which have no Markdown equivalent and were dropped on purpose. Three of the four sit inside `## 0.6.0.0`, so they travel in the release body's source without rendering; moving the two maintainer-only ones to docs/releasing.md is the owner's call. release.yml gains an "Extract the release notes" step before release:prepare that pulls the `## <version>` section with awk and fails the run if it is missing or empty - the strict-mode guarantee #199 had, in six lines, with no fallback to --generate-notes. The GitHub release step posts that file. A dry run rehearses the extraction and puts the body in the job summary. What encoded the AsciiDoc shape changes with it: the changelog citation gate reads `###` section headings and `-` bullets (its tests updated), the package-rename guard's comment names the `### Breaking` bullet, and the live docs name the heading syntax as it is now. docs/releasing.md says what the workflow now does with the file. Supersedes #199, whose converter did its one job here and whose release.yml changes are replaced by the six-line extract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
… is its section verbatim The file was renamed in the previous commit with its AsciiDoc contents intact; this commit converts them. CHANGELOG.adoc used almost nothing of AsciiDoc: headings, `url[text]` link macros, single-star bold, `**` nested bullets, three NOTE admonitions, one continuation, a handful of `//` line comments and a vestigial toc/ifndef preamble from when the README included it. GitHub release bodies are Markdown, so the file's format was the only reason a converter had to exist between the two - #199's 412-line parser and its 397-line self-test. Converting the file once removes the need for both. The new content is every version section of the old file rendered by that converter (all twenty-nine render - the eleven unprefixed ones in strict mode - including the finalised 0.6.0.0 section, whose body is byte-identical to the one #498 was going to post by hand), under `## <version>` headings that keep their original text, with the preamble converted by hand. The 0.6.0.0 text is unchanged in content. Parity against the old file: 29 version sections, 58 section headings, 5 sub-headings, 342 bullets in both of the old file's bullet syntaxes, 75 nested bullets. The converter does not emit AsciiDoc line comments, so the four `//` blocks inside version sections are restored by hand as HTML comments in the positions they held: the recompute commands for the size-of-this-release figures above `### Breaking`; the tag-day note on the latched-gate bullet in Known limitations, indented two spaces so it stays inside the list item (a `<!--` at column zero between two bullets is an HTML block and would split the list); the "there is no 0.5.3.4 release" note at the end of the 0.6.0.0 section; and the upstream release-tag URL under `## v0.4.0.0`. The one comment not carried over annotated the toc/ifndef directives, which have no Markdown equivalent and were dropped on purpose. Three of the four sit inside `## 0.6.0.0`, so they travel in the release body's source without rendering; moving the two maintainer-only ones to docs/releasing.md is the owner's call. release.yml gains an "Extract the release notes" step before release:prepare that pulls the `## <version>` section with awk and fails the run if it is missing or empty - the strict-mode guarantee #199 had, in six lines, with no fallback to --generate-notes. The GitHub release step posts that file. A dry run rehearses the extraction and puts the body in the job summary. What encoded the AsciiDoc shape changes with it: the changelog citation gate reads `###` section headings and `-` bullets (its tests updated), the package-rename guard's comment names the `### Breaking` bullet, and the live docs name the heading syntax as it is now. docs/releasing.md says what the workflow now does with the file. Supersedes #199, whose converter did its one job here and whose release.yml changes are replaced by the six-line extract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XS64Xttx4vF5datYh7fmk
|
Closing as superseded by #501, merged 2026-09-09. The changelog is now |
…ot by hand The review caught six passages still describing the heading-match bug #501 fixed and closed #199 on: release.yml now extracts the changelog's versioned section verbatim, fails if it is missing, and posts it as the release body. The tier 3 box for posting by hand is withdrawn and ticked as done by that PR, #199 leaves the can-follow list, the ownership table names the Markdown changelog, and the tag-day check becomes "read the release page and confirm the body is the section". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xoi3HYae8pjsEatuNFKieD
…ut it (#475) 0.6.0.0 is a bugs-only stability release, and it is overdue: the fork has carried the fixes for upstream's most-reported defects for months while the release waited on features. This note is the source of truth for cutting it - the owner's decisions, the merge queue from those decisions to the tag, every open question, and the checks that make the published artefacts true on the day. #197 is the tracking handle and its body points here; nothing is maintained on the issue. THE DECISIONS, 2026-09-07 and confirmed since. The bar is the stability release and nothing else; Streams and Connect move to the next-0x horizon in the roadmap data; the producer-recovery stack is outside v6. The release claim carries two named exceptions rather than waiting on them: the transactional revoke wait (#44, bounded since #466, not yet declined) and, from 2026-09-09, the poisoned-transaction wedge, both in the transactional producer mode only. The merge queue closed on 2026-09-09; later finds are 0.6.0.x unless they are data loss on a default configuration. THE BURN-DOWN, recorded as each merge landed. Tier 1, the self-contained fixes, is complete: the last two to join were the batchSize bound (#496) and the gate-latch warning (#497), both decided v6-sized on the day the queue closed. Tier 3, the plumbing, has the changelog section finalised as the release notes and the claim amended (#498) and the release page body posted verbatim from CHANGELOG.md by release.yml (#501, closing #199); what remains is the tag-day checks, the drafted issue responses, and the tag. A can-follow list names what is deliberately not v6. WHAT THE RELEASE NOTE SAYS ABOUT THE confluentinc#857 FAMILY, each line with the PR that settled it: the revoke-path deadlock proven by control arm, the eager stall withdrawn as a timing bound that flips with the processor count, the fifth item measured as the consumer-group protocol under churn rather than PC, the poller death fixed, the instance-stall sightings classified as worker saturation from the load side. The intake stall #471 found has its verdict from #487: the record-intake load gate is what stops intake, head-of-line blocking is not why, and any instance that retries forever while a fraction of its stream never succeeds latches eventually at a computable threshold, idle or not. There is no gate fix; the fix bounds the failures (#149's dead-letter queue), and until then #497 makes the state visible. One arm stays unattributed and is named as such. DATA LOSS AND DUPLICATES: the bug-162 replay branch refuted and the false truncation warning fixed (#494, closing #162). KNOWN UNKNOWNS, split in two so nothing is papered over: what is still unknown at the cut - the shard half of the per-shard liveness blind spot, the flake rows kept open with reasons, the maturity claim - and, under its own heading, the unknowns made known on 2026-09-08 and how each was settled. TAG-DAY CHECKS, folded in from the retired blockers note: master green with the lanes known to lie named, the churn scenario's no-progress window settled by replay and widened in #499 with the rebalance-dwell bound named as that class's survivor, the Lincheck lane's timeout raised against runner-speed variance, the rename named in both groupId and packages, the README's trademark wording claiming nothing it does not have (#495), and the changelog section as the release notes since #498, posted as the release body by release.yml since #501. ONE CHANGELOG EDIT, on the owner's decision of 2026-09-10: the "size of this release" table of merged-PR and line counts is removed. Measured on a branch, carrying its own re-measure instruction, stale from the next merge on; the notes make their claim through the fixes they name. Also here: a ci- note from this PR's own last review round - the file-refs gate reads a token as a path only with two segments, so the changelog rename left this branch-only note naming the old file with nothing to go red, and the note records the allow-list that would close it; the vetting sweep's reading and every open bug note's disposition, moved into the ranking note where the tiers override them; a dated survey of upstream items with no fix and no response as its own deferred note; the refactoring registry's codec entry corrected for what #480 did and did not change; and the confluentinc#546 manifest entry marked merged. Two notes retired with their content migrated: the blockers register and the merge-order plan for a far larger v6. The question this note began as, "when is v6 good enough?", was answered on 2026-09-08 and the file renamed. Serves #197; closes nothing. The tracker closes when the tag is cut. Co-authored-by: Claude Fable 5.1 (1M context) <noreply@anthropic.com>
Description
Fixes the "release page has no body" sub-item of #197.
What was wrong.
.github/workflows/release.yml(notpublish.yml- that one only publishessnapshots) does create the GitHub Release, and it did try to use the changelog: an inline
awkcompared each line to the literal string
== <releaseVersion>. The section is headed== 0.6.0.0 (unreleased), so it matched nothing, and the step fell back to--generate-notes. On therelease page an auto-generated commit list is indistinguishable from the curated notes having silently
vanished, so the bug reported nothing. That fallback is the bug, and it is gone.
What now happens.
bin/release-notes.py <version>prints the version'sCHANGELOG.adocsection asMarkdown.
release.ymlrenders it before it commits, tags, deploys or publishes anything - so amissing section fails while it is still cheap, rather than after the artifacts are on Central - and a
Dry run rehearses the render and puts the body in the job summary. A missing, empty, or
unconvertible section is a non-zero exit that fails the release. There is no fallback body.
This branch was re-cut onto today's master, discarding its own redundant copy of the
io.confluent.*→bz.stub.*rename now that master has completed that rename itself, then given alocal
simplify+code-reviewpass. Both changed the PR materially; see What the review passchanged below.
The AsciiDoc -> Markdown decision
Convert a bounded subset, and reject the rest loudly. Not "extract and accept the degradation", not
"link and give up".
===headings,*/**bullets,.ordered lists,https://…[text]andlink:…[text]macros,*bold*,`mono`,NOTE::,+continuations,//comments. Everything the two languages already share (paragraphs, monospace) passes through.attributes, includes, conditionals, xrefs - is an error, not a silent pass. That is what keeps
"bounded subset" from decaying into "mangled markup": the day someone writes a table in a release
section, CI says so.
sedgets wrong and this does not: AsciiDoc bold is one asterisk, which inMarkdown is italics (so
*Quarantine lane*would have quietly changed emphasis); and monospacespans are masked before inline rules run, so
`bz.stub.parallelconsumer.*`keeps its asterisk.Relative
link:docs/…[]targets are absolutised at the released tag - a relative link in a releasebody resolves against github.com and 404s.
not install, and the input is one small file whose shape we control.
What the review pass changed - including two bugs in the original
A local
simplify+code-reviewpass ran before this was pushed. It found two defects theself-test did not, both of which defeat the PR's own purpose:
A section that renders to nothing exited 0 with a 1-byte body. Emptiness was judged on raw
lines, so a section holding only
//comments or a+passed the check and then had every linedropped. That is the blank release page of Release 0.6.0.0 - the fork's first release #197 reached from the other side, inside the
script written to prevent it. Emptiness is now judged on the converted output:
bin/release-notes.py v0.5.2.2producedblob/vv0.5.2.2/…- a supported, tested spellingwhose relative links all 404. The notes render perfectly; only a reader clicking finds out.
code-reviewadditionally found, all verified red against the previous commit:`monospace`span shipped as raw AsciiDoc.convert_inlinesplit on backticks, so a macro's[and]landed in different pieces: exit 0, adead link on the release page, and the surviving single asterisks rendering as italics - the exact
quiet emphasis change the code's own comment says must not happen. Now masks code spans with a
placeholder instead of splitting. A macro written inside a code span still stays literal, asserted.
//line comment could abort a release. A commented-out draft entry holding[source,java], a|cell or a stray backtick failed exit 3, for a line that is dropped. Linecomments are now skipped - but not
////, which matches^\s*//too and is a comment blockwhose contents would reach the body. The reviewer's suggested fix would have opened that hole.
LC_ALL=Ccrashed on em dashes with a traceback reportedas an IO error; and a
mktempregression introduced duringsimplifythat would have turned themacOS lane red (BSD
mkstemp(3)requires trailingXs).And in the shell harness:
python3was absent, not 2.bin/check-all.shmaps 1→FAIL and2→CANNOT-RUN, so a python-less box reported this as a broken gate. It now probes as
bin/check-docs-data.shdoes, but assertsversion_info[0] >= 3- the house probe accepts apythonthat is Python 2, under which the renderer is a syntax error.$RANDOMfixture names inside$(...). bash <5.1 does not reseed$RANDOMin a subshell andthe helper is only ever called in a command substitution, so on the bash-3.2 macOS lane every
fixture landed on one path. Now
mktemp.One rejected finding worth recording: indenting
+continuations to preserve list attachment isright in the abstract but would make the live output worse - both
+uses inCHANGELOG.adocclose a whole bullet list, which is what the current trailing-paragraph output gives. The hazard it
named (an ordered list restarting at 1 after a continuation) has zero instances. Recorded as an
explicit caveat in the code instead.
Review round six: two silent-output paths, closed
The sixth
@claude reviewpass (focused onbin/release-notes.py) confirmed the emptinesspredicate, the code-span masking, the
//vs////asymmetry and the--strictwiring, andindependently agreed with the rejected
+-indentation finding - it re-ran thegrep -cE '^\.+\s' CHANGELOG.adoc-> 0 check and hand-rendered both live+sites.It filed two edges as theoretical and not worth chasing. That review has never been able to execute
anything - its sandbox declines
bash/python3every round - so both were run here, and bothare real. One is worse than the hand-trace suggested:
``like this``, was rewritten INSIDE the span.convert_inlinemasks by splitting on a single backtick, so a doubled delimiter yields an empty span and hands
the text between the two delimiters to
convert_proseas prose. Measured:``link:docs/x[y]``came back as
``[y](https://…/docs/x)``and``a *bold* b``as``a **bold** b``- the exactrewriting
convert_inline's own docstring promises never happens to a monospace span. The backtickcount is even, so the odd-count check could not see it. Now rejected (exit 3) rather than taught:
the masker would have to carry two delimiter widths, and
CHANGELOG.adochas zero instances.*converts to-, which is not blank, somarkdown.strip()passed it:render()returned"- \n- \n"and exited 0, publishing a page ofempty bullets. That is the Release 0.6.0.0 - the fork's first release #197 blank release body reached one indirection
further on, inside the script written to prevent it. Emptiness is now judged on what survives with
the list markers off - limited to the two markers
convert_lineemits, because over-stripping wouldfail a real section as "no notes", and a blocked release is the worse failure. Two assertions guard
that boundary.
Self-test 37 -> 43 assertions. Four of the six go red against the pre-fix renderer (verified by
stashing it); the other two are the over-rejection guards, which pass on both sides. The real
CHANGELOG.adocstill renders byte-identically at 5994 bytes.Behaviour change:
--stricton real releasesrelease.ymlnow passes--strictwhendryRunis false, so a real release refuses while theheading still says
== 0.6.0.0 (unreleased). A dry run still only warns, so rehearsing before thefreeze works. This replaces a manual instruction that lived in a
docs/inflight/note due fordeletion after 0.6.0.0 - an instruction nothing enforced.
No dedicated CI job - the glob already covers it
The original version of this PR added a
release-notesjob to.github/workflows/maven.ymlso therenderer self-test ran per-PR. Master has since made that redundant and it is dropped.
bin/check-all.shdiscoversbin/check-*.shandbin/test-*.shby glob, and.github/workflows/repo-hygiene.ymlrunsbin/check-all.sh --with-testsonpull_request. Adding anamed job would be exactly the hand-maintained list that script exists to abolish. Verified rather
than assumed:
bin/check-all.sh --with-testsreportsok test-release-notes.shin its 42-gate sweep.maven.ymlis byte-identical to master.Documentation went to
docs/releasing.md, notAGENTS.mdAll three of the original payload's documentation hunks targeted
AGENTS.mdsections that master hassince moved out of that file entirely.
AGENTS.mdis byte-identical to master; the content is indocs/releasing.md, which owns the topic - a paragraph under Cutting a release, the env-bindingnote beside it, and a bullet under At release time naming the real enforcer.
Script injection: the claim this PR originally made, and now actually keeps
The first draft of this checklist said the release version "reaches the script as an argv argument,
not interpolated into a shell string". That was false, and the review was right to call it.
${{ inputs.releaseVersion }}is expanded by GitHub Actions as textual substitution into therun:script before bash ever parses it - the surrounding quotes are part of the generated source, not a
guarantee about it, so an input containing shell metacharacters is injected as code.
Rather than soften the wording, the claim was made true.
release.ymlbinds the dispatch inputs toenvironment variables once at job level and every shell step reads
"$RELEASE_VERSION"; the value nowreaches the shell as data it never re-parses.
This deliberately widens the diff. The fix covers the file's four pre-existing interpolations (the
tag-exists guard,
release:prepare,git checkout,gh release create) as well as the render stepthis PR added - hardening one instance and leaving four would have left the file no safer while
implying it was.
release:prepare's-DreleaseVersion=arguments were also unquoted, so they gainword-splitting safety in the same pass.
if:/with:expressions still use${{ }}: Actions evaluatesthose itself, never through a shell, so they are not an injection path.
Exploitability was low either way -
workflow_dispatchalready requires write access - so this ishygiene, and more importantly honesty about what the checklist asserts.
The same defect existed elsewhere and was fixed:
mutation-full-sweep.ymlinterpolated thefree-text
threadsdispatch input into arun:line, two lines below the same step correctlyenv-binding its neighbours. The convention is now stated in
bin/AGENTS.md→ Workflows, whichbin/CLAUDE.mdbridges into any session touchingbin/.Verification
All 29 real
CHANGELOG.adocsections render byte-identically to the pre-review renderer. A12,000-section fuzz run reports
empty: 0, leaked-mask: 0, odd-ticks: 0, broken-links: 0. Therelease.ymlstep was simulated underbash -eo pipefail: dry-run0.6.0.0→ exit 0; real run →exit 4 on the frozen-heading check;
9.9.9.9→ exit 2.gh release createis unreachable in everyfailing case.
Failure modes confirmed loud: missing version (exit 2), unsupported AsciiDoc (3), unfrozen heading
under
--strict(4), unreadable changelog (1). CRLF, unicode, prefix-vs-longer-version(
0.6.0vs0.6.0.0), duplicate headings and pipe/bracket link labels all behave.actionlintis not installed on the dev box and is not in mise, so it was not run. Substitutes:both workflows parse as YAML,
bin/check-action-versions.shpasses, andmaven.ymlis unchangedfrom master so only
release.ymlis exposed. A Dry run of the Release workflow is the cheaprehearsal before merge - it now prints the body to the job summary.
Merge-prep pass: a stale claim in the notes this PR edits
Both release notes still said MDC context was un-captured at submit time. #205
merged on 2026-08-27 (
15a23deb0, addinginternal/MdcPropagation.java), so that was false - andthis branch had re-asserted it while rewriting the bullet, and attested the paragraph
post-merge: checked. The attestation was about tense, which was genuinely correct; the facttravelled through underneath it.
docs/inflight/release-0600-blockers.md- the Release 0.6.0.0 - the fork's first release #197 triage bullet now namesthe one item still open (confluentinc#402: Max loading factor steps reached: 100/100 #155, fix on fix(core) astubbs#155: stop the "Max loading factor steps reached" WARN on every control loop pass #201) and the
landed ones by the PR that closed each. The live tally is gone:
docs/inflight/AGENTS.mdforbidscounts because they rot with nothing going red, and this bullet's count had now been wrong twice.
docs/inflight/release-0.6.0.0.md- the MDC entry is deleted rather than rewritten as fixed, perthat directory's rule that making a stale entry accurate is the wrong move.
docs/releasing.md- "every section of this file" now namesCHANGELOG.adoc. The bullet lives indocs/releasing.md, so "this file" pointed at the wrong one.Two follow-ups recorded, not done here
bin/check-changelog-renders.sh- splitting part B into its own gate, so a changelog edit ischecked by the default
bin/check-all.shsweep rather than only--with-tests. Needs a new gateplus its own self-test and a reviewer-grant decision.
bin/check-workflow-run-interpolation.sh- the env-binding convention above is currently enforcedby nothing.
Checklist
docs/releasing.md(owns the topic),docs/inflight/release-0.6.0.0.md,docs/inflight/release-0600-blockers.md,bin/AGENTS.md, plus a.gitignoreline for__pycache__/. NotAGENTS.md- master moved these sections out; see above.CHANGELOG.adocdeliberately untouched, per AGENTS.mdbin/test-release-notes.sh, 43 assertions, run per-PR viabin/check-all.sh --with-testsinrepo-hygiene.ymlpass; the dropped CI job, the relocated docs and two newly-found bugs are all recorded above
release.ymlandmutation-full-sweep.yml. Inrelease.ymlthe render step runs before any credential is usedand handles no secret, and the net change to the release's authority is a removed fallback.
Dispatch inputs no longer reach any shell by
${{ }}interpolation - see Script injectionPart of #197.