Skip to content

ci(release): rehearsal preview tag, workflow-revision tooling for legacy tags, and a semver changelog base - #2687

Merged
Chris0Jeky merged 7 commits into
mainfrom
issue-2250/release-rehearsal-and-changelog-base
Sep 6, 2026
Merged

ci(release): rehearsal preview tag, workflow-revision tooling for legacy tags, and a semver changelog base#2687
Chris0Jeky merged 7 commits into
mainfrom
issue-2250/release-rehearsal-and-changelog-base

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

Issue #2250 items 1 to 3, all in the Release Desktop dispatch path. Items 4 and 5 are PR #2684 and are not touched here.

Item 1, rehearsal preview tag. A new workflow_dispatch input preview_tag (string, optional, empty by default). On a no-publish dispatch it makes resolve-source emit two extra outputs, render_tag and render_prerelease, and compose-notes renders the page for that tag instead of the generated v0.0.0-dryrun+<sha7>. The input is untrusted dispatch text, so it reaches Bash only through the step env var RAW_PREVIEW_TAG and clears scripts/ci/validate-release-tag.sh before any use, exactly like inputs.tag. render_prerelease is derived from render_tag by the same case statement that derives prerelease from the real tag.

The resolved tag, the publish decision, the stamped product version, the archive name, the provenance asset and every gh release call keep using the resolved tag. create-release is still gated on publish == 'true', which a preview tag never sets, and a contract test asserts that build-frontend, build-backend and create-release contain no reference to the render or preview values at all. When the dispatch does publish, the input is ignored with a ::notice:: rather than rejected with an error: this dispatch publishes a real tag, and failing a build over an input that changes nothing about what gets published costs the build for no safety gain.

The rendered page keeps the archive name and checksum of the archive that was actually built. Under a preview, the download link therefore reads releases/download/v0.3.0/taskdeck-v0.0.0-dryrun+abc1234-win-x64.zip. That is deliberate: the SHA-256 printed on the page must belong to the file the page names, and a rehearsal link is inert because no release exists. What the preview does prove is the part the issue asked for, the stable fail-closed policy, the real UPGRADING.md section and the real curated notes file.

Item 2, legacy-tag re-dispatch. Option (a) was chosen. compose-notes now takes a second actions/checkout of the workflow revision (ref: ${{ github.workflow_sha }}, path: .workflow-tooling, persist-credentials: false) and runs the composer and the new selector from that path. Reasoning: the checksum chain stays intact. The ZIP and its .sha256 are still produced by build-backend from the tagged tree and verified against it by create-release; only the page renderer moves. Every path the renderer READS is still relative to the tagged checkout, so no content from a later revision can be published under an older tag. Option (b) would have documented v0.2.0 as unpublishable while leaving a working fix on the table.

Residual, stated plainly: option (a) removes the MODULE_NOT_FOUND failure, not every pre-0.3 failure. A stable tag whose tree has no docs/releases/notes/<tag>.md still fails, because the composer refuses to publish a stable page without curated highlights. It now fails at compose time with a named missing input instead of an opaque module error. A pre-0.3 STABLE tag is therefore still not re-publishable end to end without a decision about where its highlights come from, and this PR does not make one.

Item 3, changelog base. gh release list --exclude-pre-releases --exclude-drafts --limit 1 returned the newest stable release by RELEASE DATE. The call is now --limit 100, given the same bounded 3-attempt retry with 10/20 second backoff as generate-notes (it was previously a single unretried call under set -e), and the base is chosen by a new dependency-free script, scripts/ci/select-changelog-base.mjs, which picks the newest candidate that sorts STRICTLY BEFORE the target tag by semver precedence. Its grammar mirrors scripts/ci/validate-release-tag.sh. Candidates outside that grammar are skipped with a warning, an unparseable target is refused. The first-release path is unchanged: no candidate means no previous_tag_name, logged, not silent. The existing previous_tag != RELEASE_TAG guard is kept as redundant defence in depth even though strict ordering already excludes it.

sort -V was not used. Precedence here has to place v0.3.0-rc.1 before v0.3.0, and the ordering is worth unit-testing directly.

Root cause

