fix(prep): refuse [Unreleased] text the compose would drop; mirror every cut-time gate in the PR body #1486

Merged
alex merged 3 commits from fix/prep-unreleased-drop-cut-gate-mirror into main 2026-10-02 10:21:38 +02:00
Member

What broke (measured)

Measured on 2026-10-02 with rt prep --dry-run (one-shot mode) against a throwaway adopter-shaped repo. The binary was built from the v0.63.1 source (02fae6a); the original report was on v0.63.0. compose-verify was run on the preview applied to the fixture.

A. Silent content loss. [Unreleased] text that is not under an exact ### <Kind> heading disappears:

[Unreleased] shape v0.63.1 composed section rt compose-verify
prose before the first ### six None. sections exit 0
### Notes + bullet six None. sections exit 0
### added + bullet six None. sections exit 0
## Added + bullet six None. sections exit 0
### Added, - kept, then a bare ### + bullet the bullet after the bare heading is gone (not run)

The Cutter's Seal then empties [Unreleased], so the text is gone from main as well as from the release. Mechanism: MergeSections skips lines that have no current kind. It also buckets kinds that are missing from mergeFoldOrder but never emits them.

B. The mirror said clean and the cut refused. Under ### Added, the section held - Reworked the importer. followed by a prose line. The PR description read Changelog density — clean, because it graded checks 7-9 only. rt compose-verify --version 0.2.0 then exited 1 on check 5 (style).

What changes

A. prep.Compose refuses before the fold, so the seal never runs and nothing is written. Verbatim from the new binary:

Error: compose changelog section: [Unreleased] text would be DROPPED from the release: 2 line(s) are not under an accepted heading, and sealing would then delete them from CHANGELOG.md. Nothing was written.
  CHANGELOG.md:5: ### Notes    <- not an accepted heading
  CHANGELOG.md:7: - Reworked the importer.    <- under ### Notes, not an accepted heading
Accepted headings (level 3, exact case): ### Added, ### Changed, ### Fixed, ### Removed, ### Deprecated, ### Upgrade, ### Security, ### Internal.
Move each line under one of them, or delete it, then re-run rt prep.

changelog.UncarriedLines reports the lines. It shares classifyMergeRecord with MergeSections, which is now the only place the merge's line grammar lives. changelog.UnreleasedStartLine turns positions within the body into CHANGELOG.md:<n>.

B. The PR-description block is now gates.MirrorCutGates. It calls ComposeVerify, the function rt compose-verify calls at the cut. It runs on the CHANGELOG bytes prep is about to commit (read after the seal and the hooks, before the push), with the same threshold parser and repo-identity resolver. The block lists register-check plus checks 1-9 by ranging over BodyCheckResult.Checks. gates.SectionDensity is removed.

Decision tree

