Skip to content

fix(release): publish validated candidates after field repairs - #542

Merged
taco3064 merged 2 commits into
mainfrom
fix/521-release-field-repair-cycle
Sep 22, 2026
Merged

taco3064 merged 2 commits into
mainfrom
fix/521-release-field-repair-cycle

Conversation

@taco3064

@taco3064 taco3064 commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Problem and resulting behavior

Field repairs necessarily happen after version preparation. The old Changesets gate rejected those repairs even after the final exact candidate passed CI and full field convergence. Removing that restriction fixes future release cycles, but tagging the already validated 90d2 candidate would still execute its old gate.

This PR fixes both boundaries. Changesets validation retains version origin, consumed changesets, matching release notes, and no pending changesets, while the exact-candidate field gate remains authoritative for readiness. The existing release.yml also gains a main-only workflow_dispatch recovery job so newer release tooling can verify and publish an older validated candidate. No second workflow is added.

Recovery behavior

  • Inputs select the full candidate SHA, original CI run, and matching stable tag. publish defaults to false.
  • Download and independently re-resolve the unique, unexpired CI artifact; verify the successful main CI workflow, package identity and digest, and candidate ancestry.
  • Run the current Changesets and field gates in a clean detached checkout of the original candidate. Tool SHA and candidate SHA remain distinct.
  • Reject changed/foreign artifacts, wrong or moved tags, dirty candidate checkout, stale field authority, failed gates, and non-main execution.
  • With publish enabled, sign provenance identifying the candidate source/artifact/run and the actual publisher tool SHA/workflow/run separately. Use the exact original tarball with --ignore-scripts and --provenance-file; no rebuild, repack, or field-status transfer.
  • Reverify immediately before publication. A missing tag is created at the candidate SHA; an existing tag must resolve there. GitHub notes come from the candidate changelog. The normal tag job and manual job share concurrency and run on separate event conditions.

For #521, after this PR is merged and reviewed, use release.yml from main with candidate_sha=90d2a27c913ca95a29b498e94b90831534a06c92, candidate_run_id=35738770473, tag=v4.1.0, publish=false. Review that verification run before an owner-authorized publish=true invocation. The candidate remains 90d2, not this PR's merge SHA, so no field evidence is inherited by the tooling commit.

Verification and boundaries

  • 92 focused tests across recovery orchestration, provenance, Changesets repair history, and field authority passed. After consolidating the workflow identity, the 32 provenance tests passed again.
  • Focused ESLint/lint-staged, staged diff check, and actionlint passed.
  • Live read-only preparation downloaded original artifact 10698772951 from CI 35738770473 and verified digest 9b8a1b84a499d57775883c4b261391a7d60d565a97cd7c8965b2e0604fc64023. Newer gate scripts both passed against the detached 90d2 checkout while publisher HEAD was ae81ad7. This was a local working-candidate probe, not a published or signed release.
  • Pinned sigstore 4.1.1 API and its verifier policy were checked against actual package code, including workflow identity regex and exact certificate SHA OID.
  • Independent acceptance: ACCEPTED for base ae81ad7 and candidate tree 20fb1c7242be356537b7ebc7a159be29615e027d.
  • Live GitHub OIDC signing and npm registry acceptance remain publishing-time checks; no claim is made that publication has already passed. Failure stops publication rather than falling back to unsigned output.
  • Husky commit/push hooks are skipped by explicit owner request. Exact-head PR CI remains required.

Part of #521. Supersedes its earlier post-version product freeze and tag-only execution restriction under the owner's revised process decision. The failed v4.1.0 tag has been removed; this PR does not recreate it, merge itself, publish, or close #521.

@taco3064 taco3064 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

CHANGES_REQUIRED

只看程式與 #521 發布契約,不看 CI。

這個 PR 本身解掉了「版本 commit 之後不准再有產品修復」這個未來 release cycle 的規則問題,但它 沒有解掉目前 4.1.0 的實際發布問題。而且照現在的 exact-SHA 契約,merge #542 之後反而一定要再做一次 final field convergence。

關鍵不是 release-changeset-gate.mjs 的內容,而是 哪個 commit 會執行這份新 gate。

目前:

已完成完整 field 的候選:
90d2a27c913ca95a29b498e94b90831534a06c92

