fix(release): recover duplicate v0.8.22 drafts - #534
Merged
Conversation
Three independent reviews of the v0.8.22 duplicate-draft recovery surface found no Critical or Important defects, but two mutation-surviving test gaps and several hardening items. Close them: - Pin the innermost writer URL guard to the two duplicate Release IDs so neither the canonical Release nor an asset endpoint is reachable even if a call site regresses. Previously any `.../releases/` prefix matched. - Enforce the credential-free capture guarantee on the exact bytes about to be written. Capture evidence carries the canonical Release body, which has URLs, so it cannot reuse the receipt's stricter transport rule. - Prove at capture that the canonical body fits the recovery archive asset bound, instead of failing part-way through a frozen production window. The bound is now one exported constant shared by core and adapters. - Cover local-HEAD drift in the reviewed-authority case table. Deleting the REVIEWED_COMMIT_NOT_LOCAL_HEAD check previously left every test green. - Replace the observer's test-only second parameter with the standard dependency seam and export the real factory, so the gate that runs after both duplicates are quarantined is exercised rather than always stubbed. Making that factory throw previously left every CLI test green. - Assert byte-identical replay is a no-op: zero writer constructions, zero mutations, both duplicates `preexisting-quarantined` with null fences. - Record the pre-merge production reference and the trailing-newline PATCH round-trip evidence in the runbook, plus an `evidenceCapturedAt` receipt row. Each new test was mutation-checked: it fails when its guard is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove
had a problem deploying
to
vercel-preview
September 2, 2026 23:26 — with
GitHub Actions
Failure
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CodeQL flagged js/incomplete-hostname-regexp (high) on the recovery test helper: two RegExps interpolated the GitHub API base without escaping, so the unescaped dots in `api.github.com` let the matchers accept hosts other than the exact API origin, weakening the assertions. The same bug class was live in production code. `recoveryEvidenceKind` interpolated an asset-name prefix containing the version `0.8.22`, so an asset named `dawn-v0X8X22-duplicate-<id>-original-body-<sha>.txt` was classified as the original-body archive. Verified directly: the unescaped pattern matches that name, the escaped one rejects it. Escape both. Behaviour on real names is unchanged; only near-miss names that should never have matched are now rejected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove
had a problem deploying
to
vercel-preview
September 2, 2026 23:59 — with
GitHub Actions
Failure
CodeQL does not model String.prototype.replace as escaping, so the previous fix left js/incomplete-hostname-regexp open on the same literal: alert 84 closed and alert 85 opened in its place. Drop the regexes instead of escaping them. Both fixture matchers only ever needed an exact prefix, an optional exact suffix, and a decimal id, which exactTrailingId now does directly. That removes the interpolation the rule fires on rather than suppressing the finding, and reads more plainly than the pattern it replaces. The production classifier keeps its escaped matcher; its prefix is not a hostname and its escaping is verified by test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a production-facing release-recovery command with tightly scoped but real mutation behavior that warrants final human review of operational and safety invariants.
Pull request overview
Adds a temporary, v0.8.22-candidate–pinned operator recovery surface to quarantine two duplicate escrow draft Releases (without deleting any Release/tag/assets), plus the supporting spec/plan documentation and an expanded runbook procedure for a one-time production operation.
Changes:
- Introduces a strict
capture/applyCLI (scripts/release/recover-v0.8.22-duplicate-drafts.mjs) with containment, write-once outputs, and stable sanitized failure modes. - Adds a comprehensive CLI test suite covering argument grammar, filesystem safety (no-follow, mode checks, link/symlink rejection), output reservation semantics, and observer wiring.
- Adds the recovery design + implementation plan docs, and updates the release-integrity runbook with the operator sequence, receipts, and verification steps.
File summaries
| File | Description |
|---|---|
| scripts/release/recover-v0.8.22-duplicate-drafts.mjs | Candidate-specific recovery CLI implementing strict invocation grammar, containment checks, durable write-once outputs, and production observer wiring. |
| scripts/release/test/duplicate-draft-recovery-cli.test.mjs | Extensive CLI-focused test coverage for parsing, containment, I/O safety, token/secret hygiene, and failure/rollback behavior. |
| docs/superpowers/specs/2026-09-01-v0.8.22-duplicate-draft-recovery-design.md | Formal design spec defining goals/non-goals, evidence model, quarantine states, and concurrency honesty constraints. |
| docs/superpowers/plans/2026-09-01-v0.8.22-duplicate-draft-recovery.md | Stepwise implementation plan describing modules, tests, and operational workflow for the one-time recovery and cleanup. |
| docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md | Runbook update documenting the one-time operator procedure, attempt ledger, independent verification steps, and live receipt fields. |
Review details
- Files reviewed: 8/9 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The v0.8.22 release candidate is fully escrowed but cannot advance to npm publication. GitHub holds three mutable draft Releases carrying the same canonical Dawn
ESCROWEDmarker, the same candidate commit, and the same 45-asset base:tag_name379991871untagged-be0ff4bee4ba43b521a9379982100untagged-a13939767dd2419ade01379986168untagged-20706099efa3c38335a8The controller deliberately rejects more than one marker-backed draft before any exact Release read or mutation. That invariant is correct, but it strands this candidate: the
v0.8.22tag is immutable and predates the later draft-identity fixes onmain, so a general controller change onmaincannot replace the workflow definition stored at the tag.Design:
docs/superpowers/specs/2026-09-01-v0.8.22-duplicate-draft-recovery-design.mdPlan:
docs/superpowers/plans/2026-09-01-v0.8.22-duplicate-draft-recovery.mdWhat this adds
A temporary, candidate-pinned operator command — not a generic identity override, and not reachable from any release-owner workflow or package script.
scripts/release/duplicate-draft-recovery.mjs— frozen candidate policy, canonical evidence schema, four-state duplicate classifier, archive/receipt/notice bytes, capture and apply orchestration, final authorization check.scripts/release/duplicate-draft-recovery-adapters.mjs— bounded read-only production adapters, reviewed-merge authority, and the narrow recovery-only body writer.scripts/release/recover-v0.8.22-duplicate-drafts.mjs— strict two-mode CLI with write-once,0600, contained receipts.node scripts/release/recover-v0.8.22-duplicate-drafts.mjs capture \ --reviewed-commit "$RECOVERY_SHA" \ --output .dawn/release-recovery/v0.8.22-capture-01.json node scripts/release/recover-v0.8.22-duplicate-drafts.mjs apply \ --evidence .dawn/release-recovery/v0.8.22-capture-01.json \ --acknowledge-non-atomic-release-edit-freeze \ --output .dawn/release-recovery/v0.8.22-apply-01.jsonFor each duplicate, in ascending Release-ID order,
applyarchives the original body as an asset, uploads a canonical JSON recovery receipt asset, then replaces only the livebodywith a recovery notice that is intentionally not a valid Dawn marker. Every individual mutation is preceded by a fresh full re-read of live authorization facts.What it does not do
No Release, tag, existing asset, npm version, or Actions artifact is deleted. Nothing is published.
.github/workflows/release.ymlis neither modified, enabled, nor dispatched by the command. Normal duplicate detection is unmodified and stays fail-closed — the only pre-existing file this PR touches is the runbook.Concurrency honesty
GitHub does not document conditional
PATCHfor the Release update endpoint, and live draft Releases return weak ETags, so there is no standards-compliantIf-Matchcompare-and-swap available. The command therefore does not claim atomicity. It uses an explicit fail-closed compare-before-write fence over a normalized projection (Release ID, opaquetag_name, title,target_commitish, draft/prerelease/immutable flags, ordered asset IDs/names/digests/sizes), sends one body-only PATCH, and immediately revalidates. The residual TOCTOU window is covered operationally by a recorded operator edit freeze, and the final receipt recordsatomic: falseplus the freeze acknowledgement.Review
Three independent reviews (mutation boundary, evidence/resume semantics, CLI/test conformance) returned no Critical and no Important defects. The third mutation-tested 11 safety invariants; 9 were caught and 2 survived. Both survivors are closed here, and every new test was mutation-checked to fail when its guard is removed:
REVIEWED_COMMIT_NOT_LOCAL_HEADcheck left every test green;Also hardened: the innermost writer URL guard is pinned to the two duplicate Release IDs (it previously matched any
.../releases/prefix); capture evidence is credential-scanned on the exact bytes before publication; the canonical body's archive-asset bound is proved at capture from one shared constant rather than failing mid-window; and byte-identical replay now explicitly asserts zero writer constructions, zero mutations, and twopreexisting-quarantinedoutcomes with null fences.Two review concerns were resolved with evidence rather than code, and both are recorded in the runbook:
\n, and its survival throughPATCHis not assumed:canonicalReleaseBodyalready composes controller bodies ending in exactly one\n, and the shippedupdateDraftReleaseIfCurrentPATCHes such a body then re-reads it under exact body equality. The escrow drafts carry that trailing newline today.publish-npmascompleted/skipped, one attempt each.Known residual gap, stated rather than buried: there is no end-to-end drive of
observeProductionCandidate+planReleasethrough fakes for the recovery path. Those components are covered by the existingobserve-production.test.mjs; what is added here covers the glue's construction and wiring, not its async body.Verification
Production premises re-confirmed read-only against
cacheplane/dawnaiimmediately before opening this PR:v0.8.22peels to2a80deece2ff958fe7fde8fddeb4f99bed70a1c854924b7e963e593d3988d9ce1708bfbb2dfec46606cd4a47125987d24ad789f0, 45 assets each, mutable, not prerelease.github/workflows/release.ymlisdisabled_manually, with no nonterminal runsv0.8.22; npmlatestis0.8.21and0.8.22returnsE404Also proven directly: a quarantined duplicate no longer satisfies
isManagedReleaseForTag(release, "v0.8.22"), and the recovery notice is rejected byparseReleaseMarker.Gates: focused suites 185/185;
DAWN_REQUIRE_DOCKER=1 pnpm ci:validategreen (459 test files, 0 failures). One earlier run hit a single unrelated failure intest/k8s-compat/assert-docker-smoke.test.tsunder full-suite concurrency; that file passes 94/94 in isolation, shares no import with this diff, and the run before it passed on identical k8s-compat code, so it is a load-dependent Docker flake rather than a regression here — noted because the signature is new.Follow-up
This surface is removed in a cleanup PR after v0.8.22 and the queued v0.8.23 fixed group are terminal. The design, plan, operator receipts, and duplicate evidence drafts are preserved.
🤖 Generated with Claude Code