A: refuse, or report in the PR body?

  • Refuse in rt prep (the lean taken here) is right when the loss cannot be undone from the release's side and the fix is always an edit on main. Both hold. The seal deletes the text from main, and every refused shape has a mechanical fix: move the line under an accepted heading, or delete it. Cost: while [Unreleased] stays malformed, every push:main prep exits 1 and the rolling PR is not refreshed. NOT MEASURED: what merging the stale rolling PR onto the new main does.
  • Report in the PR body and keep composing is right when a malformed [Unreleased] should not block an otherwise fine fragment-only release, and reviewers reliably read the block. Against it: that PR's prepare commit has already sealed, so the text is deleted the moment it merges. The report would document the loss, not prevent it.
  • Carry the text (for example, fold ### Notes into some section) is right if the toolkit wants to accept free-form notes. It is not done here. C6 has no section for prose before the first heading, and picking a kind for ### Notes would publish text under a heading the author did not choose.
  • No escape hatch. An override that discards the text would bring the defect back on purpose; the fix is an edit to CHANGELOG.md.

A: where the detector lives

  • A classifier shared with MergeSections (taken) is right when the detector must agree with the merge by construction.
  • An output-conservation check (every non-blank [Unreleased] line must appear in the rendered section) is right if drop paths beyond the merge are expected. It is weaker as the runtime check, because a line repeated in a fragment masks a drop. It is used as the test oracle instead: TestUncarriedLines_AgreesWithMergeSections.

B: what the mirror grades

  • The whole ComposeVerify on the sealed CHANGELOG (taken) is right when the mirror must not drift from the authority. The old mirror listed checks 7-9. #931 made check 5 fail-loud, and nothing connected that change to the mirror. Calling the authority's own function means a future check 10 shows up without edits here.
  • Adding check 5 to the section-body mirror would be right if the sealed file were not available at PR-body time, or if checks 1-3 and 6 refused falsely on a real header. The file is available, because prep just wrote it. #725 objected to checks 1-6 because of the synthesised header; this grades the real one. Listing checks again would bring back the drift class that produced B.
  • Ungradable input (version absent, version not the newest, no ## [ heading) renders as REFUSED with the precondition named. This carries #725's vacuity guard forward.

B: the block sentinel

<!-- rt:density-verdict --> is kept although the block is now wider, so anything that greps for it keeps working. Rename it if nothing outside the repo is known to depend on it.

Byte identity

  • Unit pin: TestCompose_acceptedUnreleasedIsByteIdentical pins the composed section, for valid ### Kind blocks and for an empty [Unreleased], to the bytes the pre-change binary sealed for the same input.
  • Binary diff: the old and new rt prep --dry-run produce identical sealed CHANGELOGs for valid blocks (33 lines), an empty [Unreleased] (31), and an H2 after an H3 (35).
  • PR description: it changes for every consumer, because B's block replaces the density block. The CHANGELOG does not change.

Tests and mutation verification (AGENTS.md section 8, Axis B)

The end-to-end tests (cmd/rt, rt prep --dry-run on a temp repo) were written first and were red on unfixed code:

  • A: 5 of 5 shapes exited 0 instead of 1.
  • B: in 2 of 2 routes (manual prose, and two fragments of one kind), the PR body had no REFUSED and no check-5 FAIL.

The control, TestPrep_validUnreleasedStillComposes, is green before and after.

Each mutation was applied on its own to the fixed tree and then reverted:

# Mutation Tests that went red
M1 Compose skips the refusal E2E refusal 5/5, TestCompose_refusesUnreleasedTextTheFoldWouldDrop, TestCompose_refusalWithoutLocationCountsWithinTheBody
M2 orphans (before the first heading / after a bare ### ) not reported TestUncarriedLines 4 cases, _AgreesWithMergeSections 4 cases, E2E prose / H2 / bare-heading
M3 accepted kinds matched case-insensitively wrong-case in TestUncarriedLines, _AgreesWithMergeSections, E2E, TestCompose_refusalWithoutLocation…
M4 shared classifier treats None. as content pre-existing TestMergeSections_noneIsSentinelNotContent, TestTransition_MergeDropsScaffoldedNone, plus TestUncarriedLines/unknown_kind
M5 UnreleasedStartLine off by one TestUnreleasedStartLine, E2E 5/5
M6 mirror renders and fails on checks 7-9 only (the old behaviour) TestMirrorCutGates_EveryCutTimeRefusalIsNamed (checks 1, 2, 3, 5, 6, register), …CleanSectionRendersASayingSoBlock, E2E 2/2
M7 precondition headline reads "clean" TestMirrorCutGates_UngradableInputIsNeverClean 3/3, TestRollingPRBody_GradesTheSealedFileNotTheSection
M7b precondition not counted as failed TestMirrorCutGates_UngradableInputIsNeverClean 3/3
M8 mirror grades the pre-seal CHANGELOG E2E mirror 2/2

Under M6 the check-7 and check-8 cases stay green. That is correct: those are checks the old mirror already covered.

Full suite: go test -count=1 ./... in golang:1.25 on an LF clone gives 29 packages ok and 0 failing. go vet ./... and gofmt -l are clean. Every added line in .go files is ASCII (section 11).

Self-bootstrap (AGENTS.md section 2)

This changes compose machinery (rt prep), so section 2 applies:

  • If the toolkit's wrappers are still @main when this merges: no re-pin is needed. On live main (02fae6a, 2026-10-02) all seven wrappers pin @main. check-self-bootstrap takes its floating-pin exit (#456), and bootstrap-rt builds rt from the workspace commit, so the next push:main prep already runs this code.
  • If they are pinned to a tag or rc by then: run rt repin vX.Y.Z-rc.N after this merges and before the next cut. Otherwise the rolling PR keeps being composed by the old prep.
  • check-self-bootstrap will not go red on this PR either way. Its guarded set (DefaultComposeScripts) is the reusable workflow YAMLs plus scripts/workflow-parse-check.sh, and this PR changes none of them.
  • Adopters pinned to a release tag get both fixes when they move to the release that carries them, because their wrapper fetches that tag's binary.

Adopter impact

For an adopter whose [Unreleased] already holds text in a refused shape, the first prep after upgrading exits 1 and names each line. That is intended: the fix is an edit to CHANGELOG.md on main, and the text is never lost.

NOT CHECKED / out of scope

  • rt recover-pending-cut drops the same shapes. FoldBackIntoUnreleased also merges a non-empty [Unreleased] through MergeSections. A Go probe on this tree folded back New prose after the prepare. plus ### Notes / - A note. together with v0.2.0's ### Added / - Shipped.. The result was only ### Added / - Shipped., with err=nil. Tracked in #1491. (#1487 / #1490 is a different recovery defect: entries shipping twice, not text dropped.)
  • An H2 after an accepted H3 (### Added … ## Fixed …) is carried, not dropped: it lands inside the Added section as a literal ## Fixed line. This was measured byte-identical between the old and new binaries. It is a misfiling, not a loss, so it is not refused here. Tracked in #1492, together with the misleading refusal reason for a ### line inside a code fence (review note 1).
  • The optional PR-time reusable-changelog-body-check.yml grades the same section where an adopter wired it. Not measured: whether a bot-opened rolling PR triggers it on Forgejo. Where it does and the check is required, check 5 was already red on the PR, and the old block contradicted it.
  • Check 3 needs a repo identity. As in compose-verify, the mirror reads it from GITHUB_REPOSITORY / FORGEJO_REPOSITORY. A local prep without them grades every pin as a self-reference. That can produce a false REFUSED, never a false clean.

Changelog

Two fragments, both referencing this PR: changelog.d/1486-prep-unreleased-drop.fixed.md and changelog.d/1486-prep-cut-gate-mirror.fixed.md. Locally, rt fragment-check (content mode) passes them on kind, summary header, issue reference and density.

Cut verification (AGENTS.md section 6)

On the first rolling PR after this merges, verify at source:

  1. The description block lists register-check and checks 1-9, graded against the sealed CHANGELOG.
  2. A malformed [Unreleased] on main makes prep exit 1 and leaves the rolling branch unrefreshed.
  3. The prep step has GITHUB_REPOSITORY or FORGEJO_REPOSITORY in its environment, so check 3 does not grade every pin as a self-reference. Neither is set explicitly in reusable-release.yml. The workflow already relies on the runner-provided GITHUB_REPOSITORY in a run step (reusable-release.yml:967), so it is expected here too, but that is inferred, not measured.

🤖 Generated with Claude Code

## What broke (measured) Measured on 2026-10-02 with `rt prep --dry-run` (one-shot mode) against a throwaway adopter-shaped repo. The binary was built from the v0.63.1 source (`02fae6a`); the original report was on v0.63.0. `compose-verify` was run on the preview applied to the fixture. **A. Silent content loss.** `[Unreleased]` text that is not under an exact `### <Kind>` heading disappears: | `[Unreleased]` shape | v0.63.1 composed section | `rt compose-verify` | |---|---|---| | prose before the first `###` | six `None.` sections | exit 0 | | `### Notes` + bullet | six `None.` sections | exit 0 | | `### added` + bullet | six `None.` sections | exit 0 | | `## Added` + bullet | six `None.` sections | exit 0 | | `### Added`, `- kept`, then a bare `### ` + bullet | the bullet after the bare heading is gone | (not run) | The Cutter's `Seal` then empties `[Unreleased]`, so the text is gone from `main` as well as from the release. Mechanism: `MergeSections` skips lines that have no current kind. It also buckets kinds that are missing from `mergeFoldOrder` but never emits them. **B. The mirror said clean and the cut refused.** Under `### Added`, the section held `- Reworked the importer.` followed by a prose line. The PR description read `Changelog density — clean`, because it graded checks 7-9 only. `rt compose-verify --version 0.2.0` then exited 1 on check 5 (style). ## What changes **A.** `prep.Compose` refuses before the fold, so the seal never runs and nothing is written. Verbatim from the new binary: ``` Error: compose changelog section: [Unreleased] text would be DROPPED from the release: 2 line(s) are not under an accepted heading, and sealing would then delete them from CHANGELOG.md. Nothing was written. CHANGELOG.md:5: ### Notes <- not an accepted heading CHANGELOG.md:7: - Reworked the importer. <- under ### Notes, not an accepted heading Accepted headings (level 3, exact case): ### Added, ### Changed, ### Fixed, ### Removed, ### Deprecated, ### Upgrade, ### Security, ### Internal. Move each line under one of them, or delete it, then re-run rt prep. ``` `changelog.UncarriedLines` reports the lines. It shares `classifyMergeRecord` with `MergeSections`, which is now the only place the merge's line grammar lives. `changelog.UnreleasedStartLine` turns positions within the body into `CHANGELOG.md:<n>`. **B.** The PR-description block is now `gates.MirrorCutGates`. It calls `ComposeVerify`, the function `rt compose-verify` calls at the cut. It runs on the CHANGELOG bytes prep is about to commit (read after the seal and the hooks, before the push), with the same threshold parser and repo-identity resolver. The block lists register-check plus checks 1-9 by ranging over `BodyCheckResult.Checks`. `gates.SectionDensity` is removed. ## Decision tree ### A: refuse, or report in the PR body? - **Refuse in `rt prep`** (the lean taken here) is right when the loss cannot be undone from the release's side and the fix is always an edit on `main`. Both hold. The seal deletes the text from `main`, and every refused shape has a mechanical fix: move the line under an accepted heading, or delete it. Cost: while `[Unreleased]` stays malformed, every push:main prep exits 1 and the rolling PR is not refreshed. NOT MEASURED: what merging the stale rolling PR onto the new `main` does. - **Report in the PR body and keep composing** is right when a malformed `[Unreleased]` should not block an otherwise fine fragment-only release, and reviewers reliably read the block. Against it: that PR's prepare commit has already sealed, so the text is deleted the moment it merges. The report would document the loss, not prevent it. - **Carry the text** (for example, fold `### Notes` into some section) is right if the toolkit wants to accept free-form notes. It is not done here. C6 has no section for prose before the first heading, and picking a kind for `### Notes` would publish text under a heading the author did not choose. - **No escape hatch.** An override that discards the text would bring the defect back on purpose; the fix is an edit to CHANGELOG.md. ### A: where the detector lives - **A classifier shared with `MergeSections`** (taken) is right when the detector must agree with the merge by construction. - **An output-conservation check** (every non-blank `[Unreleased]` line must appear in the rendered section) is right if drop paths beyond the merge are expected. It is weaker as the runtime check, because a line repeated in a fragment masks a drop. It is used as the test oracle instead: `TestUncarriedLines_AgreesWithMergeSections`. ### B: what the mirror grades - **The whole `ComposeVerify` on the sealed CHANGELOG** (taken) is right when the mirror must not drift from the authority. The old mirror listed checks 7-9. #931 made check 5 fail-loud, and nothing connected that change to the mirror. Calling the authority's own function means a future check 10 shows up without edits here. - **Adding check 5 to the section-body mirror** would be right if the sealed file were not available at PR-body time, or if checks 1-3 and 6 refused falsely on a real header. The file is available, because prep just wrote it. #725 objected to checks 1-6 because of the *synthesised* header; this grades the real one. Listing checks again would bring back the drift class that produced B. - **Ungradable input** (version absent, version not the newest, no `## [` heading) renders as REFUSED with the precondition named. This carries #725's vacuity guard forward. ### B: the block sentinel `<!-- rt:density-verdict -->` is kept although the block is now wider, so anything that greps for it keeps working. Rename it if nothing outside the repo is known to depend on it. ## Byte identity - **Unit pin:** `TestCompose_acceptedUnreleasedIsByteIdentical` pins the composed section, for valid `### Kind` blocks and for an empty `[Unreleased]`, to the bytes the pre-change binary sealed for the same input. - **Binary diff:** the old and new `rt prep --dry-run` produce identical sealed CHANGELOGs for valid blocks (33 lines), an empty `[Unreleased]` (31), and an H2 after an H3 (35). - **PR description:** it changes for every consumer, because B's block replaces the density block. The CHANGELOG does not change. ## Tests and mutation verification (AGENTS.md section 8, Axis B) The end-to-end tests (`cmd/rt`, `rt prep --dry-run` on a temp repo) were written first and were red on unfixed code: - **A:** 5 of 5 shapes exited 0 instead of 1. - **B:** in 2 of 2 routes (manual prose, and two fragments of one kind), the PR body had no REFUSED and no check-5 FAIL. The control, `TestPrep_validUnreleasedStillComposes`, is green before and after. Each mutation was applied on its own to the fixed tree and then reverted: | # | Mutation | Tests that went red | |---|---|---| | M1 | `Compose` skips the refusal | E2E refusal 5/5, `TestCompose_refusesUnreleasedTextTheFoldWouldDrop`, `TestCompose_refusalWithoutLocationCountsWithinTheBody` | | M2 | orphans (before the first heading / after a bare `### `) not reported | `TestUncarriedLines` 4 cases, `_AgreesWithMergeSections` 4 cases, E2E prose / H2 / bare-heading | | M3 | accepted kinds matched case-insensitively | wrong-case in `TestUncarriedLines`, `_AgreesWithMergeSections`, E2E, `TestCompose_refusalWithoutLocation…` | | M4 | shared classifier treats `None.` as content | pre-existing `TestMergeSections_noneIsSentinelNotContent`, `TestTransition_MergeDropsScaffoldedNone`, plus `TestUncarriedLines/unknown_kind` | | M5 | `UnreleasedStartLine` off by one | `TestUnreleasedStartLine`, E2E 5/5 | | M6 | mirror renders and fails on checks 7-9 only (the old behaviour) | `TestMirrorCutGates_EveryCutTimeRefusalIsNamed` (checks 1, 2, 3, 5, 6, register), `…CleanSectionRendersASayingSoBlock`, E2E 2/2 | | M7 | precondition headline reads "clean" | `TestMirrorCutGates_UngradableInputIsNeverClean` 3/3, `TestRollingPRBody_GradesTheSealedFileNotTheSection` | | M7b | precondition not counted as failed | `TestMirrorCutGates_UngradableInputIsNeverClean` 3/3 | | M8 | mirror grades the pre-seal CHANGELOG | E2E mirror 2/2 | Under M6 the check-7 and check-8 cases stay green. That is correct: those are checks the old mirror already covered. Full suite: `go test -count=1 ./...` in `golang:1.25` on an LF clone gives 29 packages ok and 0 failing. `go vet ./...` and `gofmt -l` are clean. Every added line in `.go` files is ASCII (section 11). ## Self-bootstrap (AGENTS.md section 2) This changes compose machinery (`rt prep`), so section 2 applies: - **If the toolkit's wrappers are still `@main` when this merges:** no re-pin is needed. On live `main` (`02fae6a`, 2026-10-02) all seven wrappers pin `@main`. `check-self-bootstrap` takes its floating-pin exit (#456), and `bootstrap-rt` builds `rt` from the workspace commit, so the next push:main prep already runs this code. - **If they are pinned to a tag or rc by then:** run `rt repin vX.Y.Z-rc.N` after this merges and before the next cut. Otherwise the rolling PR keeps being composed by the old prep. - **`check-self-bootstrap` will not go red on this PR either way.** Its guarded set (`DefaultComposeScripts`) is the reusable workflow YAMLs plus `scripts/workflow-parse-check.sh`, and this PR changes none of them. - **Adopters pinned to a release tag** get both fixes when they move to the release that carries them, because their wrapper fetches that tag's binary. ## Adopter impact For an adopter whose `[Unreleased]` already holds text in a refused shape, the first prep after upgrading exits 1 and names each line. That is intended: the fix is an edit to CHANGELOG.md on `main`, and the text is never lost. ## NOT CHECKED / out of scope - **`rt recover-pending-cut` drops the same shapes.** `FoldBackIntoUnreleased` also merges a non-empty `[Unreleased]` through `MergeSections`. A Go probe on this tree folded back `New prose after the prepare.` plus `### Notes` / `- A note.` together with v0.2.0's `### Added` / `- Shipped.`. The result was only `### Added` / `- Shipped.`, with `err=nil`. Tracked in #1491. (#1487 / #1490 is a different recovery defect: entries shipping twice, not text dropped.) - **An H2 after an accepted H3** (`### Added` … `## Fixed` …) is carried, not dropped: it lands inside the Added section as a literal `## Fixed` line. This was measured byte-identical between the old and new binaries. It is a misfiling, not a loss, so it is not refused here. Tracked in #1492, together with the misleading refusal reason for a `### ` line inside a code fence (review note 1). - **The optional PR-time `reusable-changelog-body-check.yml`** grades the same section where an adopter wired it. Not measured: whether a bot-opened rolling PR triggers it on Forgejo. Where it does and the check is required, check 5 was already red on the PR, and the old block contradicted it. - **Check 3 needs a repo identity.** As in compose-verify, the mirror reads it from `GITHUB_REPOSITORY` / `FORGEJO_REPOSITORY`. A local prep without them grades every pin as a self-reference. That can produce a false REFUSED, never a false clean. ## Changelog Two fragments, both referencing this PR: `changelog.d/1486-prep-unreleased-drop.fixed.md` and `changelog.d/1486-prep-cut-gate-mirror.fixed.md`. Locally, `rt fragment-check` (content mode) passes them on kind, summary header, issue reference and density. ## Cut verification (AGENTS.md section 6) On the first rolling PR after this merges, verify at source: 1. The description block lists register-check and checks 1-9, graded against the sealed CHANGELOG. 2. A malformed `[Unreleased]` on `main` makes prep exit 1 and leaves the rolling branch unrefreshed. 3. The prep step has `GITHUB_REPOSITORY` or `FORGEJO_REPOSITORY` in its environment, so check 3 does not grade every pin as a self-reference. Neither is set explicitly in `reusable-release.yml`. The workflow already relies on the runner-provided `GITHUB_REPOSITORY` in a run step (`reusable-release.yml:967`), so it is expected here too, but that is inferred, not measured. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(prep): refuse [Unreleased] text the compose would drop; mirror every cut-time gate in the PR body
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
base-divergence-check / check (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Has been cancelled
check-self-bootstrap / check (pull_request) Has been cancelled
changelog-body-check / changelog body Cold-Read linter (pull_request) Has been cancelled
fragment-check / check (pull_request) Has been cancelled
fragment-check / coverage (pull_request) Has been cancelled
fragment-check / density (pull_request) Has been cancelled
gitea-twin-check / check (pull_request) Has been cancelled
go-ci / lint + build + test (pull_request) Has been cancelled
go-ci / page landing-tree failure (pull_request) Has been cancelled
go-ci / record reviewed vs landed commit (pull_request) Has been cancelled
manifest-check / check (pull_request) Has been cancelled
manifest-check / manifest-vs-tag consistency (pull_request) Has been cancelled
prep-order-check / check (pull_request) Has been cancelled
readme-pin-check / mirror (pull_request) Has been cancelled
readme-pin-check / digest (pull_request) Has been cancelled
register-check / check (pull_request) Has been cancelled
register-check / register-drift check (pull_request) Has been cancelled
tests / workflow-schema (pull_request) Has been cancelled
tests / bats (pull_request) Has been cancelled
tests / dated-examples (pull_request) Has been cancelled
tests / contract-paths (pull_request) Has been cancelled
tests / shellcheck (pull_request) Has been cancelled
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Has been cancelled
workflow-parse-check / check (pull_request) Has been cancelled
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Has been cancelled
workflow-parse-check / workflow parse and schema (pull_request) Has been cancelled
31677cc1fa
rt prep silently dropped [Unreleased] text outside an exact `### <Kind>`
heading (prose before the first heading, `### Notes`, `### added`,
`## Added`, text after a bare `### `), and the seal then deleted it from
CHANGELOG.md with every gate green. Compose now refuses before anything is
written, naming each line as <file>:<line> with the accepted headings.
UncarriedLines shares MergeSections' line classifier, so the two cannot
disagree. Accepted shapes compose byte-identically.

The rolling-PR description graded checks 7-9 only, so a section that
compose-verify refuses on check 5 read "clean" until the cut failed after
the merge. The block now runs ComposeVerify itself on the sealed CHANGELOG
prep commits and lists the register check and checks 1-9; ungradable input
reads REFUSED, never clean. gates.SectionDensity is removed.

Compose machinery change, AGENTS.md section 2: the toolkit's wrappers pin
@main on live main, so no re-pin is needed while that holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore(changelog): reference #1486 in the prep fragments
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
base-divergence-check / check (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 6s
go-ci / lint + build + test (pull_request) Failing after 11s
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 6s
readme-pin-check / mirror (pull_request) Successful in 7s
readme-pin-check / digest (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 5s
tests / bats (pull_request) Successful in 32s
tests / dated-examples (pull_request) Successful in 5s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
e2fc222033
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude left a comment

Review of head e2fc22203343b24e9cea692b6fc0832e92afc22d. No blocking findings.

Posted as COMMENT, not APPROVED. This review was written through the claude account, which is also the PR's author. Forgejo refuses self-approval across the whole instance (AGENTS.md section 8). A second reviewer still needs to approve.

What I verified

  • Full suite. Ran on a fresh LF clone in golang:1.25: go vet ./... is clean, gofmt -l cmd internal is empty, and go test -count=1 ./... passes 29 of 29 packages. The base 02fae6a is green on the four touched packages.

  • Mutation check (Axis B). I re-ran two of the PR's claimed mutations myself. Both went red as claimed:

    • M1 (Compose skips the refusal) fails TestPrep_refusesUnreleasedContentItWouldDrop, TestCompose_refusesUnreleasedTextTheFoldWouldDrop and TestCompose_refusalWithoutLocationCountsWithinTheBody.
    • M8 (the mirror reads the pre-seal clBytes) fails TestPrep_rollingPRBodyMirrorsTheStyleCheck.
  • The refusal does not over-refuse. I ran these [Unreleased] bodies through Compose:

    Input Result
    ### Security + bullet composes; the bullet is carried
    ### Internal + bullet composes; the bullet is carried
    ### Upgrade + bullet composes; the bullet is carried
    ### Added, then ## Fixed + bullet composes; the H2 lands inside Added, as the PR body says

    So all eight headings the error message calls accepted really do survive sectionsFromMarkdown and the parser.

  • The mirror uses the same inputs as rt compose-verify. Both call gates.ComposeVerify with parseBodyCheckThresholds() and resolveThisRepo(). The mirror then re-runs ChangelogBodyCheck and RegisterCheckStdin(sectionContentByVersion(...)) for the per-check detail. The sealed bytes are read after the seal, the hooks, the build-bake and the doc-ref steps, and before the commit and push, so the block grades the file the PR will carry.

  • Sentinel. Nothing else in the tree (Go, YAML, shell or docs) greps for rt:density-verdict. The rename suggested in the PR body is safe from this repo's side whenever you want it.

Non-blocking notes

  1. A ### line inside a fenced block under [Unreleased] is refused with <- not an accepted heading. For example, ### Added followed by a fenced block containing ### not a heading. Refusing is correct: the merge would treat that line as a heading and drop it and the rest of the fence. But the author put that line in on purpose as literal text, so the reason may confuse them. A hint about fenced code in the reason, or in the conventions.md paragraph, would help. Fine as a follow-up.
  2. An H2 after an accepted H3 is misfiled, not lost (## Fixed ends up as body text under Added). The PR body already measures and scopes this out. It is worth an issue, because nothing grades it: check 6 only looks at ### duplicates.
  3. recover-pending-cut has the same drop class through FoldBackIntoUnreleased. The PR body says a follow-up is in progress. Please link it here before merging so the gap is tracked.
  4. Operational cost while [Unreleased] is malformed. Every push:main prep exits 1 and the rolling PR goes stale. This is documented, and the decision tree's reasoning for refusing is sound: the seal deletes the text from main, so reporting it in the PR body would only document the loss. Your own "NOT MEASURED" item is the one to watch at the first cut: what happens if the stale rolling PR is merged.

Cut verification (section 6)

The two at-source checks listed in the PR body are the right ones. I'd add one more: confirm the reusable-release prep step has GITHUB_REPOSITORY or FORGEJO_REPOSITORY in its environment. Without it, check 3 grades every pin as a self-reference and the block would show a false REFUSED.

🤖 Generated with Claude Code

Review of head `e2fc22203343b24e9cea692b6fc0832e92afc22d`. No blocking findings. **Posted as COMMENT, not APPROVED.** This review was written through the `claude` account, which is also the PR's author. Forgejo refuses self-approval across the whole instance (AGENTS.md section 8). A second reviewer still needs to approve. ## What I verified - **Full suite.** Ran on a fresh LF clone in `golang:1.25`: `go vet ./...` is clean, `gofmt -l cmd internal` is empty, and `go test -count=1 ./...` passes 29 of 29 packages. The base `02fae6a` is green on the four touched packages. - **Mutation check (Axis B).** I re-ran two of the PR's claimed mutations myself. Both went red as claimed: - **M1** (Compose skips the refusal) fails `TestPrep_refusesUnreleasedContentItWouldDrop`, `TestCompose_refusesUnreleasedTextTheFoldWouldDrop` and `TestCompose_refusalWithoutLocationCountsWithinTheBody`. - **M8** (the mirror reads the pre-seal `clBytes`) fails `TestPrep_rollingPRBodyMirrorsTheStyleCheck`. - **The refusal does not over-refuse.** I ran these `[Unreleased]` bodies through `Compose`: | Input | Result | |---|---| | `### Security` + bullet | composes; the bullet is carried | | `### Internal` + bullet | composes; the bullet is carried | | `### Upgrade` + bullet | composes; the bullet is carried | | `### Added`, then `## Fixed` + bullet | composes; the H2 lands inside Added, as the PR body says | So all eight headings the error message calls accepted really do survive `sectionsFromMarkdown` and the parser. - **The mirror uses the same inputs as `rt compose-verify`.** Both call `gates.ComposeVerify` with `parseBodyCheckThresholds()` and `resolveThisRepo()`. The mirror then re-runs `ChangelogBodyCheck` and `RegisterCheckStdin(sectionContentByVersion(...))` for the per-check detail. The sealed bytes are read after the seal, the hooks, the build-bake and the doc-ref steps, and before the commit and push, so the block grades the file the PR will carry. - **Sentinel.** Nothing else in the tree (Go, YAML, shell or docs) greps for `rt:density-verdict`. The rename suggested in the PR body is safe from this repo's side whenever you want it. ## Non-blocking notes 1. **A `### ` line inside a fenced block under `[Unreleased]` is refused** with `<- not an accepted heading`. For example, `### Added` followed by a fenced block containing `### not a heading`. Refusing is correct: the merge would treat that line as a heading and drop it and the rest of the fence. But the author put that line in on purpose as literal text, so the reason may confuse them. A hint about fenced code in the reason, or in the `conventions.md` paragraph, would help. Fine as a follow-up. 2. **An H2 after an accepted H3 is misfiled, not lost** (`## Fixed` ends up as body text under Added). The PR body already measures and scopes this out. It is worth an issue, because nothing grades it: check 6 only looks at `###` duplicates. 3. **`recover-pending-cut` has the same drop class** through `FoldBackIntoUnreleased`. The PR body says a follow-up is in progress. Please link it here before merging so the gap is tracked. 4. **Operational cost while `[Unreleased]` is malformed.** Every push:main prep exits 1 and the rolling PR goes stale. This is documented, and the decision tree's reasoning for refusing is sound: the seal deletes the text from `main`, so reporting it in the PR body would only document the loss. Your own "NOT MEASURED" item is the one to watch at the first cut: what happens if the stale rolling PR is merged. ## Cut verification (section 6) The two at-source checks listed in the PR body are the right ones. I'd add one more: confirm the reusable-release prep step has `GITHUB_REPOSITORY` or `FORGEJO_REPOSITORY` in its environment. Without it, check 3 grades every pin as a self-reference and the block would show a false REFUSED. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
alex approved these changes 2026-10-02 10:04:04 +02:00
Dismissed
Author
Member

Thanks for the review. Disposition of each note, all without new commits. The head is still e2fc22203343b24e9cea692b6fc0832e92afc22d; only the PR description changed (body-only, AGENTS.md section 6.2).

  1. Fenced ### line refused with a misleading reason: tracked in #1492 as an acceptance criterion: say so in the reason, and in docs/conventions.md. I didn't fix it here, because the refusal itself is correct and a code change would re-run the whole gate set for a wording improvement.
  2. H2 after an accepted H3 is misfiled and nothing grades it: filed as #1492, with the measured before/after (byte-identical between v0.63.1 and this PR).
  3. recover-pending-cut drop class: filed as #1491, with the probe output. Correction to my own body: the in-progress follow-up I referred to turned out to be #1487 / #1490, which is a different recovery defect (entries shipping twice, not text dropped). The drop was untracked until #1491. The description now links #1491 and says this.
  4. Operational cost while [Unreleased] is malformed: no change. The stale-rolling-PR merge stays the NOT MEASURED item to watch at the first cut.

Cut verification: added your third check (GITHUB_REPOSITORY / FORGEJO_REPOSITORY in the prep step's environment) to the description. Neither variable is set explicitly in reusable-release.yml. reusable-release.yml:967 already relies on the runner-provided GITHUB_REPOSITORY in a run step, so it is expected in the prep step too. That is inferred, not measured, and the description says so.

🤖 Generated with Claude Code

Thanks for the review. Disposition of each note, all without new commits. The head is still `e2fc22203343b24e9cea692b6fc0832e92afc22d`; only the PR description changed (body-only, AGENTS.md section 6.2). 1. **Fenced `### ` line refused with a misleading reason:** tracked in #1492 as an acceptance criterion: say so in the reason, and in `docs/conventions.md`. I didn't fix it here, because the refusal itself is correct and a code change would re-run the whole gate set for a wording improvement. 2. **H2 after an accepted H3 is misfiled and nothing grades it:** filed as #1492, with the measured before/after (byte-identical between v0.63.1 and this PR). 3. **`recover-pending-cut` drop class:** filed as #1491, with the probe output. Correction to my own body: the in-progress follow-up I referred to turned out to be #1487 / #1490, which is a *different* recovery defect (entries shipping twice, not text dropped). The drop was untracked until #1491. The description now links #1491 and says this. 4. **Operational cost while `[Unreleased]` is malformed:** no change. The stale-rolling-PR merge stays the NOT MEASURED item to watch at the first cut. **Cut verification:** added your third check (`GITHUB_REPOSITORY` / `FORGEJO_REPOSITORY` in the prep step's environment) to the description. Neither variable is set explicitly in `reusable-release.yml`. `reusable-release.yml:967` already relies on the runner-provided `GITHUB_REPOSITORY` in a run step, so it is expected in the prep step too. That is inferred, not measured, and the description says so. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Member

HOLD: a new commit is coming; please do not merge on e2fc22203343b24e9cea692b6fc0832e92afc22d.

The required go-ci / lint + build + test check is red on this head. CI was not fully green. The job log is not anonymously readable, so I reproduced the job's lint step: golangci-lint run --timeout=5m --max-issues-per-linter=0 --max-same-issues=0 (v2.14.0, no config in the repo, so default linters):

  • base 02fae6a: 0 issues.
  • head e2fc222: internal/changelog/merge.go:139:2: QF1002: could use tagged switch on u.Heading (staticcheck)

That is UncarriedLine.Reason in this PR. The fix is behaviour-neutral: a tagged switch on u.Heading. It lands as a new commit, with no amend or force-push. I'll post the new head SHA when it is pushed.

🤖 Generated with Claude Code

**HOLD:** a new commit is coming; please do not merge on `e2fc22203343b24e9cea692b6fc0832e92afc22d`. The required `go-ci / lint + build + test` check is **red** on this head. CI was not fully green. The job log is not anonymously readable, so I reproduced the job's lint step: `golangci-lint run --timeout=5m --max-issues-per-linter=0 --max-same-issues=0` (v2.14.0, no config in the repo, so default linters): - base `02fae6a`: `0 issues.` - head `e2fc222`: `internal/changelog/merge.go:139:2: QF1002: could use tagged switch on u.Heading (staticcheck)` That is `UncarriedLine.Reason` in this PR. The fix is behaviour-neutral: a tagged switch on `u.Heading`. It lands as a new commit, with no amend or force-push. I'll post the new head SHA when it is pushed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chore(changelog): use a tagged switch in UncarriedLine.Reason
Some checks failed
readme-pin-check / mirror (pull_request) Successful in 7s
readme-pin-check / digest (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 32s
tests / dated-examples (pull_request) Successful in 5s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 5s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
check-self-bootstrap / check (push) Successful in 5s
gitea-twin-check / check (push) Successful in 5s
go-ci / lint + build + test (push) Successful in 29s
go-ci / record reviewed vs landed commit (push) Failing after 3s
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Successful in 5s
prepared-uncut-check / prepared-but-uncut release (push) Successful in 7s
prepared-uncut-check / check (push) Successful in 0s
release / decide + act (push) Successful in 9s
release / release (push) Successful in 0s
tests / workflow-schema (push) Successful in 3s
tests / bats (push) Successful in 31s
tests / dated-examples (push) Successful in 5s
tests / contract-paths (push) Successful in 4s
tests / shellcheck (push) Successful in 3s
go-ci / page landing-tree failure (push) Has been skipped
release / fire-cut (push) Has been skipped
2e8f46fc8b
golangci-lint's staticcheck QF1002 flagged the untagged switch and turned
go-ci's lint step red on #1486. Behaviour is unchanged; TestUncarriedLine_Reason
covers every branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude dismissed alex's review 2026-10-02 10:11:58 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

Author
Member

HOLD released. New head: 2e8f46fc8b977a92021673731af30dfa643342bd

The new commit is 2e8f46f (chore(changelog): use a tagged switch in UncarriedLine.Reason). It is a single-file, 3-line change in internal/changelog/merge.go: switch { case u.Heading == "" … case u.Heading == u.Text … } becomes switch u.Heading { case "" … case u.Text … }. Behaviour is unchanged, and TestUncarriedLine_Reason covers every branch.

Verified locally on this head:

  • golangci-lint run --timeout=5m --max-issues-per-linter=0 --max-same-issues=0 (v2.14.0): 0 issues.
  • go build ./... and go test -count=1 ./... (golang:1.25, LF clone): 29 packages ok, 0 failing.

The push dismissed the earlier approval on e2fc222, so this head needs a fresh one. Before submitting it, pin it to the head you read: rt review-submit-check --owner frankenbit --repo release-toolkit --pr 1486 --reviewed-head 2e8f46fc8b977a92021673731af30dfa643342bd (AGENTS.md section 6.2).

🤖 Generated with Claude Code

**HOLD released.** New head: `2e8f46fc8b977a92021673731af30dfa643342bd` The new commit is `2e8f46f` (chore(changelog): use a tagged switch in UncarriedLine.Reason). It is a single-file, 3-line change in `internal/changelog/merge.go`: `switch { case u.Heading == "" … case u.Heading == u.Text … }` becomes `switch u.Heading { case "" … case u.Text … }`. Behaviour is unchanged, and `TestUncarriedLine_Reason` covers every branch. Verified locally on this head: - `golangci-lint run --timeout=5m --max-issues-per-linter=0 --max-same-issues=0` (v2.14.0): `0 issues.` - `go build ./...` and `go test -count=1 ./...` (golang:1.25, LF clone): 29 packages ok, 0 failing. The push dismissed the earlier approval on `e2fc222`, so this head needs a fresh one. Before submitting it, pin it to the head you read: `rt review-submit-check --owner frankenbit --repo release-toolkit --pr 1486 --reviewed-head 2e8f46fc8b977a92021673731af30dfa643342bd` (AGENTS.md section 6.2). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude left a comment

Follow-up review of head 2e8f46fc8b977a92021673731af30dfa643342bd, the delta from e2fc222. No findings.

Posted as COMMENT, not APPROVED, for the same reason as before: this account (claude) is the PR's author, and Forgejo refuses self-approval. The approval on e2fc222 was dismissed when this commit was pushed, so this head needs a fresh one from a maintainer. Before submitting it, run rt review-submit-check ... --reviewed-head 2e8f46fc8b977a92021673731af30dfa643342bd (section 6.2).

The new commit 2e8f46f

The commit is a 3-line change in internal/changelog/merge.go. UncarriedLine.Reason switches from switch { case u.Heading == "" ...; case u.Heading == u.Text ... } to switch u.Heading { case "": ...; case u.Text: ... }.

  • No behaviour change. Go evaluates case expressions top to bottom and takes the first match. The branch order is the same, including the Heading == "" && Text == "" overlap, where the first case still wins. TestUncarriedLine_Reason covers all three branches.

  • Lint, reproduced independently with the official golangci/golangci-lint:v2.14.0 image on a fresh clone, using the PR's flags (--max-issues-per-linter=0 --max-same-issues=0, no repo config):

    Commit Result
    base 02fae6a 0 issues.
    old head e2fc222 1 issue (staticcheck)
    new head 2e8f46f 0 issues.

    This confirms the HOLD comment's diagnosis: the red go-ci check on e2fc222 came from this PR, and this commit clears it.

  • Suite on 2e8f46f (golang:1.25, LF clone): go vet clean, gofmt -l empty, go test -count=1 ./... passes 29/29 packages.

  • CI. The combined status on 2e8f46f is success, with 30 of 30 contexts green, including go-ci / lint + build + test. I re-ran lint and tests locally above instead of relying only on that report (section 8, Axis C).

Dispositions of my earlier notes

I checked each one at its source:

  1. Fenced ### reason, and 2. H2 after H3: #1492 exists (kind/bug, priority/low). Its acceptance criteria cover both cases, require the byte-identity pin, and require mutation verification. Deferring is fine; neither case loses text.
  2. recover-pending-cut drop class: #1491 exists, with the probe output and a criterion to check the second merge argument. That criterion is the right catch: UncarriedLines is only exact for the first argument. The corrected description now links #1491 and separates it from #1487 / #1490.
  3. Stale rolling PR: unchanged, and still recorded as NOT MEASURED in the description. That is acceptable.

Cut verification: item 3 was added to the description and labelled as inferred, not measured. The cited line reusable-release.yml:967 (OWNER="${GITHUB_REPOSITORY%%/*}") is real at this head, and it relies on the runner-provided variable as described.

The process follows section 6.2: the body-only correction was announced, then a HOLD before the new commit, then the HOLD was released with the new SHA. The commit was a new commit, not an amend or force-push.

Ready to merge once a maintainer approves 2e8f46f.

🤖 Generated with Claude Code

Follow-up review of head `2e8f46fc8b977a92021673731af30dfa643342bd`, the delta from `e2fc222`. No findings. **Posted as COMMENT, not APPROVED,** for the same reason as before: this account (`claude`) is the PR's author, and Forgejo refuses self-approval. The approval on `e2fc222` was dismissed when this commit was pushed, so this head needs a fresh one from a maintainer. Before submitting it, run `rt review-submit-check ... --reviewed-head 2e8f46fc8b977a92021673731af30dfa643342bd` (section 6.2). ## The new commit `2e8f46f` The commit is a 3-line change in `internal/changelog/merge.go`. `UncarriedLine.Reason` switches from `switch { case u.Heading == "" ...; case u.Heading == u.Text ... }` to `switch u.Heading { case "": ...; case u.Text: ... }`. - **No behaviour change.** Go evaluates case expressions top to bottom and takes the first match. The branch order is the same, including the `Heading == "" && Text == ""` overlap, where the first case still wins. `TestUncarriedLine_Reason` covers all three branches. - **Lint, reproduced independently** with the official `golangci/golangci-lint:v2.14.0` image on a fresh clone, using the PR's flags (`--max-issues-per-linter=0 --max-same-issues=0`, no repo config): | Commit | Result | |---|---| | base `02fae6a` | `0 issues.` | | old head `e2fc222` | 1 issue (staticcheck) | | new head `2e8f46f` | `0 issues.` | This confirms the HOLD comment's diagnosis: the red `go-ci` check on `e2fc222` came from this PR, and this commit clears it. - **Suite on `2e8f46f`** (`golang:1.25`, LF clone): `go vet` clean, `gofmt -l` empty, `go test -count=1 ./...` passes 29/29 packages. - **CI.** The combined status on `2e8f46f` is `success`, with 30 of 30 contexts green, including `go-ci / lint + build + test`. I re-ran lint and tests locally above instead of relying only on that report (section 8, Axis C). ## Dispositions of my earlier notes I checked each one at its source: 1. **Fenced `### ` reason, and 2. H2 after H3:** #1492 exists (`kind/bug`, `priority/low`). Its acceptance criteria cover both cases, require the byte-identity pin, and require mutation verification. Deferring is fine; neither case loses text. 3. **`recover-pending-cut` drop class:** #1491 exists, with the probe output and a criterion to check the second merge argument. That criterion is the right catch: `UncarriedLines` is only exact for the first argument. The corrected description now links #1491 and separates it from #1487 / #1490. 4. **Stale rolling PR:** unchanged, and still recorded as NOT MEASURED in the description. That is acceptable. **Cut verification:** item 3 was added to the description and labelled as inferred, not measured. The cited line `reusable-release.yml:967` (`OWNER="${GITHUB_REPOSITORY%%/*}"`) is real at this head, and it relies on the runner-provided variable as described. The process follows section 6.2: the body-only correction was announced, then a HOLD before the new commit, then the HOLD was released with the new SHA. The commit was a new commit, not an amend or force-push. Ready to merge once a maintainer approves `2e8f46f`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
alex approved these changes 2026-10-02 10:21:28 +02:00
alex merged commit 2e8f46fc8b into main 2026-10-02 10:21:38 +02:00
alex deleted branch fix/prep-unreleased-drop-cut-gate-mirror 2026-10-02 10:21:38 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frankenbit/release-toolkit!1486
No description provided.