但 #542 的新 release gate 在:

ae81ad7a487d1ba9632c3e409289e5e83331bd3c
→ merge 後會是另一個新的 main SHA

而 .github/workflows/release.yml 仍是 tag push 後直接 checkout tag target 本身,沒有從 default branch 載入新版 release script。

所以兩條路目前都走不通:

1. tag 仍指向 90d2a27

Release workflow checkout 90d2a27,執行的仍是 90d2 當時舊版:

scripts/release-changeset-gate.mjs

也就是仍會因 version commit 後的 src/ 修復而失敗。

#542 merge 到 main 不會改變 90d2 tag checkout 裡的 script。

2. tag 改指向 #542 merge 後的新 SHA

這時新版 Changesets gate 會生效,但 release-field-gate.mjs 仍要求:

blueprint/field-convergence = success
candidateSha === exact tag SHA

而目前完整 field authority 是 90d2,不是新的 merge SHA。

這個 PR 自己新增的文件甚至明確寫了:

a later commit cannot inherit an earlier candidate's field status, including a commit that changes only release tooling.

因此 merge #542 後若要 tag 新 SHA,依這個 PR 自己的契約就必須:

new main SHA
→ new CI candidate
→ complete field matrix
→ field-convergence success
→ tag

也就是又回到我們正想避免的完整 field 重跑。

為什麼新增的 test 沒抓到這件事

release-changeset-gate.repairs.test.mjs 測的是:

「如果新版 gate 已經存在於 repaired SHA」
→ Changesets gate 可以接受
→ field gate 要求 repaired SHA 自己的 evidence

這個單元模型本身沒錯,但它沒有測到 目前 4.1 的 transition problem:

已驗證 candidate = 90d2
新版 release gate = 後來才出現於另一個 SHA

所以「actual gate invocation succeeds on existing 90d2 history」只是拿 新版 script 去讀 90d2 的 Git history;它不等於 tag 90d2 時 GitHub Actions 會執行新版 script。

要先決定這張 PR 的目標

如果 #542 的目標只是:

修正未來 4.1.x / 4.2 的 release policy,允許 version preparation 後修 product,再對最後 candidate 做 full field

那目前方向基本成立,但它不能拿來解現在 4.1.0 不重跑 field 的發布問題。

如果 #542 的目標是目前 #521:

發布已經完成 full field 的 90d2 原始候選,不再重跑 16 組

那這個 implementation 還缺一條真正能從新版 main 執行、但只允許發布 90d2 exact artifact 的 recovery path,或另一個同等安全、明確處理「validated SHA 與 release-tool SHA 不同」的機制。

不能用「merge 新 gate → tag 新 SHA」偷偷繞過 exact-SHA field authority,也不能期待 tag 90d2 自動吃到 main 上的新 script。

目前這是 blocker。除此之外,單就「取消 version commit 後 product repair 禁令、改由 exact final field candidate 作 freeze point」這個長期 policy,我沒有看到第二個程式層級問題。

@taco3064 taco3064 changed the title fix(release): allow field repairs after version preparation fix(release): publish validated candidates after field repairs Sep 22, 2026

@taco3064 taco3064 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

CHANGES_REQUIRED

只看程式與 #521 發布契約,不看 CI。

上一輪真正的 blocker 已經解掉:這版新增了可以從新版 main 執行、但只發布舊的 exact validated candidate 的 recovery path,而且沒有把 90d2 的 field status 搬到 tooling SHA。

程式上的 recovery 邊界目前是對的:

  • workflow_dispatch 只允許從 refs/heads/main 執行;
  • candidate 必須是成功的 main CI run,且 artifact 唯一、未過期;
  • manifest / tarball digest / package identity 都重新驗證;
  • candidate 必須是 recovery tooling SHA 的 ancestor;
  • 用新版 Changesets / field gate 對 detached 90d2 checkout 驗證;
  • existing tag 若存在必須仍解析到 candidate;不存在才在 publish 階段建立;
  • publish 使用原始 tgz,不 rebuild、不 repack;
  • publish 前再次 reverify;
  • exact-SHA field authority 仍留在 90d2,不會繼承到 #542 merge SHA。

我目前沒有再看到上一輪「validated SHA 與 release-tool SHA 不同」的程式層級 blocker。