Item 1: resolve-source had exactly one tag, so the rehearsal name v0.0.0-dryrun+<sha7> was also the render tag. That name carries a prerelease segment, so the composer always took the RC fallback and the uploaded composed-page-body was structurally a different page from the one a stable tag would render.

Item 2: #1795 pins every checkout to the tagged commit so the published bytes provably come from that tag. The composer was invoked as node scripts/ci/compose-release-notes.mjs, a path resolved inside that tagged checkout, which conflated release CONTENT (correctly tag-pinned) with workflow TOOLING (which has no reason to be).

Item 3: gh release list orders by release date and --limit 1 took the first row, so "the newest stable release" and "the stable release preceding this tag" were treated as the same thing. They diverge the moment a stable tag is published before one of its own release candidates is re-run.

Verification

Red first, then green. Every command run from the worktree root.

Item 1 and 2 contract tests plus the rewritten item 3 test, against the unchanged workflow:

node --test --test-reporter=tap scripts/ci/release-desktop-dispatch.test.mjs
# tests 86
# pass 71
# fail 15

The 15 failures were: every checkout refuses to persist Git credentials; a dedicated job composes the page body and runs on the rehearsal path too; the composer receives the render tag, prerelease decision and repo through env; the composer is fed the checksum, UPGRADING.md and the curated notes for the tag; the changelog base is the newest stable release before the target tag, with bounded retries; the rehearsal preview tag reaches Bash only through a step env var; the preview tag input is declared rehearsal-only and never required; the preview tag clears the same grammar gate as a real release tag; render_tag falls back to the resolved tag and is ignored on a publishing dispatch; resolve-source publishes the render tag and its prerelease decision as outputs; no publishing step reads the preview tag or the render tag; the composer renders the preview tag while the asset stays the built one; workflow tooling is checked out from the workflow revision into its own path; the tooling checkout is proved to carry the composer before the build is spent; release content is still read from the tagged checkout, not the tooling one.

Sample assertion text from that run:

AssertionError [ERR_ASSERTION]: the curated notes are the tagged tree, unprefixed
  expected: /--notes "docs\/releases\/notes\/\$\{RENDER_TAG\}\.md"/
AssertionError [ERR_ASSERTION]: a missing composer is named explicitly rather than surfacing as MODULE_NOT_FOUND
  expected: /if \[ ! -f \.workflow-tooling\/scripts\/ci\/compose-release-notes\.mjs \]/
AssertionError [ERR_ASSERTION]: inputs.preview_tag must be bound to an env var, not spliced into a run block
  expected: /^\s+RAW_PREVIEW_TAG: \$\{\{ inputs\.preview_tag \}\}$/m

Selector suite, before the script existed:

node --test --test-reporter=tap scripts/ci/select-changelog-base.test.mjs
# Error [ERR_MODULE_NOT_FOUND]: Cannot find module '...\scripts\ci\select-changelog-base.mjs'
# tests 1
# pass 0
# fail 1

After writing the script but before wiring the suite into ci-required.yml, 17 tests, 16 pass, 1 fail, the failure being ci-required runs this suite beside the release workflow contract.

Green, after the changes:

