mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-20 07:43:07 +08:00
Add release approval gate
This commit is contained in:
committed by
Affaan Mustafa
parent
2c0d226439
commit
9819626459
@@ -85,7 +85,7 @@ As of 2026-05-19:
|
|||||||
current May 19 queue-zero state, canonical ECC identity merge, release video
|
current May 19 queue-zero state, canonical ECC identity merge, release video
|
||||||
suite gate, partner/sponsor/talk outreach pack, owner approval packet
|
suite gate, partner/sponsor/talk outreach pack, owner approval packet
|
||||||
(`owner-approval-packet-2026-05-19.md`), preview-pack smoke digest
|
(`owner-approval-packet-2026-05-19.md`), preview-pack smoke digest
|
||||||
`790430aef4a8`, local 2560-test suite, PR #2001 merge and GitHub Actions run
|
`531328aaaa53`, local 2560-test suite, PR #2001 merge and GitHub Actions run
|
||||||
`26102500291` success, PR #2002's owner-approval dashboard gate refresh and
|
`26102500291` success, PR #2002's owner-approval dashboard gate refresh and
|
||||||
GitHub Actions run `26103853507`, PR #2004's Linear readiness evidence sync
|
GitHub Actions run `26103853507`, PR #2004's Linear readiness evidence sync
|
||||||
and GitHub Actions run `26105012698`, plus PR #2005's post-PR #2004
|
and GitHub Actions run `26105012698`, plus PR #2005's post-PR #2004
|
||||||
@@ -755,7 +755,7 @@ is not complete unless the evidence column exists and has been freshly verified.
|
|||||||
| Manage repository discussions | Repo-family discussion recheck plus response playbook | Platform audit reports 0 discussion maintainer-touch gaps and 0 answerable Q&A missing accepted answers; trunk has 59 total discussions after #2003 was routed with a maintainer response; `docs/architecture/discussion-response-playbook.md` distinguishes support, maintainer coordination, stale/concluded, release, informational, and security-sensitive response paths | Complete |
|
| Manage repository discussions | Repo-family discussion recheck plus response playbook | Platform audit reports 0 discussion maintainer-touch gaps and 0 answerable Q&A missing accepted answers; trunk has 59 total discussions after #2003 was routed with a maintainer response; `docs/architecture/discussion-response-playbook.md` distinguishes support, maintainer coordination, stale/concluded, release, informational, and security-sensitive response paths | Complete |
|
||||||
| Manage PR discussions | PR review/comment closure plus merge/close state | ECC #1990-#2011 merged through the harness audit, canonical identity, release video suite, growth outreach, evidence refresh, visual QA, suite-count, owner-approval packet, owner-approval dashboard gate, Linear readiness evidence, supply-chain evidence gate, per-project Claude Code adapter, continuous-learning project-registry hygiene, and GateGuard quoted git introspection batch; no open tracked PRs remain | Complete |
|
| Manage PR discussions | PR review/comment closure plus merge/close state | ECC #1990-#2011 merged through the harness audit, canonical identity, release video suite, growth outreach, evidence refresh, visual QA, suite-count, owner-approval packet, owner-approval dashboard gate, Linear readiness evidence, supply-chain evidence gate, per-project Claude Code adapter, continuous-learning project-registry hygiene, and GateGuard quoted git introspection batch; no open tracked PRs remain | Complete |
|
||||||
| Salvage useful stale work | `docs/stale-pr-salvage-ledger.md` plus `docs/legacy-artifact-inventory.md` | Ledger records salvaged, superseded, skipped, and manual-review tails; #1815-#1818 added cost tracking, skill scout, frontend design guidance, code-reviewer false-positive guardrails, and the May 12 gap pass; #1687, #1609, #1563, #1564, and #1565 localization tails are attached to Linear ITO-55 for language-owner review and no automatic import remains release-blocking | Complete; repeat legacy scan before release |
|
| Salvage useful stale work | `docs/stale-pr-salvage-ledger.md` plus `docs/legacy-artifact-inventory.md` | Ledger records salvaged, superseded, skipped, and manual-review tails; #1815-#1818 added cost tracking, skill scout, frontend design guidance, code-reviewer false-positive guardrails, and the May 12 gap pass; #1687, #1609, #1563, #1564, and #1565 localization tails are attached to Linear ITO-55 for language-owner review and no automatic import remains release-blocking | Complete; repeat legacy scan before release |
|
||||||
| ECC 2.0 preview pack ready | Release docs, quickstart, publication readiness, release notes | `docs/releases/2.0.0-rc.1/` and readiness docs are in-tree; May 19 evidence records queue-zero state, canonical ECC identity, release video suite, growth outreach pack, owner approval packet, local 2560-test suite, PR #2001 merge and GitHub Actions run `26102500291`, PR #2002 owner-approval dashboard gate refresh and GitHub Actions run `26103853507`, PR #2004 Linear readiness evidence sync and GitHub Actions run `26105012698`, PR #2008 supply-chain evidence gate CI run `26108473648`, post-PR #2006 main CI run `26109953093`, PR #2009 project-registry hygiene GitHub Actions run `26111313938`, post-PR #2009 main CI run `26111946778`, post-PR #2011 GateGuard main CI run `26113695068`, May 19 operator dashboard, `owner-approval-packet-2026-05-19.md`, and preview-pack smoke digest `790430aef4a8` | Needs final release approval |
|
| ECC 2.0 preview pack ready | Release docs, quickstart, publication readiness, release notes | `docs/releases/2.0.0-rc.1/` and readiness docs are in-tree; May 19 evidence records queue-zero state, canonical ECC identity, release video suite, growth outreach pack, owner approval packet, local 2560-test suite, PR #2001 merge and GitHub Actions run `26102500291`, PR #2002 owner-approval dashboard gate refresh and GitHub Actions run `26103853507`, PR #2004 Linear readiness evidence sync and GitHub Actions run `26105012698`, PR #2008 supply-chain evidence gate CI run `26108473648`, post-PR #2006 main CI run `26109953093`, PR #2009 project-registry hygiene GitHub Actions run `26111313938`, post-PR #2009 main CI run `26111946778`, post-PR #2011 GateGuard main CI run `26113695068`, May 19 operator dashboard, `owner-approval-packet-2026-05-19.md`, `release-approval-gate.js`, and preview-pack smoke digest `531328aaaa53` | Needs final release approval |
|
||||||
| Hermes specialized skills included safely | Hermes setup/import docs and sanitized skill surface | Hermes setup and import playbook are public; secrets stay local | Needs final release review |
|
| Hermes specialized skills included safely | Hermes setup/import docs and sanitized skill surface | Hermes setup and import playbook are public; secrets stay local | Needs final release review |
|
||||||
| Naming and rename readiness | Naming matrix across package/plugin/docs/social surfaces | `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` records current package, repo, Claude plugin, Codex plugin, OpenCode, and npm availability evidence | Complete for rc.1; post-rc rename remains future work |
|
| Naming and rename readiness | Naming matrix across package/plugin/docs/social surfaces | `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` records current package, repo, Claude plugin, Codex plugin, OpenCode, and npm availability evidence | Complete for rc.1; post-rc rename remains future work |
|
||||||
| Claude and Codex plugin publication | Contact/submission path with required artifacts and status | Publication readiness, naming matrix, and May 12 dry-run evidence document plugin validation, clean-checkout Claude tag/install smoke, and Codex marketplace CLI shape | Needs explicit approval for real tag/push and marketplace submission |
|
| Claude and Codex plugin publication | Contact/submission path with required artifacts and status | Publication readiness, naming matrix, and May 12 dry-run evidence document plugin validation, clean-checkout Claude tag/install smoke, and Codex marketplace CLI shape | Needs explicit approval for real tag/push and marketplace submission |
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
- verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1`
|
- verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1`
|
||||||
- verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold
|
- verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold
|
||||||
- complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post
|
- complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post
|
||||||
|
- run `npm run release:approval-gate -- --format json` after owner approvals
|
||||||
|
and live URL readbacks are recorded; it must return ready true before any
|
||||||
|
publish, upload, social, or outbound action
|
||||||
- rerun the release name/plugin publication checklist before creating a
|
- rerun the release name/plugin publication checklist before creating a
|
||||||
GitHub prerelease, publishing npm, pushing Claude plugin tags, recording the
|
GitHub prerelease, publishing npm, pushing Claude plugin tags, recording the
|
||||||
Codex marketplace path, or posting public copy
|
Codex marketplace path, or posting public copy
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ Source commit for the clean evidence baseline this packet extends:
|
|||||||
| Evidence | Current recorded state | Repeat before approval |
|
| Evidence | Current recorded state | Repeat before approval |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Platform audit | ready true, 0 open PRs, 0 open issues, 0 discussion gaps, 0 dirty files | yes |
|
| Platform audit | ready true, 0 open PRs, 0 open issues, 0 discussion gaps, 0 dirty files | yes |
|
||||||
| Preview pack smoke | ready true, digest `790430aef4a8`, 5/5 checks | yes |
|
| Preview pack smoke | ready true, digest `531328aaaa53`, 5/5 checks | yes |
|
||||||
|
| Release approval gate | ready false, digest `ef8f49f727b7`, 4/6 checks pass; owner decisions and live URL readbacks pending | yes |
|
||||||
| Video suite | ready true, 15/15 source assets, 13/13 suite artifacts, 12/12 publish candidates | yes |
|
| Video suite | ready true, 15/15 source assets, 13/13 suite artifacts, 12/12 publish candidates | yes |
|
||||||
| Release surface tests | 27/27 passed after this packet was added | yes |
|
| Release surface tests | 27/27 passed after this packet was added | yes |
|
||||||
| Full local suite | 2560/2560 passed after PR #2011 was prepared; focused GateGuard regression passed 91/91 again on current `main` | yes |
|
| Full local suite | 2560/2560 passed after PR #2011 was prepared; focused GateGuard regression passed 91/91 again on current `main` | yes |
|
||||||
@@ -56,6 +57,7 @@ Run these from the exact release commit before approving publication:
|
|||||||
git status --short --branch
|
git status --short --branch
|
||||||
node scripts/platform-audit.js --json
|
node scripts/platform-audit.js --json
|
||||||
npm run preview-pack:smoke -- --format json
|
npm run preview-pack:smoke -- --format json
|
||||||
|
npm run release:approval-gate -- --format json
|
||||||
npm run release:video-suite -- --format json
|
npm run release:video-suite -- --format json
|
||||||
npm run harness:adapters -- --check
|
npm run harness:adapters -- --check
|
||||||
npm run harness:audit -- --format json
|
npm run harness:audit -- --format json
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ surfaces, or posting announcements.
|
|||||||
| `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` |
|
| `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` |
|
||||||
| `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --json` |
|
| `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --json` |
|
||||||
| `scripts/preview-pack-smoke.js` | Deterministic preview-pack smoke gate | Verified by `npm run preview-pack:smoke` |
|
| `scripts/preview-pack-smoke.js` | Deterministic preview-pack smoke gate | Verified by `npm run preview-pack:smoke` |
|
||||||
|
| `scripts/release-approval-gate.js` | Final owner-decision, live-URL, and launch-copy gate | Must return ready true before any release publish, package publish, plugin tag, video upload, announcement, or outbound batch |
|
||||||
| `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication |
|
| `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication |
|
||||||
| `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch |
|
| `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch |
|
||||||
| `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions |
|
| `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions |
|
||||||
@@ -25,7 +26,7 @@ surfaces, or posting announcements.
|
|||||||
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-16.md` | Current May 16/17 queue cleanup, recsys skill merge, GateGuard triage, PR #1947 supply-chain protection, AgentShield #87 plugin-cache confidence evidence, AgentShield #88 evidence-pack inspect/readback, AgentShield #89 evidence-pack fleet routing, AgentShield #90 fleet review items, AgentShield #91 policy export, AgentShield #92 policy promotion, ECC-Tools #76 fleet-summary consumption, ECC-Tools #77 hosted finding evidence paths, ECC-Tools #78 harness policy-route linking, dashboard refresh, and combined Node/Rust/release-surface gate evidence through the May 16 mirror | Must still be repeated from a strict clean checkout before real publication |
|
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-16.md` | Current May 16/17 queue cleanup, recsys skill merge, GateGuard triage, PR #1947 supply-chain protection, AgentShield #87 plugin-cache confidence evidence, AgentShield #88 evidence-pack inspect/readback, AgentShield #89 evidence-pack fleet routing, AgentShield #90 fleet review items, AgentShield #91 policy export, AgentShield #92 policy promotion, ECC-Tools #76 fleet-summary consumption, ECC-Tools #77 hosted finding evidence paths, ECC-Tools #78 harness policy-route linking, dashboard refresh, and combined Node/Rust/release-surface gate evidence through the May 16 mirror | Must still be repeated from a strict clean checkout before real publication |
|
||||||
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-17.md` | May 17 queue-zero state, Japanese localization merge, Dependabot TypeScript and Node type merges, post-merge ja-JP lint repair, Mini Shai-Hulud/TanStack protection recheck, npm audit/signature checks, legacy and Linear progress routing, deterministic preview-pack smoke, operator dashboard refresh, Linear sync, and GitHub CI evidence for `27dc2918` | Superseded by the May 18 evidence snapshot; repeat from a strict clean checkout before real publication |
|
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-17.md` | May 17 queue-zero state, Japanese localization merge, Dependabot TypeScript and Node type merges, post-merge ja-JP lint repair, Mini Shai-Hulud/TanStack protection recheck, npm audit/signature checks, legacy and Linear progress routing, deterministic preview-pack smoke, operator dashboard refresh, Linear sync, and GitHub CI evidence for `27dc2918` | Superseded by the May 18 evidence snapshot; repeat from a strict clean checkout before real publication |
|
||||||
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-18.md` | May 18 queue-zero state, #1970/#1971/#1972 merge batch, #1978 review/closure, supply-chain recheck, AgentShield evidence mirror, Linear sync, current-head CI/security scan success for `4470e2e6`, and ITO-46 naming/plugin publication closure | Superseded by the May 19 ECC identity, video, and growth evidence snapshot |
|
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-18.md` | May 18 queue-zero state, #1970/#1971/#1972 merge batch, #1978 review/closure, supply-chain recheck, AgentShield evidence mirror, Linear sync, current-head CI/security scan success for `4470e2e6`, and ITO-46 naming/plugin publication closure | Superseded by the May 19 ECC identity, video, and growth evidence snapshot |
|
||||||
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md` | Current May 19 evidence for canonical ECC identity, release video suite, partner/sponsor/talk outreach pack, owner approval packet, May 19 operator dashboard, preview-pack smoke digest `790430aef4a8`, 2560-test local suite, PR #1998 visual QA CI success, PR #1999 dashboard evidence CI success, PR #2000 suite-count evidence success, PR #2001 owner approval packet CI success, PR #2002 owner-approval dashboard gate CI success, PR #2004 Linear readiness evidence sync CI success, PR #2008 supply-chain evidence gate CI success, post-PR #2006 main CI success, PR #2009 project-registry hygiene CI success, post-PR #2009 main CI success, post-PR #2011 GateGuard CI success, and the May 19 Linear sync document | Current strongest readiness snapshot; must still be repeated from a strict clean checkout before real publication |
|
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md` | Current May 19 evidence for canonical ECC identity, release video suite, partner/sponsor/talk outreach pack, owner approval packet, release approval gate, May 19 operator dashboard, preview-pack smoke digest `531328aaaa53`, 2560-test local suite, PR #1998 visual QA CI success, PR #1999 dashboard evidence CI success, PR #2000 suite-count evidence success, PR #2001 owner approval packet CI success, PR #2002 owner-approval dashboard gate CI success, PR #2004 Linear readiness evidence sync CI success, PR #2008 supply-chain evidence gate CI success, post-PR #2006 main CI success, PR #2009 project-registry hygiene CI success, post-PR #2009 main CI success, post-PR #2011 GateGuard CI success, and the May 19 Linear sync document | Current strongest readiness snapshot; must still be repeated from a strict clean checkout before real publication |
|
||||||
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-17.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 18 generated dashboard |
|
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-17.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 18 generated dashboard |
|
||||||
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-18.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 19 generated dashboard |
|
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-18.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 19 generated dashboard |
|
||||||
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Current prompt-to-artifact operator dashboard | Shows PR/issue/discussion/platform/supply-chain gates current and adds the current `$1,728/mo` to `$10,000/mo` hypergrowth, video owner-approval, and outbound-pack operating lanes |
|
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Current prompt-to-artifact operator dashboard | Shows PR/issue/discussion/platform/supply-chain gates current and adds the current `$1,728/mo` to `$10,000/mo` hypergrowth, video owner-approval, and outbound-pack operating lanes |
|
||||||
@@ -80,6 +81,7 @@ Run these from the exact release commit before publication:
|
|||||||
git status --short --branch
|
git status --short --branch
|
||||||
node scripts/platform-audit.js --json
|
node scripts/platform-audit.js --json
|
||||||
npm run preview-pack:smoke
|
npm run preview-pack:smoke
|
||||||
|
npm run release:approval-gate -- --format json
|
||||||
npm run release:video-suite -- --format json
|
npm run release:video-suite -- --format json
|
||||||
npm run harness:adapters -- --check
|
npm run harness:adapters -- --check
|
||||||
npm run harness:audit -- --format json
|
npm run harness:audit -- --format json
|
||||||
@@ -98,6 +100,8 @@ The preview pack is assembled, but publication is still blocked until these live
|
|||||||
surfaces exist and are recorded in a final evidence file:
|
surfaces exist and are recorded in a final evidence file:
|
||||||
|
|
||||||
- final release URL ledger regenerated from the intended release commit;
|
- final release URL ledger regenerated from the intended release commit;
|
||||||
|
- `npm run release:approval-gate -- --format json` returning ready true after
|
||||||
|
owner approvals and live URL readbacks are recorded;
|
||||||
- final release name/plugin publication checklist rerun from the intended
|
- final release name/plugin publication checklist rerun from the intended
|
||||||
release commit;
|
release commit;
|
||||||
- GitHub prerelease `v2.0.0-rc.1`;
|
- GitHub prerelease `v2.0.0-rc.1`;
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ Tracked repositories in the platform audit were:
|
|||||||
| Gate | Command | Result |
|
| Gate | Command | Result |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Release-surface tests | `node tests/docs/ecc2-release-surface.test.js` | 27 passed, 0 failed |
|
| Release-surface tests | `node tests/docs/ecc2-release-surface.test.js` | 27 passed, 0 failed |
|
||||||
| Preview-pack smoke | `npm run preview-pack:smoke -- --format json` | Ready true; digest `790430aef4a8`; 31 required artifacts; 5 passed, 0 failed |
|
| Preview-pack smoke | `npm run preview-pack:smoke -- --format json` | Ready true; digest `531328aaaa53`; 32 required artifacts; 5 passed, 0 failed |
|
||||||
|
| Release approval gate | `npm run release:approval-gate -- --format json` | Expected blocked; digest `ef8f49f727b7`; 4 passed, 2 failed; owner decisions and live URL readbacks remain approval-gated |
|
||||||
| Operator dashboard | `npm run operator:dashboard -- --write docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Regenerated from the May 19 `main` baseline with platform audit ready true, 0 tracked PRs, 0 tracked issues, 0 discussion gaps, `$1,728/mo` current MRR, `$10,000/mo` target MRR, the release video suite marked current, and top actions for plugin publication, notifications, outbound approval, AgentShield, and ECC Tools billing |
|
| Operator dashboard | `npm run operator:dashboard -- --write docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Regenerated from the May 19 `main` baseline with platform audit ready true, 0 tracked PRs, 0 tracked issues, 0 discussion gaps, `$1,728/mo` current MRR, `$10,000/mo` target MRR, the release video suite marked current, and top actions for plugin publication, notifications, outbound approval, AgentShield, and ECC Tools billing |
|
||||||
| Supply-chain verification | `npm audit --audit-level=moderate`; `npm audit signatures`; `yarn install --immutable --mode=skip-build` | Current supply-chain refresh found 0 npm vulnerabilities, verified 254 registry signatures and 30 attestations, and accepted the Yarn lock after pinning `@types/node@25.7.0` plus refreshing `brace-expansion` to `5.0.6` / `1.1.14` |
|
| Supply-chain verification | `npm audit --audit-level=moderate`; `npm audit signatures`; `yarn install --immutable --mode=skip-build` | Current supply-chain refresh found 0 npm vulnerabilities, verified 254 registry signatures and 30 attestations, and accepted the Yarn lock after pinning `@types/node@25.7.0` plus refreshing `brace-expansion` to `5.0.6` / `1.1.14` |
|
||||||
| Release video suite | `npm run release:video-suite -- --format json --summary` with `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT` | Ready true; 15/15 source assets present; 13/13 render, timeline, caption, EDL, and segment artifacts present; 12/12 publish-candidate outputs present with zero detected black-frame segments; primary rough render self-eval passed at 144.759 seconds, 1920x1080, 1 audio stream, and 106.78 MB |
|
| Release video suite | `npm run release:video-suite -- --format json --summary` with `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT` | Ready true; 15/15 source assets present; 13/13 render, timeline, caption, EDL, and segment artifacts present; 12/12 publish-candidate outputs present with zero detected black-frame segments; primary rough render self-eval passed at 144.759 seconds, 1920x1080, 1 audio stream, and 106.78 MB |
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ Record the exact commit SHA and command output before any publication action:
|
|||||||
| Evidence | Command | Required result | Recorded output |
|
| Evidence | Command | Required result | Recorded output |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Current May 19 baseline `bc519e5b8ed42f26c0a5a611756e04351c323f21`: `## main...origin/main`; repeat from the exact final publication commit before release |
|
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Current May 19 baseline `bc519e5b8ed42f26c0a5a611756e04351c323f21`: `## main...origin/main`; repeat from the exact final publication commit before release |
|
||||||
| Preview-pack smoke | `npm run preview-pack:smoke` | Preview pack artifacts, Hermes boundary, final verification command list, and publication blockers pass | `publication-evidence-2026-05-19.md`: ready yes, digest `790430aef4a8`, 31 artifacts, 5 passed, 0 failed; repeat in the final strict clean-checkout release pass |
|
| Preview-pack smoke | `npm run preview-pack:smoke` | Preview pack artifacts, Hermes boundary, final verification command list, and publication blockers pass | `publication-evidence-2026-05-19.md`: ready yes, digest `531328aaaa53`, 32 artifacts, 5 passed, 0 failed; repeat in the final strict clean-checkout release pass |
|
||||||
|
| Release approval gate | `npm run release:approval-gate -- --format json` | Ready true only after owner decision rows are approved, live release/package/plugin/video/billing URLs are recorded, and launch/outbound copy has no placeholders or private paths | Current May 19 state is intentionally blocked because owner decisions and live URL readbacks remain approval-gated |
|
||||||
| Harness audit | `npm run harness:audit -- --format json` | 80/80 passing | Current release gate: 80/80 across 8 applicable categories, 0 top actions |
|
| Harness audit | `npm run harness:audit -- --format json` | 80/80 passing | Current release gate: 80/80 across 8 applicable categories, 0 top actions |
|
||||||
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | Current release gate: PASS, 11 adapters |
|
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | Current release gate: PASS, 11 adapters |
|
||||||
| Observability readiness | `npm run observability:ready` | 21/21 passing | Current release gate: 21/21, ready true |
|
| Observability readiness | `npm run observability:ready` | 21/21 passing | Current release gate: 21/21, ready true |
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ npm pack --dry-run --json
|
|||||||
npm publish --tag next --dry-run
|
npm publish --tag next --dry-run
|
||||||
npm run build:opencode
|
npm run build:opencode
|
||||||
npm run preview-pack:smoke
|
npm run preview-pack:smoke
|
||||||
|
npm run release:approval-gate -- --format json
|
||||||
```
|
```
|
||||||
|
|
||||||
If a command is unavailable on the release machine, record the exact error and
|
If a command is unavailable on the release machine, record the exact error and
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ npm view ecc-universal name version dist-tags --json
|
|||||||
codex plugin marketplace add --help
|
codex plugin marketplace add --help
|
||||||
rg -n "TODO|TBD|PLACEHOLDER" docs/releases/2.0.0-rc.1
|
rg -n "TODO|TBD|PLACEHOLDER" docs/releases/2.0.0-rc.1
|
||||||
npm run preview-pack:smoke
|
npm run preview-pack:smoke
|
||||||
|
npm run release:approval-gate -- --format json
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not post the social or notification copy until the approval-gated URLs above
|
Do not post the social or notification copy until the approval-gated URLs above
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"scripts/operator-readiness-dashboard.js",
|
"scripts/operator-readiness-dashboard.js",
|
||||||
"scripts/platform-audit.js",
|
"scripts/platform-audit.js",
|
||||||
"scripts/preview-pack-smoke.js",
|
"scripts/preview-pack-smoke.js",
|
||||||
|
"scripts/release-approval-gate.js",
|
||||||
"scripts/release-video-suite.js",
|
"scripts/release-video-suite.js",
|
||||||
"scripts/hooks/",
|
"scripts/hooks/",
|
||||||
"scripts/install-apply.js",
|
"scripts/install-apply.js",
|
||||||
@@ -312,6 +313,7 @@
|
|||||||
"observability:ready": "node scripts/observability-readiness.js",
|
"observability:ready": "node scripts/observability-readiness.js",
|
||||||
"operator:dashboard": "node scripts/operator-readiness-dashboard.js",
|
"operator:dashboard": "node scripts/operator-readiness-dashboard.js",
|
||||||
"preview-pack:smoke": "node scripts/preview-pack-smoke.js",
|
"preview-pack:smoke": "node scripts/preview-pack-smoke.js",
|
||||||
|
"release:approval-gate": "node scripts/release-approval-gate.js",
|
||||||
"release:video-suite": "node scripts/release-video-suite.js",
|
"release:video-suite": "node scripts/release-video-suite.js",
|
||||||
"platform:audit": "node scripts/platform-audit.js",
|
"platform:audit": "node scripts/platform-audit.js",
|
||||||
"discussion:audit": "node scripts/discussion-audit.js",
|
"discussion:audit": "node scripts/discussion-audit.js",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const REQUIRED_ARTIFACTS = [
|
|||||||
'docs/architecture/observability-readiness.md',
|
'docs/architecture/observability-readiness.md',
|
||||||
'docs/architecture/progress-sync-contract.md',
|
'docs/architecture/progress-sync-contract.md',
|
||||||
'scripts/preview-pack-smoke.js',
|
'scripts/preview-pack-smoke.js',
|
||||||
|
'scripts/release-approval-gate.js',
|
||||||
`${RELEASE_DIR}/release-notes.md`,
|
`${RELEASE_DIR}/release-notes.md`,
|
||||||
`${RELEASE_DIR}/quickstart.md`,
|
`${RELEASE_DIR}/quickstart.md`,
|
||||||
`${RELEASE_DIR}/launch-checklist.md`,
|
`${RELEASE_DIR}/launch-checklist.md`,
|
||||||
@@ -47,6 +48,7 @@ const REQUIRED_VERIFICATION_COMMANDS = [
|
|||||||
'git status --short --branch',
|
'git status --short --branch',
|
||||||
'node scripts/platform-audit.js --json',
|
'node scripts/platform-audit.js --json',
|
||||||
'npm run preview-pack:smoke',
|
'npm run preview-pack:smoke',
|
||||||
|
'npm run release:approval-gate -- --format json',
|
||||||
'npm run release:video-suite -- --format json',
|
'npm run release:video-suite -- --format json',
|
||||||
'npm run harness:adapters -- --check',
|
'npm run harness:adapters -- --check',
|
||||||
'npm run harness:audit -- --format json',
|
'npm run harness:audit -- --format json',
|
||||||
|
|||||||
553
scripts/release-approval-gate.js
Normal file
553
scripts/release-approval-gate.js
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const RELEASE = '2.0.0-rc.1';
|
||||||
|
const RELEASE_DIR = `docs/releases/${RELEASE}`;
|
||||||
|
const SCHEMA_VERSION = 'ecc.release-approval-gate.v1';
|
||||||
|
const SCRIPT_PATH = 'scripts/release-approval-gate.js';
|
||||||
|
const OWNER_PACKET_PATH = `${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`;
|
||||||
|
const URL_LEDGER_PATH = `${RELEASE_DIR}/release-url-ledger-2026-05-19.md`;
|
||||||
|
const PREVIEW_MANIFEST_PATH = `${RELEASE_DIR}/preview-pack-manifest.md`;
|
||||||
|
const REQUIRED_COMMAND = 'npm run release:approval-gate -- --format json';
|
||||||
|
|
||||||
|
const REQUIRED_DECISIONS = [
|
||||||
|
{
|
||||||
|
id: 'github-prerelease',
|
||||||
|
label: 'GitHub prerelease',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'npm-next-publish',
|
||||||
|
label: 'npm `next` publish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-plugin-tag',
|
||||||
|
label: 'Claude plugin tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'codex-repo-marketplace',
|
||||||
|
label: 'Codex repo marketplace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ecc-tools-billing-language',
|
||||||
|
label: 'ECC Tools billing language',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'video-upload',
|
||||||
|
label: 'Video upload',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'social-and-longform',
|
||||||
|
label: 'X, LinkedIn, GitHub Discussion, longform',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'outbound-growth',
|
||||||
|
label: 'Sponsor, partner, consulting, conference, podcast outreach',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const REQUIRED_URL_SURFACES = [
|
||||||
|
{
|
||||||
|
id: 'github-prerelease-url',
|
||||||
|
label: 'GitHub prerelease URL',
|
||||||
|
exampleUrl: 'https://github.com/affaan-m/ECC/releases/tag/v2.0.0-rc.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'npm-rc-package-url',
|
||||||
|
label: 'npm rc package URL',
|
||||||
|
exampleUrl: 'https://www.npmjs.com/package/ecc-universal/v/2.0.0-rc.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-plugin-tag-url',
|
||||||
|
label: 'Claude plugin tag URL',
|
||||||
|
exampleUrl: 'https://github.com/affaan-m/ECC/releases/tag/ecc--v2.0.0-rc.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'codex-repo-marketplace-evidence',
|
||||||
|
label: 'Codex repo-marketplace evidence',
|
||||||
|
exampleUrl: 'https://github.com/affaan-m/ECC/tree/v2.0.0-rc.1/.codex-plugin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'primary-launch-video-url',
|
||||||
|
label: 'Primary launch video URL',
|
||||||
|
exampleUrl: 'https://x.com/affaanmustafa/status/0000000000000000000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'short-clip-urls',
|
||||||
|
label: 'Short clip URLs',
|
||||||
|
exampleUrl: 'https://x.com/affaanmustafa/status/0000000000000000001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ecc-tools-billing-readiness-url',
|
||||||
|
label: 'ECC Tools billing/readiness URL',
|
||||||
|
exampleUrl: 'https://github.com/ECC-Tools',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ANNOUNCEMENT_FILES = [
|
||||||
|
`${RELEASE_DIR}/release-notes.md`,
|
||||||
|
`${RELEASE_DIR}/x-thread.md`,
|
||||||
|
`${RELEASE_DIR}/linkedin-post.md`,
|
||||||
|
`${RELEASE_DIR}/article-outline.md`,
|
||||||
|
`${RELEASE_DIR}/partner-sponsor-talks-pack.md`,
|
||||||
|
'docs/business/social-launch-copy.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log([
|
||||||
|
'Usage: node scripts/release-approval-gate.js [--format <text|json>] [--root <dir>]',
|
||||||
|
'',
|
||||||
|
'Final approval gate for ECC 2.0 rc.1 publication and outbound actions.',
|
||||||
|
'',
|
||||||
|
'Options:',
|
||||||
|
' --format <text|json> Output format (default: text)',
|
||||||
|
' --json Alias for --format json',
|
||||||
|
' --root <dir> Repository root to inspect (default: cwd)',
|
||||||
|
' --help, -h Show this help',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readArgValue(args, index, flagName) {
|
||||||
|
const value = args[index + 1];
|
||||||
|
if (!value || value.startsWith('--')) {
|
||||||
|
throw new Error(`${flagName} requires a value`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = argv.slice(2);
|
||||||
|
const parsed = {
|
||||||
|
format: 'text',
|
||||||
|
help: false,
|
||||||
|
root: path.resolve(process.cwd()),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const arg = args[index];
|
||||||
|
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
parsed.help = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--json') {
|
||||||
|
parsed.format = 'json';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--format') {
|
||||||
|
parsed.format = readArgValue(args, index, arg).toLowerCase();
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--format=')) {
|
||||||
|
parsed.format = arg.slice('--format='.length).toLowerCase();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--root') {
|
||||||
|
parsed.root = path.resolve(readArgValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--root=')) {
|
||||||
|
parsed.root = path.resolve(arg.slice('--root='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['text', 'json'].includes(parsed.format)) {
|
||||||
|
throw new Error(`Invalid format: ${parsed.format}. Use text or json.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readText(rootDir, relativePath) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
|
||||||
|
} catch (_error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileExists(rootDir, relativePath) {
|
||||||
|
return fs.existsSync(path.join(rootDir, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeParseJson(text) {
|
||||||
|
if (!text.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLabel(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/[`*_]/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeState(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/[`*_]/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMarkdownRow(row) {
|
||||||
|
const trimmed = row.trim();
|
||||||
|
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed
|
||||||
|
.slice(1, -1)
|
||||||
|
.split('|')
|
||||||
|
.map(cell => cell.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDecisionRegister(packet) {
|
||||||
|
const decisions = new Map();
|
||||||
|
|
||||||
|
for (const line of packet.split('\n')) {
|
||||||
|
const cells = splitMarkdownRow(line);
|
||||||
|
if (cells.length < 4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [decision, state] = cells;
|
||||||
|
const normalizedDecision = normalizeLabel(decision);
|
||||||
|
if (
|
||||||
|
!normalizedDecision
|
||||||
|
|| normalizedDecision === 'decision'
|
||||||
|
|| /^-+$/.test(normalizedDecision)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
decisions.set(normalizedDecision, normalizeState(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
return decisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isApproved(state) {
|
||||||
|
return state === 'approve' || state === 'approved';
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineNumberForIndex(text, index) {
|
||||||
|
return text.slice(0, index).split('\n').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAnnouncementOffenders(rootDir, relativePaths) {
|
||||||
|
const offenders = [];
|
||||||
|
const privatePathPattern = /\/Users\/(?!\.\.\.)[A-Za-z0-9._-]+|\/home\/(?!user|runner)[A-Za-z0-9._-]+/g;
|
||||||
|
const anglePlaceholderPattern = /<(?!(?:https?:\/\/|mailto:|#))[^>\n]*(?:url|link|todo|tbd|placeholder)[^>\n]*>/gi;
|
||||||
|
const barePlaceholderPattern = /\bTODO\b|\bTBD\b|\bPLACEHOLDER\b/g;
|
||||||
|
|
||||||
|
for (const relativePath of relativePaths) {
|
||||||
|
const text = readText(rootDir, relativePath);
|
||||||
|
if (!text) {
|
||||||
|
offenders.push({
|
||||||
|
path: relativePath,
|
||||||
|
line: 1,
|
||||||
|
marker: 'missing file',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const match of text.matchAll(privatePathPattern)) {
|
||||||
|
offenders.push({
|
||||||
|
path: relativePath,
|
||||||
|
line: lineNumberForIndex(text, match.index),
|
||||||
|
marker: match[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const match of text.matchAll(anglePlaceholderPattern)) {
|
||||||
|
offenders.push({
|
||||||
|
path: relativePath,
|
||||||
|
line: lineNumberForIndex(text, match.index),
|
||||||
|
marker: match[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const match of text.matchAll(barePlaceholderPattern)) {
|
||||||
|
offenders.push({
|
||||||
|
path: relativePath,
|
||||||
|
line: lineNumberForIndex(text, match.index),
|
||||||
|
marker: match[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return offenders;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ledgerBlockers(ledger) {
|
||||||
|
const blockers = [];
|
||||||
|
|
||||||
|
if (/^##\s+Approval-Gated URLs\s*$/im.test(ledger)) {
|
||||||
|
blockers.push('approval-gated URL section still present');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [pattern, label] of [
|
||||||
|
[/not published yet/i, 'not-published marker still present'],
|
||||||
|
[/must return/i, 'must-return readback marker still present'],
|
||||||
|
[/Gate before use/i, 'gate-before-use column still present'],
|
||||||
|
[/\bpending\b/i, 'pending marker still present'],
|
||||||
|
[/\bblocked\b/i, 'blocked marker still present'],
|
||||||
|
]) {
|
||||||
|
if (pattern.test(ledger)) {
|
||||||
|
blockers.push(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCheck(id, status, evidence, fix) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
evidence,
|
||||||
|
fix: status === 'pass' ? '' : fix,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function topActionsForChecks(checks) {
|
||||||
|
const actions = [];
|
||||||
|
const failedIds = new Set(checks.filter(check => check.status !== 'pass').map(check => check.id));
|
||||||
|
|
||||||
|
if (failedIds.has('release-approval-script-registered')) {
|
||||||
|
actions.push('Wire release:approval-gate into package.json, package files, and the preview-pack manifest.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.has('owner-decisions-approved')) {
|
||||||
|
actions.push('Approve, defer, or block each owner decision row explicitly after final evidence is rerun from the release commit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.has('release-url-ledger-finalized')) {
|
||||||
|
actions.push('Replace approval-gated URL ledger rows with live readback URLs from the approved release, package, plugin, video, and billing surfaces.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.has('final-evidence-command-listed')) {
|
||||||
|
actions.push('Add release:approval-gate to the final evidence command lists before asking for publication approval.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.has('announcement-copy-finalized')) {
|
||||||
|
actions.push('Remove unresolved placeholders and private local paths from launch, social, and outbound copy.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.has('public-action-guard-present')) {
|
||||||
|
actions.push('Restore the explicit no-outbound/no-publish authorization boundary in the owner packet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReport(options = {}) {
|
||||||
|
const rootDir = path.resolve(options.root || process.cwd());
|
||||||
|
const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
|
||||||
|
const packageScripts = packageJson.scripts || {};
|
||||||
|
const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : [];
|
||||||
|
const ownerPacket = readText(rootDir, OWNER_PACKET_PATH);
|
||||||
|
const ledger = readText(rootDir, URL_LEDGER_PATH);
|
||||||
|
const manifest = readText(rootDir, PREVIEW_MANIFEST_PATH);
|
||||||
|
const decisions = parseDecisionRegister(ownerPacket);
|
||||||
|
|
||||||
|
const missingDecisions = [];
|
||||||
|
const unapprovedDecisions = [];
|
||||||
|
for (const decision of REQUIRED_DECISIONS) {
|
||||||
|
const state = decisions.get(normalizeLabel(decision.label));
|
||||||
|
if (!state) {
|
||||||
|
missingDecisions.push(decision.label);
|
||||||
|
} else if (!isApproved(state)) {
|
||||||
|
unapprovedDecisions.push(`${decision.label}=${state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingUrlSurfaces = REQUIRED_URL_SURFACES
|
||||||
|
.filter(surface => !ledger.includes(surface.label))
|
||||||
|
.map(surface => surface.label);
|
||||||
|
const urlBlockers = ledgerBlockers(ledger);
|
||||||
|
const announcementOffenders = findAnnouncementOffenders(rootDir, ANNOUNCEMENT_FILES);
|
||||||
|
const commandListedIn = [
|
||||||
|
ownerPacket.includes(REQUIRED_COMMAND) ? OWNER_PACKET_PATH : '',
|
||||||
|
ledger.includes(REQUIRED_COMMAND) ? URL_LEDGER_PATH : '',
|
||||||
|
manifest.includes(REQUIRED_COMMAND) ? PREVIEW_MANIFEST_PATH : '',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
makeCheck(
|
||||||
|
'release-approval-script-registered',
|
||||||
|
packageScripts['release:approval-gate'] === `node ${SCRIPT_PATH}`
|
||||||
|
&& packageFiles.includes(SCRIPT_PATH)
|
||||||
|
&& fileExists(rootDir, SCRIPT_PATH)
|
||||||
|
&& manifest.includes(`\`${SCRIPT_PATH}\``)
|
||||||
|
&& manifest.includes(REQUIRED_COMMAND)
|
||||||
|
? 'pass'
|
||||||
|
: 'fail',
|
||||||
|
'package script, npm package file entry, local script, and preview-pack manifest reference',
|
||||||
|
'Add release:approval-gate to package scripts, package files, and preview-pack-manifest.md.'
|
||||||
|
),
|
||||||
|
makeCheck(
|
||||||
|
'owner-decisions-approved',
|
||||||
|
missingDecisions.length === 0 && unapprovedDecisions.length === 0 ? 'pass' : 'fail',
|
||||||
|
missingDecisions.length === 0 && unapprovedDecisions.length === 0
|
||||||
|
? `${REQUIRED_DECISIONS.length} owner decision rows are approved`
|
||||||
|
: `missing decisions: ${missingDecisions.join(', ') || 'none'}; pending decisions: ${unapprovedDecisions.join(', ') || 'none'}`,
|
||||||
|
'Set every required owner decision row to approve only after the final release evidence has been rerun.'
|
||||||
|
),
|
||||||
|
makeCheck(
|
||||||
|
'release-url-ledger-finalized',
|
||||||
|
ledger
|
||||||
|
&& missingUrlSurfaces.length === 0
|
||||||
|
&& urlBlockers.length === 0
|
||||||
|
? 'pass'
|
||||||
|
: 'fail',
|
||||||
|
ledger && missingUrlSurfaces.length === 0 && urlBlockers.length === 0
|
||||||
|
? `${REQUIRED_URL_SURFACES.length} final URL surfaces are recorded without approval-gated blockers`
|
||||||
|
: `missing URL surfaces: ${missingUrlSurfaces.join(', ') || 'none'}; blockers: ${urlBlockers.join(', ') || 'none'}`,
|
||||||
|
'Regenerate the release URL ledger after the approved publication actions and record live readback URLs.'
|
||||||
|
),
|
||||||
|
makeCheck(
|
||||||
|
'final-evidence-command-listed',
|
||||||
|
commandListedIn.length === 3 ? 'pass' : 'fail',
|
||||||
|
commandListedIn.length === 3
|
||||||
|
? `${REQUIRED_COMMAND} is listed in owner packet, URL ledger, and preview manifest`
|
||||||
|
: `${REQUIRED_COMMAND} listed in: ${commandListedIn.join(', ') || 'none'}`,
|
||||||
|
'List release:approval-gate in every final evidence command block.'
|
||||||
|
),
|
||||||
|
makeCheck(
|
||||||
|
'announcement-copy-finalized',
|
||||||
|
announcementOffenders.length === 0 ? 'pass' : 'fail',
|
||||||
|
announcementOffenders.length === 0
|
||||||
|
? `${ANNOUNCEMENT_FILES.length} launch/outbound copy files have no placeholders or private paths`
|
||||||
|
: `offenders: ${announcementOffenders.map(item => `${item.path}:${item.line}`).join(', ')}`,
|
||||||
|
'Replace placeholders with live URLs and remove private local paths from launch/outbound copy.'
|
||||||
|
),
|
||||||
|
makeCheck(
|
||||||
|
'public-action-guard-present',
|
||||||
|
ownerPacket.includes(
|
||||||
|
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement is authorized by this packet alone.'
|
||||||
|
)
|
||||||
|
? 'pass'
|
||||||
|
: 'fail',
|
||||||
|
'owner packet preserves the explicit no-public-action authorization boundary',
|
||||||
|
'Restore the owner-packet sentence that blocks outbound, posts, package publish, plugin tags, and billing announcements.'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const failed = checks.filter(check => check.status !== 'pass');
|
||||||
|
const digest = crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(JSON.stringify(checks.map(check => [check.id, check.status, check.evidence])))
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, 12);
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: SCHEMA_VERSION,
|
||||||
|
release: RELEASE,
|
||||||
|
ready: failed.length === 0,
|
||||||
|
digest,
|
||||||
|
summary: {
|
||||||
|
passed: checks.length - failed.length,
|
||||||
|
failed: failed.length,
|
||||||
|
total: checks.length,
|
||||||
|
},
|
||||||
|
top_actions: topActionsForChecks(checks),
|
||||||
|
checks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderText(report) {
|
||||||
|
const lines = [
|
||||||
|
'ECC release approval gate',
|
||||||
|
`Release: ${report.release}`,
|
||||||
|
`Ready: ${report.ready ? 'yes' : 'no'}`,
|
||||||
|
`Digest: ${report.digest}`,
|
||||||
|
'',
|
||||||
|
'Checks:',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const check of report.checks) {
|
||||||
|
lines.push(`- ${check.status} ${check.id}: ${check.evidence}`);
|
||||||
|
if (check.fix) {
|
||||||
|
lines.push(` fix: ${check.fix}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.top_actions.length > 0) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Top actions:');
|
||||||
|
for (const action of report.top_actions) {
|
||||||
|
lines.push(`- ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
lines.push(`Passed: ${report.summary.passed}`);
|
||||||
|
lines.push(`Failed: ${report.summary.failed}`);
|
||||||
|
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
let parsed;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsed = parseArgs(process.argv);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.help) {
|
||||||
|
usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = buildReport({ root: parsed.root });
|
||||||
|
|
||||||
|
if (parsed.format === 'json') {
|
||||||
|
console.log(JSON.stringify(report, null, 2));
|
||||||
|
} else {
|
||||||
|
process.stdout.write(renderText(report));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!report.ready) {
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ANNOUNCEMENT_FILES,
|
||||||
|
REQUIRED_COMMAND,
|
||||||
|
REQUIRED_DECISIONS,
|
||||||
|
REQUIRED_URL_SURFACES,
|
||||||
|
buildReport,
|
||||||
|
parseArgs,
|
||||||
|
renderText,
|
||||||
|
};
|
||||||
@@ -177,6 +177,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
|
|||||||
'skills/hermes-imports/SKILL.md',
|
'skills/hermes-imports/SKILL.md',
|
||||||
'docs/architecture/harness-adapter-compliance.md',
|
'docs/architecture/harness-adapter-compliance.md',
|
||||||
'scripts/preview-pack-smoke.js',
|
'scripts/preview-pack-smoke.js',
|
||||||
|
'scripts/release-approval-gate.js',
|
||||||
'docs/releases/2.0.0-rc.1/publication-readiness.md',
|
'docs/releases/2.0.0-rc.1/publication-readiness.md',
|
||||||
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md',
|
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md',
|
||||||
'docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md',
|
'docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md',
|
||||||
@@ -201,6 +202,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
|
|||||||
assert.ok(manifest.includes('no raw workspace exports'));
|
assert.ok(manifest.includes('no raw workspace exports'));
|
||||||
assert.ok(manifest.includes('Final Verification Commands'));
|
assert.ok(manifest.includes('Final Verification Commands'));
|
||||||
assert.ok(manifest.includes('npm run preview-pack:smoke'));
|
assert.ok(manifest.includes('npm run preview-pack:smoke'));
|
||||||
|
assert.ok(manifest.includes('npm run release:approval-gate -- --format json'));
|
||||||
assert.ok(manifest.includes('npm run release:video-suite -- --format json'));
|
assert.ok(manifest.includes('npm run release:video-suite -- --format json'));
|
||||||
assert.ok(manifest.includes('Reference-Inspired Adapter Direction'));
|
assert.ok(manifest.includes('Reference-Inspired Adapter Direction'));
|
||||||
});
|
});
|
||||||
@@ -229,6 +231,7 @@ test('owner approval packet consolidates the final gated decisions', () => {
|
|||||||
for (const command of [
|
for (const command of [
|
||||||
'node scripts/platform-audit.js --json',
|
'node scripts/platform-audit.js --json',
|
||||||
'npm run preview-pack:smoke -- --format json',
|
'npm run preview-pack:smoke -- --format json',
|
||||||
|
'npm run release:approval-gate -- --format json',
|
||||||
'npm run release:video-suite -- --format json',
|
'npm run release:video-suite -- --format json',
|
||||||
'node tests/run-all.js',
|
'node tests/run-all.js',
|
||||||
]) {
|
]) {
|
||||||
@@ -255,7 +258,7 @@ test('GA roadmap mirrors the current May 19 release evidence', () => {
|
|||||||
|
|
||||||
for (const marker of [
|
for (const marker of [
|
||||||
'owner-approval-packet-2026-05-19.md',
|
'owner-approval-packet-2026-05-19.md',
|
||||||
'preview-pack smoke digest `790430aef4a8`',
|
'preview-pack smoke digest `531328aaaa53`',
|
||||||
'local 2560-test suite',
|
'local 2560-test suite',
|
||||||
'PR #2001',
|
'PR #2001',
|
||||||
'GitHub Actions run `26102500291`',
|
'GitHub Actions run `26102500291`',
|
||||||
@@ -344,6 +347,31 @@ test('release video suite manifest gates the content launch lane', () => {
|
|||||||
assert.ok(packageJson.files.includes('scripts/release-video-suite.js'));
|
assert.ok(packageJson.files.includes('scripts/release-video-suite.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('release approval gate blocks publication until owner decisions and URLs are final', () => {
|
||||||
|
const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
|
||||||
|
const packet = read('docs/releases/2.0.0-rc.1/owner-approval-packet-2026-05-19.md');
|
||||||
|
const ledger = read('docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md');
|
||||||
|
const script = read('scripts/release-approval-gate.js');
|
||||||
|
const packageJson = JSON.parse(read('package.json'));
|
||||||
|
|
||||||
|
for (const marker of [
|
||||||
|
'ecc.release-approval-gate.v1',
|
||||||
|
'owner-decisions-approved',
|
||||||
|
'release-url-ledger-finalized',
|
||||||
|
'announcement-copy-finalized',
|
||||||
|
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement',
|
||||||
|
]) {
|
||||||
|
assert.ok(script.includes(marker), `release approval gate missing ${marker}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(manifest.includes('scripts/release-approval-gate.js'));
|
||||||
|
assert.ok(manifest.includes('npm run release:approval-gate -- --format json'));
|
||||||
|
assert.ok(packet.includes('npm run release:approval-gate -- --format json'));
|
||||||
|
assert.ok(ledger.includes('npm run release:approval-gate -- --format json'));
|
||||||
|
assert.strictEqual(packageJson.scripts['release:approval-gate'], 'node scripts/release-approval-gate.js');
|
||||||
|
assert.ok(packageJson.files.includes('scripts/release-approval-gate.js'));
|
||||||
|
});
|
||||||
|
|
||||||
test('partner sponsor talks pack gates the hypergrowth outbound lane', () => {
|
test('partner sponsor talks pack gates the hypergrowth outbound lane', () => {
|
||||||
const partnerPack = read('docs/releases/2.0.0-rc.1/partner-sponsor-talks-pack.md');
|
const partnerPack = read('docs/releases/2.0.0-rc.1/partner-sponsor-talks-pack.md');
|
||||||
const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
|
const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
|
||||||
@@ -510,6 +538,7 @@ test('release name and plugin publication checklist freezes rc.1 surfaces', () =
|
|||||||
'codex plugin marketplace add --help',
|
'codex plugin marketplace add --help',
|
||||||
'npm publish --tag next --dry-run',
|
'npm publish --tag next --dry-run',
|
||||||
'npm run preview-pack:smoke',
|
'npm run preview-pack:smoke',
|
||||||
|
'npm run release:approval-gate -- --format json',
|
||||||
]) {
|
]) {
|
||||||
assert.ok(checklist.includes(command), `release name/plugin checklist missing command ${command}`);
|
assert.ok(checklist.includes(command), `release name/plugin checklist missing command ${command}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ function buildExpectedPublishPaths(repoRoot) {
|
|||||||
"scripts/operator-readiness-dashboard.js",
|
"scripts/operator-readiness-dashboard.js",
|
||||||
"scripts/platform-audit.js",
|
"scripts/platform-audit.js",
|
||||||
"scripts/preview-pack-smoke.js",
|
"scripts/preview-pack-smoke.js",
|
||||||
|
"scripts/release-approval-gate.js",
|
||||||
"scripts/release-video-suite.js",
|
"scripts/release-video-suite.js",
|
||||||
"scripts/skill-create-output.js",
|
"scripts/skill-create-output.js",
|
||||||
"scripts/repair.js",
|
"scripts/repair.js",
|
||||||
@@ -132,6 +133,7 @@ function main() {
|
|||||||
"scripts/discussion-audit.js",
|
"scripts/discussion-audit.js",
|
||||||
"scripts/operator-readiness-dashboard.js",
|
"scripts/operator-readiness-dashboard.js",
|
||||||
"scripts/preview-pack-smoke.js",
|
"scripts/preview-pack-smoke.js",
|
||||||
|
"scripts/release-approval-gate.js",
|
||||||
"scripts/release-video-suite.js",
|
"scripts/release-video-suite.js",
|
||||||
"scripts/work-items.js",
|
"scripts/work-items.js",
|
||||||
"scripts/platform-audit.js",
|
"scripts/platform-audit.js",
|
||||||
|
|||||||
320
tests/scripts/release-approval-gate.test.js
Normal file
320
tests/scripts/release-approval-gate.test.js
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFileSync, spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'release-approval-gate.js');
|
||||||
|
const {
|
||||||
|
REQUIRED_DECISIONS,
|
||||||
|
REQUIRED_URL_SURFACES,
|
||||||
|
buildReport,
|
||||||
|
parseArgs,
|
||||||
|
renderText,
|
||||||
|
} = require(SCRIPT);
|
||||||
|
|
||||||
|
const RELEASE_DIR = 'docs/releases/2.0.0-rc.1';
|
||||||
|
|
||||||
|
function createTempDir(prefix) {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup(dirPath) {
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(rootDir, relativePath, content) {
|
||||||
|
const targetPath = path.join(rootDir, relativePath);
|
||||||
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||||
|
fs.writeFileSync(targetPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function approvedPacketContent(overrides = {}) {
|
||||||
|
const decisions = new Map(REQUIRED_DECISIONS.map(decision => [decision.label, 'approve']));
|
||||||
|
for (const [label, value] of Object.entries(overrides)) {
|
||||||
|
decisions.set(label, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'# ECC v2.0.0-rc.1 Owner Approval Packet',
|
||||||
|
'',
|
||||||
|
'## Decision Register',
|
||||||
|
'',
|
||||||
|
'| Decision | Approve / defer / block | Evidence required first | Notes |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
...REQUIRED_DECISIONS.map(decision => (
|
||||||
|
`| ${decision.label} | ${decisions.get(decision.label)} | final evidence | approved fixture |`
|
||||||
|
)),
|
||||||
|
'',
|
||||||
|
'## Final Evidence Commands',
|
||||||
|
'',
|
||||||
|
'```bash',
|
||||||
|
'npm run release:approval-gate -- --format json',
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement is authorized by this packet alone.',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalLedgerContent(extra = '') {
|
||||||
|
return [
|
||||||
|
'# ECC v2.0.0-rc.1 Release URL Ledger',
|
||||||
|
'',
|
||||||
|
'## Final Published URLs',
|
||||||
|
'',
|
||||||
|
'| Surface | URL | Verification |',
|
||||||
|
'| --- | --- | --- |',
|
||||||
|
...REQUIRED_URL_SURFACES.map(surface => (
|
||||||
|
`| ${surface.label} | ${surface.exampleUrl} | readback from final release commit |`
|
||||||
|
)),
|
||||||
|
'',
|
||||||
|
'## Final Verification Commands',
|
||||||
|
'',
|
||||||
|
'```bash',
|
||||||
|
'npm run release:approval-gate -- --format json',
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
extra,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function manifestContent() {
|
||||||
|
return [
|
||||||
|
'# ECC v2.0.0-rc.1 Preview Pack Manifest',
|
||||||
|
'',
|
||||||
|
'| Artifact | Role | Gate |',
|
||||||
|
'| --- | --- | --- |',
|
||||||
|
'| `scripts/release-approval-gate.js` | Final owner approval and live URL gate | Verified by `npm run release:approval-gate -- --format json` |',
|
||||||
|
'',
|
||||||
|
'## Final Verification Commands',
|
||||||
|
'',
|
||||||
|
'```bash',
|
||||||
|
'npm run release:approval-gate -- --format json',
|
||||||
|
'```',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedRepo(rootDir, overrides = {}) {
|
||||||
|
const files = {
|
||||||
|
'package.json': JSON.stringify({
|
||||||
|
files: ['scripts/release-approval-gate.js'],
|
||||||
|
scripts: {
|
||||||
|
'release:approval-gate': 'node scripts/release-approval-gate.js',
|
||||||
|
},
|
||||||
|
}, null, 2),
|
||||||
|
'scripts/release-approval-gate.js': 'release approval gate script',
|
||||||
|
[`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent(),
|
||||||
|
[`${RELEASE_DIR}/release-url-ledger-2026-05-19.md`]: finalLedgerContent(),
|
||||||
|
[`${RELEASE_DIR}/preview-pack-manifest.md`]: manifestContent(),
|
||||||
|
[`${RELEASE_DIR}/release-notes.md`]: 'Release notes with final URLs.',
|
||||||
|
[`${RELEASE_DIR}/x-thread.md`]: 'X post with final URLs.',
|
||||||
|
[`${RELEASE_DIR}/linkedin-post.md`]: 'LinkedIn post with final URLs.',
|
||||||
|
[`${RELEASE_DIR}/article-outline.md`]: 'Article outline with final URLs.',
|
||||||
|
[`${RELEASE_DIR}/partner-sponsor-talks-pack.md`]: 'Outbound copy with final URLs.',
|
||||||
|
'docs/business/social-launch-copy.md': 'Business launch copy with final URLs.',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
|
||||||
|
if (content === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writeFile(rootDir, relativePath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(args = [], options = {}) {
|
||||||
|
return execFileSync('node', [SCRIPT, ...args], {
|
||||||
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runProcess(args = [], options = {}) {
|
||||||
|
return spawnSync('node', [SCRIPT, ...args], {
|
||||||
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` PASS ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` FAIL ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing release-approval-gate.js ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (test('parseArgs accepts approval gate flags and rejects invalid values', () => {
|
||||||
|
const rootDir = createTempDir('release-approval-args-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = parseArgs([
|
||||||
|
'node',
|
||||||
|
'script',
|
||||||
|
'--format=json',
|
||||||
|
`--root=${rootDir}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(parsed.format, 'json');
|
||||||
|
assert.strictEqual(parsed.root, path.resolve(rootDir));
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--root']), /--root requires a value/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('seeded approved release passes every publication approval check', () => {
|
||||||
|
const rootDir = createTempDir('release-approval-pass-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir);
|
||||||
|
const report = buildReport({ root: rootDir });
|
||||||
|
|
||||||
|
assert.strictEqual(report.schema_version, 'ecc.release-approval-gate.v1');
|
||||||
|
assert.strictEqual(report.ready, true);
|
||||||
|
assert.strictEqual(report.summary.failed, 0);
|
||||||
|
assert.deepStrictEqual(report.top_actions, []);
|
||||||
|
assert.ok(report.checks.every(check => check.status === 'pass'));
|
||||||
|
|
||||||
|
const text = renderText(report);
|
||||||
|
assert.ok(text.includes('Ready: yes'));
|
||||||
|
assert.ok(text.includes('Failed: 0'));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('deferred owner decisions keep the publication gate blocked', () => {
|
||||||
|
const rootDir = createTempDir('release-approval-deferred-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir, {
|
||||||
|
[`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent({
|
||||||
|
'GitHub prerelease': 'defer',
|
||||||
|
'Sponsor, partner, consulting, conference, podcast outreach': 'block',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = buildReport({ root: rootDir });
|
||||||
|
const decisions = report.checks.find(check => check.id === 'owner-decisions-approved');
|
||||||
|
|
||||||
|
assert.strictEqual(report.ready, false);
|
||||||
|
assert.strictEqual(decisions.status, 'fail');
|
||||||
|
assert.ok(decisions.evidence.includes('GitHub prerelease=defer'));
|
||||||
|
assert.ok(decisions.evidence.includes('Sponsor, partner, consulting, conference, podcast outreach=block'));
|
||||||
|
assert.ok(report.top_actions.some(action => action.includes('Approve, defer, or block')));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('approval-gated URL ledger rows keep the publication gate blocked', () => {
|
||||||
|
const rootDir = createTempDir('release-approval-ledger-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir, {
|
||||||
|
[`${RELEASE_DIR}/release-url-ledger-2026-05-19.md`]: [
|
||||||
|
'# ECC v2.0.0-rc.1 Release URL Ledger',
|
||||||
|
'',
|
||||||
|
'## Approval-Gated URLs',
|
||||||
|
'',
|
||||||
|
'| Surface | Intended URL or command | Gate before use |',
|
||||||
|
'| --- | --- | --- |',
|
||||||
|
'| GitHub prerelease | https://github.com/affaan-m/ECC/releases/tag/v2.0.0-rc.1 | must return the prerelease |',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = buildReport({ root: rootDir });
|
||||||
|
const ledger = report.checks.find(check => check.id === 'release-url-ledger-finalized');
|
||||||
|
|
||||||
|
assert.strictEqual(report.ready, false);
|
||||||
|
assert.strictEqual(ledger.status, 'fail');
|
||||||
|
assert.ok(ledger.evidence.includes('approval-gated URL section still present'));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('announcement drafts fail on unresolved placeholders and private paths', () => {
|
||||||
|
const rootDir = createTempDir('release-approval-copy-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir, {
|
||||||
|
[`${RELEASE_DIR}/x-thread.md`]: 'Ship copy with <video-url> and /Users/affaan/raw-footage.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = buildReport({ root: rootDir });
|
||||||
|
const copy = report.checks.find(check => check.id === 'announcement-copy-finalized');
|
||||||
|
|
||||||
|
assert.strictEqual(report.ready, false);
|
||||||
|
assert.strictEqual(copy.status, 'fail');
|
||||||
|
assert.ok(copy.evidence.includes(`${RELEASE_DIR}/x-thread.md:1`));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('CLI emits json and uses status 2 for blocked approval reports', () => {
|
||||||
|
const rootDir = createTempDir('release-approval-cli-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir);
|
||||||
|
const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
|
||||||
|
const parsed = JSON.parse(stdout);
|
||||||
|
assert.strictEqual(parsed.ready, true);
|
||||||
|
|
||||||
|
writeFile(
|
||||||
|
rootDir,
|
||||||
|
`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`,
|
||||||
|
approvedPacketContent({ 'Video upload': 'defer' })
|
||||||
|
);
|
||||||
|
const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
|
||||||
|
assert.strictEqual(failedRun.status, 2);
|
||||||
|
assert.strictEqual(failedRun.stderr, '');
|
||||||
|
assert.ok(failedRun.stdout.includes('"ready": false'));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('CLI help exits successfully and invalid flags fail before reporting', () => {
|
||||||
|
const help = runProcess(['--help']);
|
||||||
|
assert.strictEqual(help.status, 0);
|
||||||
|
assert.strictEqual(help.stderr, '');
|
||||||
|
assert.ok(help.stdout.includes('Usage: node scripts/release-approval-gate.js'));
|
||||||
|
|
||||||
|
const invalid = runProcess(['--format=xml']);
|
||||||
|
assert.strictEqual(invalid.status, 1);
|
||||||
|
assert.strictEqual(invalid.stdout, '');
|
||||||
|
assert.match(invalid.stderr, /Error: Invalid format/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nPassed: ${passed}`);
|
||||||
|
console.log(`Failed: ${failed}`);
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
runTests();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user