但現在有一個 decision-fidelity blocker:#521 本身還沒改。

#521 現在仍明寫:

Stage 5:
push tag v4.1.0
→ Release (tag)

Do not manually dispatch this workflow.
The tag push is the trigger and the release workflow is the publisher.

而 #542 現在實作的是:

workflow_dispatch on main
→ publish=false verification
→ owner-authorized publish=true recovery
→ create v4.1.0 at the old converged SHA
→ publish original CI tarball

這兩個契約是直接衝突的。

同時 #521 的 Stage 5 還要求 Changesets gate 驗證:

no disallowed publishable input changed after the release SHA

但 #542 正在刪掉這條 gate,改成「version preparation 只建立版本來源,final field candidate 才是 freeze point」。

所以目前不是 code 還缺什麼,而是 release SSOT 還停在舊規則。

請先把 #521 的發布契約同步到這個 recovery model,至少更新:

  1. Stage 5 增加 manual recovery publication 例外;
  2. 移除「Do not manually dispatch this workflow」作為絕對規則;
  3. Changesets gate 的責任改成 version origin / consumed changesets / changelog / no pending changesets;
  4. 明確保留 exact candidate field authority,不允許 tooling SHA 繼承;
  5. Action summary / Publication acceptance criteria 同步 recovery flow;
  6. 針對這次 4.1 明確寫出:
    candidate=90d2a27c...、run=35738770473、先 publish=false,review 後才允許 publish=true。

等 #521 同步後,我目前從這版程式看不到第二個 blocker。

也就是說,這次不是要再改 recovery code,而是要把已經決定的新發布規則寫回 #521。

@taco3064 taco3064 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

APPROVED

重新依最新 head c6cc4beb5d4985a2edeb40a5e8b7fef4befacada 與已同步後的 #521 發布契約做 decision-fidelity review。

上一輪 blocker 已收掉。

這版現在把兩個身份切開得正確:

product candidate = 90d2a27c913ca95a29b498e94b90831534a06c92
publisher tooling = #542 merge 後的新 main SHA

而且 recovery path 沒有偷渡 field authority:

  • 不把 90d2 的 blueprint/field-convergence 複製到 tooling SHA;
  • 不把 tag 移到 tooling SHA;
  • candidate 必須來自成功的 main CI run;
  • unique artifact / manifest / package identity / digest 都重新驗證;
  • candidate 必須是 tooling SHA 的 ancestor;
  • newer Changesets / field gates 對 clean detached candidate checkout 執行;
  • existing tag 若存在必須解析回 candidate;
  • publish 使用原 CI tgz,不 rebuild、不 repack;
  • publish 前再次 reverify;
  • recovery provenance 明確區分 candidate/run/artifact 與實際 publisher tooling/workflow run;
  • verification-only 與真正 publish 分成 publish=false / owner-authorized publish=true 兩階段。

#521 也已同步 recovery publication 例外、Changesets gate 新責任,以及本次 4.1 的 exact recovery identity:

candidate_sha = 90d2a27c913ca95a29b498e94b90831534a06c92
candidate_run_id = 35738770473
tag = v4.1.0
artifact SHA-256 = 9b8a1b84a499d57775883c4b261391a7d60d565a97cd7c8965b2e0604fc64023

另外我把 #521 Stage 2 殘留的「main 一變,舊 candidate 一律失效」修成只針對 product/source/package input change;release-tooling-only commit 現在和 recovery model 一致,不再自相矛盾。

只看程式與發布契約,不看 CI。目前沒有剩餘 blocker。

下一個正確流程是:

merge #542
→ main 上跑 recovery publish=false
→ review verification run
→ owner 明確授權
→ 用相同 candidate/run/tag inputs 跑 publish=true
→ 驗 npm + GitHub Release

不需要因為 #542 的 tooling SHA 變更而重跑 16 組 field。

@taco3064
taco3064 marked this pull request as ready for review September 22, 2026 16:46
@taco3064
taco3064 merged commit 464af79 into main Sep 22, 2026
13 checks passed
@taco3064
taco3064 deleted the fix/521-release-field-repair-cycle branch September 22, 2026 16:46
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.

Cut and publish Blueprint 4.1.0 from one exact converged release candidate

1 participant