node --test --test-reporter=tap scripts/ci/release-desktop-dispatch.test.mjs   # tests 86, pass 86, fail 0
node --test --test-reporter=tap scripts/ci/select-changelog-base.test.mjs      # tests 17, pass 17, fail 0
node --test --test-reporter=tap scripts/ci/smart-ci/*.test.mjs                 # tests 91, pass 91, fail 0
node --test --test-reporter=tap scripts/ci/release-cache-contract.test.mjs     # tests 28, pass 28, fail 0
node scripts/check-github-ops-governance.mjs   # GitHub operations governance check passed.
node scripts/check-docs-governance.mjs         # Docs governance check passed.
git diff --check                               # clean, exit 0

Structural checks on the workflow itself:

  • YAML parse with PyYAML: parses, jobs resolve-source, build-frontend, build-backend, compose-notes, create-release, dispatch inputs tag, preview_tag.
  • Every shell: bash run: block extracted and syntax-checked: bash -n checked 25 run blocks, 0 failures.

Behavioural rehearsal of the changed render step, run against the real composer and the real selector from a .workflow-tooling-shaped path, with a synthetic tagged tree (an UPGRADING.md carrying a ## v0.3.0 section, a docs/releases/notes/v0.3.0.md, and a checksum file for the dry-run archive):

  • selector, target v0.3.0-rc.1, candidate list v0.3.0, v0.2.0, v0.1.1 in date order: printed v0.2.0. This is the item 3 regression; the old call would have returned v0.3.0.
  • render with RENDER_TAG=v0.3.0, RENDER_PRERELEASE=false: rendered 1004 bytes, the Breaking changes body from the UPGRADING section is present, the quick start link is pinned to v0.3.0, and the file name and SHA-256 shown are the built dry-run archive's.
  • the same render without a preview tag (v0.0.0-dryrun+abc1234, prerelease): warns UPGRADING.md has no "## v0.0.0-dryrun+abc1234" section and docs/releases/notes/v0.0.0-dryrun+abc1234.md is missing or empty, and the UPGRADING body is absent. That contrast is exactly what item 1 exists to remove.

Not verified

  • Workflow Lint (actionlint plus ShellCheck plus Pyflakes) was NOT run locally. Neither actionlint nor shellcheck is installed on this machine; ci-extended.yml installs a pinned actionlint 1.7.12 by checksum at run time. bash -n over the extracted run blocks is not a substitute for ShellCheck. The specific thing to watch on the hosted run is github.workflow_sha, which is a documented GitHub context property but which I could not confirm actionlint 1.7.12 accepts. CI-control changes are hosted-only proving under .claude/rules/ci-control.md, so the hosted lint on this PR head is the real gate.
  • scripts/ci/actionlint-bootstrap.test.mjs fails locally, 7 tests, 3 pass, 4 fail, all with ACTIONLINT_BIN must point to the installed tool. That is pre-existing and environmental; the file is untouched by this PR.
  • No live release dispatch was run. A workflow_dispatch cannot be rehearsed from a PR, so every workflow claim above rests on the source-text contract suite plus the simulation, not on a real run. A rehearsal dispatch with preview_tag set, and a re-dispatch of a pre-0.3 tag, are both maintainer actions.
  • The --limit 100 bound was not exercised against a repository with more than 100 stable releases.
  • No backend, frontend or E2E checks were run; nothing in those regions is touched.

Risk notes

  • preview_tag is the one input a maintainer needs to understand. It renders and never publishes. It is refused unless it clears the release-tag grammar, a dispatch that supplies it and publishes is refused outright in resolve-source before any build starts (round 1 below), and create-release remains gated on publish, which the input cannot set. The contract suite asserts that no publishing job references it or the render outputs. The practical consequence to know: on a rehearsal the page will show the preview tag in its headings and links while naming the dry-run archive that was actually built, so the download link on a preview is intentionally not a working URL.
  • The second checkout adds one more place credentials could be persisted. It sets persist-credentials: false, and the existing contract test now counts six checkouts rather than five and asserts the flag on every one.
  • github.workflow_sha is the commit of the workflow file that is running, so the tooling cannot come from a ref the dispatcher did not select. If GitHub ever stopped populating it the checkout would fail rather than silently take a default ref, and the explicit guard step names the missing composer.
  • Moving the composer out of the tagged checkout means a future fix to the renderer applies to a re-dispatch of an older tag. That is the intended behaviour and the reason the change is scoped to the renderer only, never to release content.
  • select-changelog-base.mjs fails closed on an unparseable target and skips unparseable candidates. A skip is a warning rather than an error so that an unrelated tag in the repository cannot fail a publish; the cost is that a stable release named outside the grammar would be invisible as a base.
  • stable-tags.txt is written into the workspace of the tagged checkout. It is never uploaded and never reaches release-assets/.
  • No existing contract test was weakened. Three were changed and each was tightened: the checkout count went from five to six with the reason named, the composer path assertion now pins the tooling path, and the changelog base test now asserts semver selection, a bounded-but-wide listing, two retry loops and a fail-closed listing, and explicitly refuses the old --limit 1 shape.

Files

  • .github/workflows/release-desktop.yml — the preview_tag input, the render outputs, the tooling checkout and its guard, the semver changelog base, and a header comment recording all three decisions.
  • scripts/ci/release-desktop-dispatch.test.mjs — 86 tests, up from 76.
  • scripts/ci/select-changelog-base.mjs — new, dependency-free.
  • scripts/ci/select-changelog-base.test.mjs — new, 17 tests.
  • .github/workflows/ci-required.yml — one step running the new suite beside the dispatch contract.

Review round 1 (fix round)

Head 6b591eb carries the fixes for the round-1 review of 8a98c96. Five findings, five fixed, none declined:

  1. MEDIUM — a preview_tag on a PUBLISHING dispatch was ignored with a ::notice, justified by a comment claiming a refusal "would cost a build for no safety gain". Factually wrong: resolve-source is the first job, builds nothing, and both build jobs need it, so exit 1 there costs zero build minutes — and the refusal is the only mechanical signal that the dispatcher's intent (preview) and action (publish) disagree. Now a ::error naming both inputs, then exit 1; the comment, the input description and the header comment say so, and the contract test pins the refusal plus the fact that resolve-source is needs:-free with every build job downstream of it.
  2. LOW — the test named "the tooling checkout is proved to carry the composer before the build is spent" asserted a property the workflow does not have (compose-notes needs build-backend), and matched /exit 1/ against the whole job block, which carries several unrelated exit 1 lines. Renamed to what it proves — the guard names a missing composer explicitly before the composer is invoked — and the assertion is anchored to the guard step's own run block via a new stepBlock() helper. The guard was not moved into resolve-source: compose-notes needs its own .workflow-tooling checkout regardless (artifacts do not cross jobs), so an early guard duplicates the checkout and the guard in two jobs rather than moving them, and the condition it guards — the workflow file and the composer being in the same tree at the same workflow_sha — is close to impossible, so the expected saving is near zero against a permanent duplicate.
  3. LOWgithub.workflow_sha on an annotated-tag push may be the tag OBJECT id and nothing proved actions/checkout peels it. The guard step now runs git -C .workflow-tooling rev-parse --verify 'HEAD^{commit}', fails closed with a named message when it does not resolve, and prints the resolved commit next to TOOLING_SHA. Asserted in the contract test.
  4. LOWgh release list --limit 100 had no truncation signal. The window is now stable_tag_limit=200, named once so the check cannot drift from the limit, and a listing that comes back exactly full is refused with a ::error saying the window must be widened. The contract test pins both the limit and the equality check.
  5. LOWdocs/ops/RELEASE_TRUST_AND_DISTRIBUTION.md still described the base as "the newest published stable release" and never mentioned preview_tag. Both sentences now match the workflow. docs/STATUS.md, OUTSTANDING_TASKS.md, docs/IMPLEMENTATION_MASTERPLAN.md and docs/releases/V0_3_0_READINESS.md were deliberately left alone.

Verified at 6b591eb: node --test scripts/ci/release-desktop-dispatch.test.mjs — 86 tests, 86 pass (red first: 3 failing before the workflow change); select-changelog-base.test.mjs 17/17; scripts/ci/smart-ci/*.test.mjs 91/91; check-github-ops-governance.mjs and check-docs-governance.mjs both pass; bash -n over all 25 extracted run blocks, 0 failures; git diff --check clean. Workflow Lint cannot run locally (no actionlint on this box) — the hosted job is the evidence.

Two rounds is the ceiling. This PR parks under SC-10 for the maintainer after this round.

Refs #2250 (items 1 to 3; items 4 and 5 are PR #2684). Refs #2248, #2234, #1795. Control-plane change: parks under SC-10 for the maintainer's review after the fresh-context review.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

…ing window and an unpeeled tooling HEAD

Review round 1 on #2687.

* preview_tag on a PUBLISHING dispatch was ignored with a ::notice, on the
  reasoning that refusing "would cost a build for no safety gain". That is
  wrong: resolve-source is the first job, builds nothing, and both build jobs
  need it, so exit 1 there costs zero build minutes — and the refusal is the
  only mechanical signal that the dispatcher's intent (preview) and action
  (publish) disagree. It now prints a ::error naming both inputs and exits 1.

* The tooling guard now proves the .workflow-tooling checkout peeled to a real
  commit (git rev-parse --verify 'HEAD^{commit}') and prints the resolved commit
  next to TOOLING_SHA. github.workflow_sha on an annotated-tag push can be the
  tag OBJECT id; actions/checkout is expected to peel it, and nothing proved it.

* The stable-release listing window goes 100 -> 200 and is named once
  (stable_tag_limit), and a listing that comes back exactly full is refused:
  a full page is indistinguishable from a truncated one and the changelog base
  is chosen only from what was listed.

Contract tests move with them: the notice assertion becomes a refusal assertion
that also pins resolve-source as needs-free with every build job downstream of
it; the guard test is renamed to what it proves (compose-notes needs
build-backend, so the guard runs AFTER the Windows build — it names a missing
composer before the composer is invoked) and its exit-1 assertion is anchored to
the guard step's own run block instead of the whole job, which carries several
unrelated exit 1 lines.
…sal rule

The release-trust doc still described the changelog base as "the newest
published stable release", which is the pre-#2250 behaviour, and its rehearsal
paragraph did not mention preview_tag at all. Both now match the workflow: the
base is the newest stable release that sorts strictly before the target tag by
semver, and preview_tag renders the real stable page on a rehearsal while a
publishing dispatch that supplies it is refused before any build runs.
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Fix round

Round 1 of the review pipeline. Head moved 8a98c96 -> 6b591eb (two commits: b84abdb workflow + contract tests, 6b591eb the ops doc). Five findings, five fixed, none declined.

1. MEDIUM — preview_tag on a publishing dispatch was ignored, not refused — fixed

.github/workflows/release-desktop.yml (resolve-source). The old branch printed a ::notice and carried on, justified by a comment saying a refusal "would cost a build for no safety gain". That was factually wrong: resolve-source is the first job, builds nothing, and both build jobs (and compose-notes, and create-release) declare needs: resolve-source, so an exit 1 there costs zero build minutes — and the refusal is the only mechanical signal that the dispatcher's intent (preview) and action (publish) disagree.

Now: a ::error naming both inputs, then exit 1, before anything is built. The comment, the preview_tag input description and the file header comment were corrected to match. The contract test that pinned the notice now pins the refusal, refuses a regression to ::notice::preview_tag, asserts the error names both raw_preview_tag and tag, and asserts the refusal sits in resolve-source — which has no needs: of its own — with build-frontend, build-backend, compose-notes and create-release all downstream of it.

2. LOW — the tooling-guard test asserted a property the workflow does not have — fixed (renamed; guard deliberately not moved)

scripts/ci/release-desktop-dispatch.test.mjs. Renamed the tooling checkout is proved to carry the composer before the build is spent -> the tooling guard names a missing composer explicitly before the composer is invoked, with a comment saying why the old name was false (compose-notes needs build-backend, so the guard runs after the Windows build). The /exit 1/ assertion is anchored to the guard step's own run block through a new stepBlock(job, stepName) helper instead of the whole job block, which carries several unrelated exit 1 lines; the test also now asserts the guard precedes the node .workflow-tooling/...compose-release-notes.mjs invocation.

Choice made: renamed rather than moved. compose-notes needs its own .workflow-tooling checkout regardless — checkouts do not cross jobs — so an early guard in resolve-source duplicates the checkout and the guard across two jobs rather than moving them. And the state it guards against (the workflow file present at github.workflow_sha while the composer beside it is absent from the same tree) is close to impossible, so the expected saving is near zero against a permanent two-site duplicate. Renaming the test to what it actually proves, plus finding 3's peel check, is where the real safety is.

3. LOW — nothing proved github.workflow_sha peeled to a commit — fixed

The guard step now runs git -C .workflow-tooling rev-parse --verify 'HEAD^{commit}' after the tooling checkout, fails closed with a named ::error when it does not resolve, and prints the resolved commit next to TOOLING_SHA:

Workflow tooling checked out from <workflow_sha> (HEAD commit <sha>).

Simulated locally on a scratch repo: a checkout sitting on a commit resolves; the id of an annotated tag peels to the same commit; an empty directory yields the empty string, which the guard refuses. The contract test pins the rev-parse, the -z refusal and the two-value log line.

4. LOW — gh release list --limit 100 had no truncation signal — fixed

The window is now stable_tag_limit=200, named once so the limit and the check that reads it cannot drift apart, and a listing whose row count equals the limit is refused with a ::error saying the window must be widened — a full page is indistinguishable from a truncated one, and the changelog base is chosen only from what was listed. Simulated with limit 3: 0 and 2 rows accepted, 3 rows refused. The contract test pins stable_tag_limit=200, the -eq check, and the "widen the listing window" wording.

5. LOW — the ops doc still described the pre-#2250 changelog base — fixed

docs/ops/RELEASE_TRUST_AND_DISTRIBUTION.md: the base is now stated as the newest published stable release that sorts strictly before the tag being built by semver, not the globally newest by release date, with the consequence named (re-running an older tag after a newer one shipped cannot render a backwards changelog). The rehearsal paragraph now covers preview_tag: it renders the real stable page, it never publishes, and a publishing dispatch that supplies it is refused before any build runs. docs/STATUS.md, OUTSTANDING_TASKS.md, docs/IMPLEMENTATION_MASTERPLAN.md and docs/releases/V0_3_0_READINESS.md were left untouched as instructed.

Verification (run in the worktree, at 6b591eb)

command result
node --test scripts/ci/release-desktop-dispatch.test.mjs 86 tests, 86 pass, 0 fail — red first: 86/83/3 after the test edits and before the workflow edits, failing exactly the three assertions for findings 1, 3 and 4
node --test scripts/ci/select-changelog-base.test.mjs 17 tests, 17 pass
node --test scripts/ci/smart-ci/*.test.mjs 91 tests, 91 pass
node scripts/check-github-ops-governance.mjs passed
node scripts/check-docs-governance.mjs passed
bash -n over every extracted run: block 25 blocks checked, 0 failures (the YAML also safe_loads, which is what the extractor does first)
git diff --check clean
local simulation of the three new fail-closed fragments truncation, HEAD peel and the preview/publish refusal all behave as intended

Not verified: Workflow Lint / actionlint / shellcheck cannot run locally — actionlint is not installed on this box, so the hosted Workflow Lint job at the new head is the evidence for it. Nothing was executed against the real GitHub release API: the gh release list truncation refusal and the rev-parse peel are proved by local simulation and by the contract tests, not by a dispatch. No dotnet or frontend surface is touched, so no backend or frontend suite was run.

Test count is unchanged at 86 — every change to the suite tightened or corrected an existing test rather than adding one.

Two rounds is the ceiling; this PR parks under SC-10 for the maintainer after this round.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Review disposition (beta-platform-integrity lane): parked under SC-10

This PR changes .github/workflows/release-desktop.yml, .github/workflows/ci-required.yml and scripts/ci/**, so it is control-plane (T2) under the ADR-0066 amendment and merges only after the maintainer's own review plus the fresh-context review below. It is left open, ready for review, not merged by the lane.

Round 1 (fresh-context, read-only reviewer at 8a98c96): SHIP, no CRITICAL or HIGH; one MEDIUM, four LOW. Confirmed by reading every job: preview_tag reaches Bash only through a step env var, is consulted only on the publish == false branch and clears the same grammar gate as a real tag; render_tag and render_prerelease are consumed by compose-notes alone while the built archive name, --asset, --checksum-file, the provenance file, every gh release call and the create-release gate stay on the resolved tag; the tooling checkout moves only the page renderer (the ZIP and its sha256 are still built from and re-verified against the tagged tree, and --upgrading, --notes, --checksum-file are unprefixed); the selector's grammar is byte-identical to the shell gate's, selection is strictly-before, prereleases and drafts are excluded, an unparseable target fails closed, the first-release path still yields notes, and the listing retry matches generate-notes.
Round 2 (scoped re-review at 6b591eb): SHIP, all five findings closed, one LOW residual (a doc antecedent), fixed in a docs-only commit. Two rounds is the ceiling.

Fixed in the fix round (b84abdb, 6b591eb) and after (64cd68f, docs only)

  • MEDIUM, a preview_tag on a publishing dispatch was ignored with a notice on a false cost argument: it is now refused in resolve-source with a ::error naming both inputs and exit 1 before any build (resolve-source is the first job and every build job needs it, so the refusal costs nothing); %q escaping keeps an unchecked value from opening a second workflow command; the contract test pins the refusal, forbids the notice, and asserts the job graph.
  • LOW, the tooling-guard test claimed a property the workflow lacks and matched exit 1 against the whole job: renamed to what it proves and anchored to the guard step's own run block; the guard stays in compose-notes by choice (checkouts do not cross jobs, and the guarded state is near-impossible), recorded in the PR body.
  • LOW, github.workflow_sha on an annotated-tag push may be a tag object: the guard now verifies HEAD^{commit} resolves, fails closed otherwise, and logs both values.
  • LOW, the release listing had no truncation signal: limit 200 through one named variable, fail closed when the count equals it.
  • LOW, the release ops doc stated the superseded changelog-base rule and omitted the new input: updated; the orphaned "That artifact name" antecedent restored in 64cd68f.

Recorded, not fixed (maintainer's review)

  • Whether actions/checkout peels an annotated github.workflow_sha in practice is unproven; the workflow now fails closed if it does not, which is the point, and the first real run prints both values.
  • A missing composer at the workflow revision is still discovered after the Windows build, by choice; the message is named rather than MODULE_NOT_FOUND.
  • A repository with 200 or more stable releases is refused until the window is widened; not reachable today.
  • No live workflow_dispatch was run: a rehearsal with preview_tag set, and a re-dispatch of a pre-0.3 tag, are maintainer actions and are what proves items 1 and 2 end to end.

Verification (local; the hosted run on the exact head is the R4 proving check)

release-desktop-dispatch.test.mjs red first (15 failing on the unchanged workflow, then 3 for the fix round) and green at 86/86; the new select-changelog-base.test.mjs 17/17 (red first on the missing module and the missing ci-required step); Smart CI 91/91 and cache-trust 28/28 unchanged; GitHub-operations and docs governance green; YAML parses; bash -n over all 25 extracted run blocks; git diff --check clean; a local simulation of the changed render step (real composer and selector from a .workflow-tooling-shaped path) shows the selector choosing v0.2.0 for target v0.3.0-rc.1 where the old call returned v0.3.0, and the preview render carrying the real ## v0.3.0 UPGRADING body while the asset stays the built one. Hosted at 6b591eb: Workflow Lint and Release Workflow Contract green (actionlint accepts github.workflow_sha); the docs-only 64cd68f re-runs the same lanes.

Chris0Jeky added a commit that referenced this pull request Sep 5, 2026
…D-14's defaults; note D-10's superseded audit description
Chris0Jeky added a commit that referenced this pull request Sep 5, 2026
Readiness: the human-gate table was not unchanged at 15:15Z, its SC-10 row now counts twelve
(#2684 and #2687 named); the cancelled 6cf9ef7 run had 15 green, Secret Scan skipped, one
cancelled; #2691's seeding time and its 03:26Z red are exact; the dropped clause-4 evidence
(cancelled-run range, run ids, timestamps) is restored; #2378 and #2588 occurrence lists gain the
sweep's sightings. STATUS: same job tally; range note that the sweep's last merge is one past the
block's declared bound. OUTSTANDING_TASKS.md: the rule-3 Changelog entry for the D-12 tick and the
section K exception sentence.
…e (resolve-source outputs: keep render_tag/render_prerelease alongside version)
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Coordinator (2026-09-06, SC-10 delegated). Updated to main: three conflicts in resolve-source (this PR's render_tag/render_prerelease outputs and prints versus #2535's version output) resolved additively, both sides kept (merge 6b17f62de). The Planner Self-Test lane then failed in 13 s on the CI-11 guard: this PR's new tooling checkout was actions/checkout@v7, written before #2502 pinned every action; pinned to the same SHA the file's other checkouts use (905f82a62). Local proof at that head: action-pins.mjs --check clean, release-desktop-dispatch, select-changelog-base and the smart-ci suites 237/237. A fresh-context re-review against the moved base is running; merge follows it and the hosted run.

@Chris0Jeky
Chris0Jeky merged commit d69beed into main Sep 6, 2026
35 checks passed
@Chris0Jeky
Chris0Jeky deleted the issue-2250/release-rehearsal-and-changelog-base branch September 6, 2026 14:05
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant