Skip to content

fix(release): recover duplicate v0.8.22 drafts - #534

Merged
blove merged 44 commits into
mainfrom
blove/recover-0.8.22-duplicate-drafts
Sep 3, 2026
Merged

fix(release): recover duplicate v0.8.22 drafts#534
blove merged 44 commits into
mainfrom
blove/recover-0.8.22-duplicate-drafts

Conversation

@blove

@blove blove commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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 ESCROWED marker, the same candidate commit, and the same 45-asset base:

Role Release ID Temporary tag_name
Canonical candidate 379991871 untagged-be0ff4bee4ba43b521a9
Duplicate evidence draft 379982100 untagged-a13939767dd2419ade01
Duplicate evidence draft 379986168 untagged-20706099efa3c38335a8

The 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.22 tag is immutable and predates the later draft-identity fixes on main, so a general controller change on main cannot replace the workflow definition stored at the tag.

Design: docs/superpowers/specs/2026-09-01-v0.8.22-duplicate-draft-recovery-design.md
Plan: docs/superpowers/plans/2026-09-01-v0.8.22-duplicate-draft-recovery.md

What 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.
  • Three test files (185 tests) covering the design's testing checklist.
  • Runbook: the one-time operator sequence and freeze record fields.
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.json

For each duplicate, in ascending Release-ID order, apply archives the original body as an asset, uploads a canonical JSON recovery receipt asset, then replaces only the live body with 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.yml is 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 PATCH for the Release update endpoint, and live draft Releases return weak ETags, so there is no standards-compliant If-Match compare-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, opaque tag_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 records atomic: false plus 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:

  • local-HEAD authority drift had no test — deleting the REVIEWED_COMMIT_NOT_LOCAL_HEAD check left every test green;
  • the real post-mutation observer was always stubbed — making its factory throw left every CLI test green. Its test-only second parameter is replaced by the standard dependency seam.

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 two preexisting-quarantined outcomes with null fences.

Two review concerns were resolved with evidence rather than code, and both are recorded in the runbook:

  • Trailing newline. The recovery notice ends with one \n, and its survival through PATCH is not assumed: canonicalReleaseBody already composes controller bodies ending in exactly one \n, and the shipped updateDraftReleaseIfCurrent PATCHes such a body then re-reads it under exact body equality. The escrow drafts carry that trailing newline today.
  • Precondition 9 is implemented more strictly than the design words it, so it was checked live: all five candidate runs report publish-npm as completed/skipped, one attempt each.

Known residual gap, stated rather than buried: there is no end-to-end drive of observeProductionCandidate + planRelease through fakes for the recovery path. Those components are covered by the existing observe-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/dawnai immediately before opening this PR:

  • annotated tag v0.8.22 peels to 2a80deece2ff958fe7fde8fddeb4f99bed70a1c8
  • exactly three drafts identify the candidate — no fourth
  • both duplicates are untouched: all three bodies hash to 54924b7e963e593d3988d9ce1708bfbb2dfec46606cd4a47125987d24ad789f0, 45 assets each, mutable, not prerelease
  • .github/workflows/release.yml is disabled_manually, with no nonterminal runs
  • no published Release at v0.8.22; npm latest is 0.8.21 and 0.8.22 returns E404

Also proven directly: a quarantined duplicate no longer satisfies isManagedReleaseForTag(release, "v0.8.22"), and the recovery notice is rejected by parseReleaseMarker.

Gates: focused suites 185/185; DAWN_REQUIRE_DOCKER=1 pnpm ci:validate green (459 test files, 0 failures). One earlier run hit a single unrelated failure in test/k8s-compat/assert-docker-smoke.test.ts under 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

blove added 30 commits September 1, 2026 16:12
blove and others added 9 commits September 2, 2026 11:50
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>
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
dawnai Ready Ready Preview Sep 3, 2026 12:35am UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

Comment thread scripts/release/test/duplicate-draft-recovery.test.mjs Fixed
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>
Comment thread scripts/release/test/duplicate-draft-recovery.test.mjs Fixed
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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/apply CLI (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.

@blove
blove merged commit 9cd20cd into main Sep 3, 2026
25 of 26 checks passed
@blove
blove deleted the blove/recover-0.8.22-duplicate-drafts branch September 3, 2026 02:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants