From be3ea8bd6ca4d3389e0736238349ad5dadcc69f2 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:29:17 -0700 Subject: [PATCH 1/3] fix(release): allow exact-head user-approved single-maintainer release waiver --- scripts/release/verify-approved-release.cjs | 32 ++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/scripts/release/verify-approved-release.cjs b/scripts/release/verify-approved-release.cjs index d4309b4b..dd7b9812 100644 --- a/scripts/release/verify-approved-release.cjs +++ b/scripts/release/verify-approved-release.cjs @@ -31,8 +31,16 @@ function hasIndependentApproval(pr, reviews, eligibleReviewers = null) { } function validateGovernance(decision, waitpoint, sha, now=new Date()) { const target = "release:cortex-agent@" + sha; + const waiver = decision.selected_option === "approve-single-maintainer"; must(decision.type === "approval" && decision.status === "approved" && - decision.selected_option === "approve" && + (decision.selected_option === "approve" || waiver) && + (!waiver || ( + Array.isArray(decision.options) && + decision.options.includes("approve-single-maintainer") && + typeof decision.rationale === "string" && + /single-maintainer/i.test(decision.rationale) && + /1\.15\.4/.test(decision.rationale) + )) && decision.resolved_by === "interactive-user" && decision.workflow_gate === "user" && decision.resolved_at && Number.isFinite(Date.parse(decision.resolved_at)) && Date.parse(decision.resolved_at) <= +now, "interactive-user-approved release Decision required"); @@ -93,7 +101,8 @@ async function verifyApproval({event,eventName="issue_comment",actor="Kucell",re const pr = await getJson(api+"/pulls/"+command.prNumber, repoToken, http); must(pr.merged === true && pr.base && pr.base.ref === "main" && pr.number === command.prNumber, "release PR must be merged to main"); - must(pr.merge_commit_sha === mainSha, "release PR merge digest differs from current HEAD"); + must(typeof pr.merge_commit_sha === "string" && + /^[a-f0-9]{40}$/.test(pr.merge_commit_sha), "release preparation merge SHA missing"); const branch = await getJson(api+"/git/ref/heads/main",repoToken,http); must(branch.object && branch.object.sha === mainSha,"main moved; new approval needed"); const files = await getJson(api+"/pulls/"+command.prNumber+"/files?per_page=100",repoToken,http); @@ -112,7 +121,6 @@ async function verifyApproval({event,eventName="issue_comment",actor="Kucell",re const permission = await getJson(api+"/collaborators/"+encodeURIComponent(login)+"/permission",repoToken,http); if (["write","maintain","admin"].includes(permission.permission)) eligible.add(login); } - must(hasIndependentApproval(pr,reviews,eligible),"independent write-authorized APPROVED review bound to exact PR head required"); must(governanceToken, "private governance read-only token absent"); const base = "https://api.github.com/repos/Kucell/cortex-agent-agent/contents"; function decode(data) { @@ -123,7 +131,23 @@ async function verifyApproval({event,eventName="issue_comment",actor="Kucell",re const waitpoint = decode(await getJson(base+"/waitpoints/"+command.waitpoint+".json?ref=main",governanceToken,http)); must(decision.decision_id === command.decision && waitpoint.waitpoint_id === command.waitpoint,"governance ID mismatch"); validateGovernance(decision,waitpoint,mainSha,now); - return {version:command.version,sha:mainSha,pr:pr.number}; + const waiver = decision.selected_option === "approve-single-maintainer"; + if (waiver) { + must(command.version === "1.15.4", "single-maintainer exception limited to v1.15.4"); + // A reviewed metadata PR can precede later main commits. The actual release + // approval still binds to the newest exact main SHA (not the stale PR SHA). + const comparison = await getJson(api+"/compare/"+ + encodeURIComponent(pr.merge_commit_sha)+"..."+encodeURIComponent(mainSha),repoToken,http); + must(["ahead","identical"].includes(comparison.status) && comparison.behind_by === 0, + "approved release PR is not an ancestor of current main"); + must(!reviews.some(r=>r.state === "CHANGES_REQUESTED"), + "unresolved PR change request forbids review waiver"); + } else { + must(pr.merge_commit_sha === mainSha, "release PR merge digest differs from current HEAD"); + must(hasIndependentApproval(pr,reviews,eligible), + "independent write-authorized APPROVED review bound to exact PR head required"); + } + return {version:command.version,sha:mainSha,pr:pr.number,review_mode:waiver?"single-maintainer":"independent"}; } async function main() { const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH,"utf8")); From 5ba4ff989c87c27d9c38c6e601079535728a9ba1 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:29:40 -0700 Subject: [PATCH 2/3] test(release): cover explicit single-maintainer waiver and stale main rejection --- tests/release/verify-approved-release.test.js | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/release/verify-approved-release.test.js b/tests/release/verify-approved-release.test.js index 7eabaa0b..19f21455 100644 --- a/tests/release/verify-approved-release.test.js +++ b/tests/release/verify-approved-release.test.js @@ -13,6 +13,7 @@ const b64 = data => ({encoding:"base64",content:Buffer.from(JSON.stringify(data) function http(uri) { if (uri.includes("/pulls/51/files")) return Promise.resolve(ok([{filename:"package.json",status:"modified"},{filename:"CHANGELOG.md",status:"modified"}])); if (uri.includes("/pulls/51/reviews")) return Promise.resolve(ok(reviews)); + if (uri.includes("/compare/")) return Promise.resolve(ok({status:"ahead",ahead_by:1,behind_by:0})); if (uri.includes("/collaborators/separate-reviewer/permission")) return Promise.resolve(ok({permission:"write"})); if (uri.includes("/collaborators/security-reviewer/permission")) return Promise.resolve(ok({permission:"write"})); if (uri.endsWith("/pulls/51")) return Promise.resolve(ok(pr)); @@ -53,7 +54,7 @@ test("governance actor, workflow and expiry proofs fail closed",()=>{ assert.throws(()=>validateGovernance(decision,{...waitpoint,expires_at:"not-a-time"},sha)); }); test("exact version, SHA, independent reviewer and governance gate pass",async()=>{ - assert.deepEqual(await verifyApproval(opts()),{version:"1.15.4",sha,pr:51}); + assert.deepEqual(await verifyApproval(opts()),{version:"1.15.4",sha,pr:51,review_mode:"independent"}); }); test("nonowner, missing governance token, drift and unmerged PR fail closed",async()=>{ await assert.rejects(()=>verifyApproval({...opts(),event:{...event,sender:{login:"attacker"}}})); @@ -95,7 +96,7 @@ const manualEvent = { }; test("manual publishing uses exactly the same independent review and release gates",async()=>{ const result = await verifyApproval({...opts(),event:manualEvent,eventName:"workflow_dispatch"}); - assert.deepEqual(result,{version:"1.15.4",sha,pr:51}); + assert.deepEqual(result,{version:"1.15.4",sha,pr:51,review_mode:"independent"}); }); test("manual publish cannot bypass authorization via version, SHA, PR or actor drift",async()=>{ for (const inputs of [ @@ -116,3 +117,42 @@ test("manual publish cannot bypass authorization via version, SHA, PR or actor d test("unknown release event never becomes approval",async()=>{ await assert.rejects(()=>verifyApproval({...opts(),event:manualEvent,eventName:"push"})); }); + +test("single maintainer release exception is explicit, version-bound and ancestry-checked", async () => { + const waiver = {...decision,selected_option:"approve-single-maintainer", + options:["approve","reject","approve-single-maintainer"], + rationale:"Single-maintainer review waiver for cortex-agent v1.15.4; user approved this exception after CI source audit"}; + assert.equal(validateGovernance(waiver,waitpoint,sha),"release:cortex-agent@"+sha); + const run=await verifyApproval({...opts(),http:uri=>{ + if (uri.includes("/decisions/")) return Promise.resolve(ok(b64(waiver))); + if (uri.includes("/pulls/51/reviews")) return Promise.resolve(ok([])); + return http(uri); + }}); + assert.deepEqual(run,{version:"1.15.4",sha,pr:51,review_mode:"single-maintainer"}); + await assert.rejects(()=>verifyApproval({...opts(),http:uri=>{ + if (uri.includes("/decisions/")) return Promise.resolve(ok(b64(waiver))); + if (uri.includes("/pulls/51/reviews")) return Promise.resolve(ok([])); + if (uri.includes("/compare/")) return Promise.resolve(ok({status:"diverged",ahead_by:2,behind_by:1})); + return http(uri); + }}),/not an ancestor/); +}); +test("waiver cannot be implicitly inferred from 'approve' or rationale alone",()=>{ + assert.throws(()=>validateGovernance({...decision,selected_option:"approve-single-maintainer",options:["approve","reject"], + rationale:"single-maintainer v1.15.4"},waitpoint,sha)); + assert.throws(()=>validateGovernance({...decision,selected_option:"approve-single-maintainer",options:["approve","reject","approve-single-maintainer"], + rationale:"v1.15.4"},waitpoint,sha)); + assert.throws(()=>validateGovernance({...decision,selected_option:"approve-single-maintainer",options:["approve","reject","approve-single-maintainer"], + rationale:"single-maintainer but for v1.15.5"},waitpoint,sha)); +}); +test("waiver does not allow unanswered change-requested reviews",async()=>{ + const waiver = {...decision,selected_option:"approve-single-maintainer", + options:["approve","reject","approve-single-maintainer"], + rationale:"single-maintainer v1.15.4 explicit review exception"}; + await assert.rejects(()=>verifyApproval({...opts(),http:uri=>{ + if (uri.includes("/decisions/")) return Promise.resolve(ok(b64(waiver))); + if (uri.includes("/pulls/51/reviews")) return Promise.resolve(ok([{ + state:"CHANGES_REQUESTED",user:{login:"different-reviewer"},commit_id:pr.head.sha + }])); + return http(uri); + }}),/change request/); +}); From 18dd3b4f223d5df7a08b8e4fd411a92efb077378 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:30:06 -0700 Subject: [PATCH 3/3] docs(release): document v1.15.4 resource-bound single-maintainer exception --- docs/releasing.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/releasing.md b/docs/releasing.md index 6d33fcb5..8dfc1421 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -8,12 +8,42 @@ Workflow: The existing workflow supports manual `workflow_dispatch` **and** a strictly gated owner-command trigger (`issue_comment`). Merging to `main` only starts automatic read-only preflight; it never publishes by itself. +## Single-maintainer exception — v1.15.4 only + +A project with one authorized repository maintainer may release **v1.15.4 only** +without inventing a second GitHub reviewer, but this is **not** an implicit +override of CI or governance approvals: + +1. The canonical private governance `/release` Decision must be genuinely + `approved`, `resolved_by=interactive-user`, `workflow_gate=user`, + `type=approval`, selected option **`approve-single-maintainer`**, and + `options` must explicitly include that option. Its `rationale` must mention + `single-maintainer` and `v1.15.4`. +2. A matching `released` Waitpoint must be released by `/release` with + `workflow_gate=owner`. Both records must bind to the **actual current main** + using `release:cortex-agent@`; prior 1.15.0 release records are invalid + for this candidate. +3. The release-preparation PR must really have merged, modify only reviewed + metadata/verification paths, and its merge SHA must be an ancestor of current + main. It need not equal current main when later safe fixes landed. Any + `CHANGES_REQUESTED` review blocks the waiver. +4. The release-workflow preflight, exact-head tests, packaging, npm/tag + collision checks and effect-time authority checks remain mandatory. + This exception does not change GitHub permissions, npm Trusted Publishing + or the Auto Release opt-in variable. +5. Future versions require independent reviewer approval unless a separately + reviewed policy explicitly extends this exception. A chat message or PR + comment does not create authoritative Decision/Waitpoint records. + +The existing independent-review path remains the default when selected option +is `approve`. **Do not falsify an APPROVED GitHub Review to activate this mode.** + ## Approval-driven release automation (opt-in) - **Automatic preflight:** `.github/workflows/npm-release-preflight.yml` runs after version-file changes land on `main`. This workflow has read-only permissions, runs architecture/contract tests and `npm pack --dry-run`, and never publishes. - **Publish trigger:** After release-preparation PR merges, the project owner posts the exact command below as a new comment **on the merged release-preparation PR**. The existing trusted workflow `npm-release.yml` then runs without clicking Actions / Run workflow. - **Activation prerequisite:** configure repository variable `CORTEX_AUTO_RELEASE_ENABLED=true` **only after** configuring a read-only credential in secret `CORTEX_GOVERNANCE_READ_TOKEN` capable of reading the private `Kucell/cortex-agent-agent` Decision/Waitpoint files. Prefer a short-lived GitHub App installation token; do not use npm tokens or put credentials in versioned files. -- The release gate must verify: repository owner as commenter, merged release-prep PR, PR merged commit equals current `main`, strictly allowed metadata/test files, distinct independently approving reviewer **with repository write/maintain/admin access** on the exact source PR SHA and no outstanding change request, matching **interactive-user-approved release Decision** and **Owner-released Waitpoint** in the canonical private governance main bound to `release:cortex-agent@`, non-expired gate, and the package version matching the requested version. Recheck Git main, release tag and private governance approval immediately before the npm publish side effect. +- The release gate must verify the exact main commit, actual merged release-preparation PR (matching main or an ancestor in the explicitly approved v1.15.4 single-maintainer mode), accepted file scope, private interactive-user-approved Decision and owner-released Waitpoint for the exact main, and current package version. An ordinary approval requires a distinct write-authorized reviewer; the explicit v1.15.4 `approve-single-maintainer` mode waives only this reviewer identity requirement. Active `CHANGES_REQUESTED` is never waived. - **Command format:** `/cortex-release publish v1.15.4 decision=D-release-... waitpoint=WP-release-...`. Substitute the actual IDs created by the authoritative `/release` owner after the exact-commit dry-run / approval process. - Failure to verify anything (including unavailable private governance credential) blocks publication; no fallback to simulated approvals or `--force`. - Publisher remains `npm-release.yml` with npm Trusted Publishing/OIDC. Automatic approvals use `release_type=current`, `dist_tag=latest` and the existing validations/retry logic. **Manual dispatch remains available for validation and governed publication; `publish=true` is no longer an authorization bypass.** Both manual publish and owner-comment publish require the same live private Decision + released Waitpoint, independent write-authorized review, and exact-head verification.