diff --git a/.agents/pm/chores/pm-1h88.md b/.agents/pm/chores/pm-1h88.md new file mode 100644 index 000000000..4c211e86f --- /dev/null +++ b/.agents/pm/chores/pm-1h88.md @@ -0,0 +1,59 @@ +{ + "id": "pm-1h88", + "title": "Cut public release 2026.3.9", + "description": "Create final release commit/PR messaging, switch GitHub repository visibility to public, and publish @unbrained/pm-cli.", + "type": "Chore", + "status": "in_progress", + "priority": 0, + "tags": [ + "pm-cli", + "publishing", + "release", + "security" + ], + "created_at": "2026-03-09T13:02:02.787Z", + "updated_at": "2026-03-09T13:02:08.582Z", + "deadline": "2026-03-10T13:02:02.787Z", + "assignee": "maintainer-agent", + "author": "maintainer-agent", + "estimated_minutes": 90, + "acceptance_criteria": "Repository set public intentionally; release commit pushed; PR opened with final messaging; tag v2026.3.9 pushed; release workflow publishes @unbrained/pm-cli successfully; no credential leaks detected.", + "comments": [ + { + "created_at": "2026-03-09T13:02:02.787Z", + "author": "maintainer-agent", + "text": "Execute final launch operations with explicit leak checks and release guardrails." + } + ], + "notes": [ + { + "created_at": "2026-03-09T13:02:02.787Z", + "author": "maintainer-agent", + "text": "Prefer tag-driven publish workflow using release environment secret NPM_TOKEN." + } + ], + "files": [ + { + "path": ".github/workflows/release.yml", + "scope": "project", + "note": "tag-driven publish pipeline" + } + ], + "tests": [ + { + "command": "pnpm security:scan", + "scope": "project", + "timeout_seconds": 120, + "note": "final leak check before release" + } + ], + "docs": [ + { + "path": "docs/RELEASING.md", + "scope": "project", + "note": "release runbook" + } + ] +} + +Operational release execution task for today's production launch. diff --git a/.agents/pm/chores/pm-1hm2.md b/.agents/pm/chores/pm-1hm2.md new file mode 100644 index 000000000..7a9b22020 --- /dev/null +++ b/.agents/pm/chores/pm-1hm2.md @@ -0,0 +1,208 @@ +{ + "id": "pm-1hm2", + "title": "Release hardening: scoped npm + version policy + CI", + "description": "Audit for leaked private data, enforce calendar SemVer policy, and harden GitHub/npm release automation for scoped package publishing.", + "type": "Chore", + "status": "closed", + "priority": 0, + "tags": [ + "ci", + "pm-cli", + "release-readiness", + "security" + ], + "created_at": "2026-03-09T12:42:13.949Z", + "updated_at": "2026-03-09T12:56:34.722Z", + "deadline": "2026-03-10T12:42:13.949Z", + "author": "maintainer-agent", + "estimated_minutes": 180, + "acceptance_criteria": "No leaked credentials in tracked files/pm data; package/versioning follows YYYY.M.D[-N] policy; CI/release pipelines enforce policy and support @unbrained/pm-cli publishing + npx usage; contributor release docs updated.", + "comments": [ + { + "created_at": "2026-03-09T12:42:13.949Z", + "author": "maintainer-agent", + "text": "Initiate final release hardening sweep before public launch." + }, + { + "created_at": "2026-03-09T12:56:28.657Z", + "author": "maintainer-agent", + "text": "Evidence: release hardening validations passed. Commands: pnpm version:next => 2026.3.9; pnpm version:check => passed; pnpm security:scan => no credential-like secrets in tracked files (including .agents/pm tracked data); pnpm smoke:npx => passed (2026.3.9); node scripts/run-tests.mjs test -- tests/integration/ci-workflow-contract.spec.ts tests/integration/help-readme-contract.spec.ts tests/integration/release-readiness-contract.spec.ts => 48/48 passing; pnpm test => 54 files, 541 tests passing; node scripts/run-tests.mjs coverage => 54 files, 541 tests passing at 100% lines/branches/functions/statements." + } + ], + "notes": [ + { + "created_at": "2026-03-09T12:42:13.949Z", + "author": "maintainer-agent", + "text": "Implement version policy checks and release workflow guardrails." + } + ], + "files": [ + { + "path": ".github/ISSUE_TEMPLATE/bug-report.yml", + "scope": "project", + "note": "calendar version placeholder" + }, + { + "path": ".github/PULL_REQUEST_TEMPLATE.md", + "scope": "project", + "note": "new release safety checklist items" + }, + { + "path": ".github/workflows/ci.yml", + "scope": "project", + "note": "version/security/npx gates in CI" + }, + { + "path": ".github/workflows/nightly.yml", + "scope": "project", + "note": "nightly version and security checks" + }, + { + "path": ".github/workflows/release.yml", + "scope": "project", + "note": "release environment publish flow with tag guard" + }, + { + "path": "AGENTS.md", + "scope": "project", + "note": "scoped package-spec recursion examples" + }, + { + "path": "CHANGELOG.md", + "scope": "project", + "note": "release hardening changelog entries" + }, + { + "path": "CONTRIBUTING.md", + "scope": "project", + "note": "link release runbook" + }, + { + "path": "docs/RELEASING.md", + "scope": "project", + "note": "maintainer release runbook" + }, + { + "path": "package.json", + "scope": "project", + "note": "scope package + version policy" + }, + { + "path": "PRD.md", + "scope": "project", + "note": "scoped package-spec recursion examples" + }, + { + "path": "README.md", + "scope": "project", + "note": "scoped install docs and release checklist" + }, + { + "path": "scripts/check-secrets.mjs", + "scope": "project", + "note": "tracked-file secret leak scanner" + }, + { + "path": "scripts/install.ps1", + "scope": "project", + "note": "scoped installer default package" + }, + { + "path": "scripts/install.sh", + "scope": "project", + "note": "scoped installer default package" + }, + { + "path": "scripts/release-version.mjs", + "scope": "project", + "note": "calendar SemVer policy and registry sequencing checks" + }, + { + "path": "scripts/smoke-npx-from-pack.mjs", + "scope": "project", + "note": "packaged npx smoke test" + }, + { + "path": "tests/integration/ci-workflow-contract.spec.ts", + "scope": "project", + "note": "workflow contract assertions" + }, + { + "path": "tests/integration/help-readme-contract.spec.ts", + "scope": "project", + "note": "README installer and package assertions" + }, + { + "path": "tests/integration/release-readiness-contract.spec.ts", + "scope": "project", + "note": "version policy and packaging assertions" + } + ], + "tests": [ + { + "command": "node scripts/run-tests.mjs coverage", + "scope": "project", + "timeout_seconds": 1200, + "note": "full regression gate" + }, + { + "command": "node scripts/run-tests.mjs test -- tests/integration/ci-workflow-contract.spec.ts tests/integration/help-readme-contract.spec.ts tests/integration/release-readiness-contract.spec.ts", + "scope": "project", + "timeout_seconds": 1200, + "note": "targeted contract verification" + }, + { + "command": "pnpm security:scan", + "scope": "project", + "timeout_seconds": 120, + "note": "tracked credential leak scan" + }, + { + "command": "pnpm smoke:npx", + "scope": "project", + "timeout_seconds": 180, + "note": "packaged npx executable smoke" + }, + { + "command": "pnpm version:check", + "scope": "project", + "timeout_seconds": 120, + "note": "calendar version policy validation" + } + ], + "docs": [ + { + "path": "AGENTS.md", + "scope": "project", + "note": "dogfood command contract package-spec update" + }, + { + "path": "CHANGELOG.md", + "scope": "project", + "note": "unreleased release hardening notes" + }, + { + "path": "CONTRIBUTING.md", + "scope": "project", + "note": "release runbook linkage" + }, + { + "path": "docs/RELEASING.md", + "scope": "project", + "note": "maintainer release process" + }, + { + "path": "PRD.md", + "scope": "project", + "note": "canonical package-spec recursion examples" + }, + { + "path": "README.md", + "scope": "project", + "note": "public release/install contract" + } + ], + "close_reason": "Release management hardening completed: scoped package + calendar version policy + CI/release gates validated." +} + +Prepare repository for public production release without publishing yet. diff --git a/.agents/pm/history/pm-1h88.jsonl b/.agents/pm/history/pm-1h88.jsonl new file mode 100644 index 000000000..75cdc2224 --- /dev/null +++ b/.agents/pm/history/pm-1h88.jsonl @@ -0,0 +1,3 @@ +{"ts":"2026-03-09T13:02:02.787Z","author":"maintainer-agent","op":"create","patch":[{"op":"replace","path":"/body","value":"Operational release execution task for today's production launch."},{"op":"add","path":"/front_matter/id","value":"pm-1h88"},{"op":"add","path":"/front_matter/title","value":"Cut public release 2026.3.9"},{"op":"add","path":"/front_matter/description","value":"Create final release commit/PR messaging, switch GitHub repository visibility to public, and publish @unbrained/pm-cli."},{"op":"add","path":"/front_matter/type","value":"Chore"},{"op":"add","path":"/front_matter/status","value":"open"},{"op":"add","path":"/front_matter/priority","value":0},{"op":"add","path":"/front_matter/tags","value":["pm-cli","publishing","release","security"]},{"op":"add","path":"/front_matter/created_at","value":"2026-03-09T13:02:02.787Z"},{"op":"add","path":"/front_matter/updated_at","value":"2026-03-09T13:02:02.787Z"},{"op":"add","path":"/front_matter/deadline","value":"2026-03-10T13:02:02.787Z"},{"op":"add","path":"/front_matter/author","value":"maintainer-agent"},{"op":"add","path":"/front_matter/estimated_minutes","value":90},{"op":"add","path":"/front_matter/acceptance_criteria","value":"Repository set public intentionally; release commit pushed; PR opened with final messaging; tag v2026.3.9 pushed; release workflow publishes @unbrained/pm-cli successfully; no credential leaks detected."},{"op":"add","path":"/front_matter/comments","value":[{"created_at":"2026-03-09T13:02:02.787Z","author":"maintainer-agent","text":"Execute final launch operations with explicit leak checks and release guardrails."}]},{"op":"add","path":"/front_matter/notes","value":[{"created_at":"2026-03-09T13:02:02.787Z","author":"maintainer-agent","text":"Prefer tag-driven publish workflow using release environment secret NPM_TOKEN."}]},{"op":"add","path":"/front_matter/files","value":[{"path":".github/workflows/release.yml","scope":"project","note":"tag-driven publish pipeline"}]},{"op":"add","path":"/front_matter/tests","value":[{"command":"pnpm security:scan","scope":"project","timeout_seconds":120,"note":"final leak check before release"}]},{"op":"add","path":"/front_matter/docs","value":[{"path":"docs/RELEASING.md","scope":"project","note":"release runbook"}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"c522fb3160fa0fc074074e6d589a9c09c5dba18d537648c71a3c30682c08ff29","message":"Create operational release execution chore | explicit_unset=dependencies,learnings,assignee"} +{"ts":"2026-03-09T13:02:08.116Z","author":"maintainer-agent","op":"claim","patch":[{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T13:02:08.116Z"},{"op":"add","path":"/front_matter/assignee","value":"maintainer-agent"}],"before_hash":"c522fb3160fa0fc074074e6d589a9c09c5dba18d537648c71a3c30682c08ff29","after_hash":"7d44bb314021fffcf565b64f6677bab26e4af2e9582775af0d56eb8fb3cdeeb9","message":"Claim public release execution"} +{"ts":"2026-03-09T13:02:08.582Z","author":"maintainer-agent","op":"update","patch":[{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T13:02:08.582Z"},{"op":"replace","path":"/front_matter/status","value":"in_progress"}],"before_hash":"7d44bb314021fffcf565b64f6677bab26e4af2e9582775af0d56eb8fb3cdeeb9","after_hash":"6138dd22e640768d8fe5c91f7eb35cd7e21a8da1deb92e72a8bcdb10b99b9671","message":"Begin release execution"} diff --git a/.agents/pm/history/pm-1hm2.jsonl b/.agents/pm/history/pm-1hm2.jsonl new file mode 100644 index 000000000..d7bac365b --- /dev/null +++ b/.agents/pm/history/pm-1hm2.jsonl @@ -0,0 +1,9 @@ +{"ts":"2026-03-09T12:42:13.949Z","author":"maintainer-agent","op":"create","patch":[{"op":"replace","path":"/body","value":"Prepare repository for public production release without publishing yet."},{"op":"add","path":"/front_matter/id","value":"pm-1hm2"},{"op":"add","path":"/front_matter/title","value":"Release hardening: scoped npm + version policy + CI"},{"op":"add","path":"/front_matter/description","value":"Audit for leaked private data, enforce calendar SemVer policy, and harden GitHub/npm release automation for scoped package publishing."},{"op":"add","path":"/front_matter/type","value":"Chore"},{"op":"add","path":"/front_matter/status","value":"open"},{"op":"add","path":"/front_matter/priority","value":0},{"op":"add","path":"/front_matter/tags","value":["ci","pm-cli","release-readiness","security"]},{"op":"add","path":"/front_matter/created_at","value":"2026-03-09T12:42:13.949Z"},{"op":"add","path":"/front_matter/updated_at","value":"2026-03-09T12:42:13.949Z"},{"op":"add","path":"/front_matter/deadline","value":"2026-03-10T12:42:13.949Z"},{"op":"add","path":"/front_matter/author","value":"maintainer-agent"},{"op":"add","path":"/front_matter/estimated_minutes","value":180},{"op":"add","path":"/front_matter/acceptance_criteria","value":"No leaked credentials in tracked files/pm data; package/versioning follows YYYY.M.D[-N] policy; CI/release pipelines enforce policy and support @unbrained/pm-cli publishing + npx usage; contributor release docs updated."},{"op":"add","path":"/front_matter/comments","value":[{"created_at":"2026-03-09T12:42:13.949Z","author":"maintainer-agent","text":"Initiate final release hardening sweep before public launch."}]},{"op":"add","path":"/front_matter/notes","value":[{"created_at":"2026-03-09T12:42:13.949Z","author":"maintainer-agent","text":"Implement version policy checks and release workflow guardrails."}]},{"op":"add","path":"/front_matter/files","value":[{"path":"package.json","scope":"project","note":"scope package + version policy"}]},{"op":"add","path":"/front_matter/tests","value":[{"command":"node scripts/run-tests.mjs coverage","scope":"project","timeout_seconds":1200,"note":"full regression gate"}]},{"op":"add","path":"/front_matter/docs","value":[{"path":"README.md","scope":"project","note":"public release/install contract"}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"8b1491187c5c8064eafde778d08fbe77792e8c05e368f316e943440b69ccc87b","message":"Create release hardening chore for public launch prep | explicit_unset=dependencies,learnings,assignee"} +{"ts":"2026-03-09T12:42:19.491Z","author":"maintainer-agent","op":"claim","patch":[{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:42:19.491Z"},{"op":"add","path":"/front_matter/assignee","value":"maintainer-agent"}],"before_hash":"8b1491187c5c8064eafde778d08fbe77792e8c05e368f316e943440b69ccc87b","after_hash":"a5376dda3525773f84dd5be4d588174b56a90bfc0729c7a2ef489fd65db6c6a5","message":"Claim release hardening implementation"} +{"ts":"2026-03-09T12:42:19.977Z","author":"maintainer-agent","op":"update","patch":[{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:42:19.977Z"},{"op":"replace","path":"/front_matter/status","value":"in_progress"}],"before_hash":"a5376dda3525773f84dd5be4d588174b56a90bfc0729c7a2ef489fd65db6c6a5","after_hash":"63b18bc17e42a47e60e6b873eb43d24edaeaa5da79c0e2dd45872b940bf458e1","message":"Start release hardening implementation"} +{"ts":"2026-03-09T12:56:08.225Z","author":"maintainer-agent","op":"files_add","patch":[{"op":"replace","path":"/front_matter/files/0/note","value":"calendar version placeholder"},{"op":"replace","path":"/front_matter/files/0/path","value":".github/ISSUE_TEMPLATE/bug-report.yml"},{"op":"add","path":"/front_matter/files/1","value":{"path":".github/PULL_REQUEST_TEMPLATE.md","scope":"project","note":"new release safety checklist items"}},{"op":"add","path":"/front_matter/files/2","value":{"path":".github/workflows/ci.yml","scope":"project","note":"version/security/npx gates in CI"}},{"op":"add","path":"/front_matter/files/3","value":{"path":".github/workflows/nightly.yml","scope":"project","note":"nightly version and security checks"}},{"op":"add","path":"/front_matter/files/4","value":{"path":".github/workflows/release.yml","scope":"project","note":"release environment publish flow with tag guard"}},{"op":"add","path":"/front_matter/files/5","value":{"path":"AGENTS.md","scope":"project","note":"scoped package-spec recursion examples"}},{"op":"add","path":"/front_matter/files/6","value":{"path":"CHANGELOG.md","scope":"project","note":"release hardening changelog entries"}},{"op":"add","path":"/front_matter/files/7","value":{"path":"CONTRIBUTING.md","scope":"project","note":"link release runbook"}},{"op":"add","path":"/front_matter/files/8","value":{"path":"docs/RELEASING.md","scope":"project","note":"maintainer release runbook"}},{"op":"add","path":"/front_matter/files/9","value":{"path":"package.json","scope":"project","note":"scope package + version policy"}},{"op":"add","path":"/front_matter/files/10","value":{"path":"PRD.md","scope":"project","note":"scoped package-spec recursion examples"}},{"op":"add","path":"/front_matter/files/11","value":{"path":"README.md","scope":"project","note":"scoped install docs and release checklist"}},{"op":"add","path":"/front_matter/files/12","value":{"path":"scripts/check-secrets.mjs","scope":"project","note":"tracked-file secret leak scanner"}},{"op":"add","path":"/front_matter/files/13","value":{"path":"scripts/install.ps1","scope":"project","note":"scoped installer default package"}},{"op":"add","path":"/front_matter/files/14","value":{"path":"scripts/install.sh","scope":"project","note":"scoped installer default package"}},{"op":"add","path":"/front_matter/files/15","value":{"path":"scripts/release-version.mjs","scope":"project","note":"calendar SemVer policy and registry sequencing checks"}},{"op":"add","path":"/front_matter/files/16","value":{"path":"scripts/smoke-npx-from-pack.mjs","scope":"project","note":"packaged npx smoke test"}},{"op":"add","path":"/front_matter/files/17","value":{"path":"tests/integration/ci-workflow-contract.spec.ts","scope":"project","note":"workflow contract assertions"}},{"op":"add","path":"/front_matter/files/18","value":{"path":"tests/integration/help-readme-contract.spec.ts","scope":"project","note":"README installer and package assertions"}},{"op":"add","path":"/front_matter/files/19","value":{"path":"tests/integration/release-readiness-contract.spec.ts","scope":"project","note":"version policy and packaging assertions"}},{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:56:08.225Z"}],"before_hash":"63b18bc17e42a47e60e6b873eb43d24edaeaa5da79c0e2dd45872b940bf458e1","after_hash":"2012b04e4859a24bced825554c3869d0b8136afc16f8ab9707e0c00cc5506e37","message":"Link release hardening change-set files"} +{"ts":"2026-03-09T12:56:14.895Z","author":"maintainer-agent","op":"tests_add","patch":[{"op":"add","path":"/front_matter/tests/1","value":{"command":"node scripts/run-tests.mjs test -- tests/integration/ci-workflow-contract.spec.ts tests/integration/help-readme-contract.spec.ts tests/integration/release-readiness-contract.spec.ts","scope":"project","timeout_seconds":1200,"note":"targeted contract verification"}},{"op":"add","path":"/front_matter/tests/2","value":{"command":"pnpm security:scan","scope":"project","timeout_seconds":120,"note":"tracked credential leak scan"}},{"op":"add","path":"/front_matter/tests/3","value":{"command":"pnpm smoke:npx","scope":"project","timeout_seconds":180,"note":"packaged npx executable smoke"}},{"op":"add","path":"/front_matter/tests/4","value":{"command":"pnpm version:check","scope":"project","timeout_seconds":120,"note":"calendar version policy validation"}},{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:56:14.895Z"}],"before_hash":"2012b04e4859a24bced825554c3869d0b8136afc16f8ab9707e0c00cc5506e37","after_hash":"ad9b3013da87b57879ba40f56766ba7f7847b25d394d94e4c2bcb85d3d27332f","message":"Link release hardening validation commands"} +{"ts":"2026-03-09T12:56:20.794Z","author":"maintainer-agent","op":"docs_add","patch":[{"op":"replace","path":"/front_matter/docs/0/note","value":"dogfood command contract package-spec update"},{"op":"replace","path":"/front_matter/docs/0/path","value":"AGENTS.md"},{"op":"add","path":"/front_matter/docs/1","value":{"path":"CHANGELOG.md","scope":"project","note":"unreleased release hardening notes"}},{"op":"add","path":"/front_matter/docs/2","value":{"path":"CONTRIBUTING.md","scope":"project","note":"release runbook linkage"}},{"op":"add","path":"/front_matter/docs/3","value":{"path":"docs/RELEASING.md","scope":"project","note":"maintainer release process"}},{"op":"add","path":"/front_matter/docs/4","value":{"path":"PRD.md","scope":"project","note":"canonical package-spec recursion examples"}},{"op":"add","path":"/front_matter/docs/5","value":{"path":"README.md","scope":"project","note":"public release/install contract"}},{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:56:20.794Z"}],"before_hash":"ad9b3013da87b57879ba40f56766ba7f7847b25d394d94e4c2bcb85d3d27332f","after_hash":"c454ec81dc58ee64b50083b76663f0d6c57f68fa6aa4ee0b6863f34ecc3f2f8a","message":"Link release hardening documentation updates"} +{"ts":"2026-03-09T12:56:28.657Z","author":"maintainer-agent","op":"comment_add","patch":[{"op":"add","path":"/front_matter/comments/1","value":{"created_at":"2026-03-09T12:56:28.657Z","author":"maintainer-agent","text":"Evidence: release hardening validations passed. Commands: pnpm version:next => 2026.3.9; pnpm version:check => passed; pnpm security:scan => no credential-like secrets in tracked files (including .agents/pm tracked data); pnpm smoke:npx => passed (2026.3.9); node scripts/run-tests.mjs test -- tests/integration/ci-workflow-contract.spec.ts tests/integration/help-readme-contract.spec.ts tests/integration/release-readiness-contract.spec.ts => 48/48 passing; pnpm test => 54 files, 541 tests passing; node scripts/run-tests.mjs coverage => 54 files, 541 tests passing at 100% lines/branches/functions/statements."}},{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:56:28.657Z"}],"before_hash":"c454ec81dc58ee64b50083b76663f0d6c57f68fa6aa4ee0b6863f34ecc3f2f8a","after_hash":"588226ff0fd388df45fd7cd7fa013e5c7fd26292804486087a2cfe06c12a6183","message":"Record release hardening verification evidence"} +{"ts":"2026-03-09T12:56:34.271Z","author":"maintainer-agent","op":"close","patch":[{"op":"remove","path":"/front_matter/assignee"},{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:56:34.271Z"},{"op":"replace","path":"/front_matter/status","value":"closed"},{"op":"add","path":"/front_matter/close_reason","value":"Release management hardening completed: scoped package + calendar version policy + CI/release gates validated."}],"before_hash":"588226ff0fd388df45fd7cd7fa013e5c7fd26292804486087a2cfe06c12a6183","after_hash":"98741de60dd64e273a6abc8fd23a1fd8d98dc72a2b4153f6941eea9f8a1b0e0b","message":"Close: release hardening completed with secret scan, policy checks, npx smoke, and full test/coverage evidence."} +{"ts":"2026-03-09T12:56:34.722Z","author":"maintainer-agent","op":"release","patch":[{"op":"replace","path":"/front_matter/updated_at","value":"2026-03-09T12:56:34.722Z"}],"before_hash":"98741de60dd64e273a6abc8fd23a1fd8d98dc72a2b4153f6941eea9f8a1b0e0b","after_hash":"8bb4fac77cca7035981884bb6bbfdcbb2ea439ddf00561edde8c4b72af759703","message":"Release claim after closure"} diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 23e447324..97edf4379 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -12,7 +12,7 @@ body: attributes: label: pm-cli version description: Run `pm --version` and paste the output here. - placeholder: "0.1.0" + placeholder: "2026.3.9" validations: required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 26498205b..7e55df645 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,6 +21,9 @@ - [ ] `pnpm build` passes - [ ] `pnpm typecheck` passes - [ ] `node scripts/run-tests.mjs coverage` passes at 100% coverage (lines/branches/functions/statements) +- [ ] `pnpm version:check` passes (calendar release version policy) +- [ ] `pnpm security:scan` passes (no tracked credential leaks) +- [ ] `pnpm smoke:npx` passes (packaged npx executable smoke test) - [ ] All linked pm item files/tests/docs are updated - [ ] PRD.md / README.md / AGENTS.md updated if behavior changed - [ ] CHANGELOG.md updated under `[Unreleased]` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4d317543..5433c29b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Version policy check + if: matrix.os == 'ubuntu-latest' && matrix.node == 20 + run: pnpm version:check + + - name: Secret leak scan + if: matrix.os == 'ubuntu-latest' && matrix.node == 20 + run: pnpm security:scan + - name: Build run: pnpm build @@ -71,6 +79,10 @@ jobs: if: matrix.os == 'ubuntu-latest' && matrix.node == 20 run: npm pack --dry-run + - name: npx tarball smoke check + if: matrix.os == 'ubuntu-latest' && matrix.node == 20 + run: pnpm smoke:npx + - name: Upload coverage artifact if: always() && matrix.os == 'ubuntu-latest' && matrix.node == 20 uses: actions/upload-artifact@v4 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 93bf0b130..d3eeec1aa 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -43,6 +43,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Version policy check + if: matrix.node == 20 + run: pnpm version:check + + - name: Secret leak scan + if: matrix.node == 20 + run: pnpm security:scan + - name: Build run: pnpm build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 618245527..695299e2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - "v*.*.*" permissions: - contents: read + contents: write id-token: write concurrency: @@ -17,6 +17,8 @@ jobs: release: name: Build, Test, Publish runs-on: ubuntu-latest + environment: + name: release steps: - name: Checkout @@ -37,6 +39,12 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Version policy + tag guard + run: node scripts/release-version.mjs check --tag "${GITHUB_REF_NAME}" --verify-next + + - name: Secret leak scan + run: pnpm security:scan + - name: Build run: pnpm build @@ -52,6 +60,9 @@ jobs: - name: Packaging smoke check run: npm pack --dry-run + - name: npx tarball smoke check + run: pnpm smoke:npx + - name: Upload coverage artifact if: always() uses: actions/upload-artifact@v4 @@ -64,3 +75,8 @@ jobs: run: npm publish --access public --provenance env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/AGENTS.md b/AGENTS.md index 2f4b0a50a..1f4f56635 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,7 +129,7 @@ Use release when: - Unit/integration test runs must set `PM_PATH` to a temporary sandbox directory. - pm-driven test execution should use `node scripts/run-tests.mjs ` so both `PM_PATH` and `PM_GLOBAL_PATH` are sandboxed automatically per run. - `pm test --add` should only link sandbox-safe test-runner commands: use `node scripts/run-tests.mjs ...` or explicitly set both `PM_PATH` and `PM_GLOBAL_PATH`; sandbox-unsafe runner commands are rejected at add-time, including unsandboxed package-manager run-script variants (for example `npm run test`, `pnpm run test`, `yarn run test`, and `bun run test`) and chained direct test-runner segments that are not explicitly sandboxed. -- `pm test --run` should defensively skip legacy linked commands that invoke `pm test-all` (including global-flag and package-spec launcher forms such as `pm --json test-all`, `npx pm-cli@latest --json test-all`, `pnpm dlx pm-cli@latest --json test-all`, and `npm exec -- pm-cli@latest --json test-all`) and report deterministic skipped results. +- `pm test --run` should defensively skip legacy linked commands that invoke `pm test-all` (including global-flag and package-spec launcher forms such as `pm --json test-all`, `npx @unbrained/pm-cli@latest --json test-all`, `pnpm dlx @unbrained/pm-cli@latest --json test-all`, and `npm exec -- @unbrained/pm-cli@latest --json test-all`) and report deterministic skipped results. - `pm test-all` deduplicates linked tests by scope+normalized command or scope+path and reports duplicates as skipped; when duplicate keys disagree on `timeout_seconds`, execution uses the deterministic maximum timeout for that key. - Integration tests should invoke the built CLI (`node dist/cli.js ...`) with explicit `PM_PATH`, `PM_GLOBAL_PATH`, and `PM_AUTHOR`. - Cleanup temporary test directories after each test/suite. diff --git a/CHANGELOG.md b/CHANGELOG.md index d5453a80b..4e35945a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +#### Release Versioning and Distribution +- npm package identity switched to scoped publish target `@unbrained/pm-cli` to avoid naming collisions with existing unscoped packages while keeping the `pm` executable unchanged. +- Versioning policy now follows calendar SemVer-compatible releases: `YYYY.M.D` for the first release of a day and `YYYY.M.D-N` for subsequent same-day releases (`N >= 2`). +- Installer defaults now target `@unbrained/pm-cli` while preserving `PM_CLI_PACKAGE` override support for local/tarball smoke tests. + +#### CI/CD and Release Guardrails +- Added automated version policy enforcement script (`scripts/release-version.mjs`) with tag/version consistency checks and registry-aware same-day release sequencing. +- Added tracked-file credential leak scanner (`scripts/check-secrets.mjs`) and wired it into CI/release gates. +- Added packaged `npx` smoke test (`scripts/smoke-npx-from-pack.mjs`) to verify tarball executability before release publish. +- Release workflow now uses the GitHub `release` Environment, validates version sequencing before publish, and creates a GitHub Release with generated notes after npm publish. + #### CLI UX - `pm list` now excludes terminal statuses (`closed`, `canceled`) by default, showing only the active working-set of items. Use `pm list-all` to include all items regardless of status. This aligns with common CLI conventions (analogous to `docker ps` vs `docker ps -a`) and makes `pm list` the intuitive day-to-day view without having to type `pm list-open` or filter manually. `pm list-all` is unchanged and continues to return all items. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78a6df974..a481971ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,6 +72,7 @@ For deeper context on implementation and extension development: - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — source tree, item storage, mutation contract, history/restore, search, and testing internals. - [docs/EXTENSIONS.md](docs/EXTENSIONS.md) — full extension development guide: manifest format, ExtensionApi reference, lifecycle hooks, and a working example. +- [docs/RELEASING.md](docs/RELEASING.md) — maintainer release procedure for calendar versioning, CI gates, npm publish, and GitHub Release automation. ## Extension Development diff --git a/PRD.md b/PRD.md index 69e472a4c..bc4b886f9 100644 --- a/PRD.md +++ b/PRD.md @@ -751,7 +751,7 @@ All commands return deterministic top-level objects (TOON by default, JSON with | `pm release ` | id, optional `--author`/`--message`/`--force` | `{ item, released_by, previous_assignee, forced }` | | `pm comments --add/--limit` | id + comment text/limit | `{ id, comments, count }` | | `pm files --add/--remove` | id + file refs | `{ id, files, changed, count }` | -| `pm test --add/--remove/--run` | id + test refs/options (reject recursive `test-all` linked commands at add-time, including global-flag and package-spec launcher forms such as `pm --json test-all`, `npx pm-cli@latest --json test-all`, `pnpm dlx pm-cli@latest --json test-all`, and `npm exec -- pm-cli@latest --json test-all`, defensively skip legacy recursive entries at run-time, and reject sandbox-unsafe test-runner commands including unsandboxed direct package-manager run-script forms such as `npm run test`/`pnpm run test` and chained direct runner segments evaluated independently) | `{ id, tests, run_results, changed, count }` | +| `pm test --add/--remove/--run` | id + test refs/options (reject recursive `test-all` linked commands at add-time, including global-flag and package-spec launcher forms such as `pm --json test-all`, `npx @unbrained/pm-cli@latest --json test-all`, `pnpm dlx @unbrained/pm-cli@latest --json test-all`, and `npm exec -- @unbrained/pm-cli@latest --json test-all`, defensively skip legacy recursive entries at run-time, and reject sandbox-unsafe test-runner commands including unsandboxed direct package-manager run-script forms such as `npm run test`/`pnpm run test` and chained direct runner segments evaluated independently) | `{ id, tests, run_results, changed, count }` | | `pm test-all --status --timeout` | optional status filter; duplicate linked command/path entries are deduped per invocation (keyed by scope+normalized command or scope+path) and reported as skipped; when duplicate keys carry different `timeout_seconds`, execution uses deterministic maximum timeout for that key | `{ totals, failed, passed, skipped, results }` | | `pm stats` | none | `{ totals, by_type, by_status, generated_at }` | | `pm health` | none | `{ ok, checks, warnings, generated_at }` | @@ -1088,7 +1088,7 @@ Wrapper behavior must remain aligned with CLI semantics and exit conditions. - Extension manifest `entry` paths must not escape their owning extension directory. - Dynamic extension command loose-option parsing must ignore unsafe prototype keys (`__proto__`, `constructor`, `prototype`) and use null-prototype option maps before passing option snapshots to extension command handlers. - Never execute linked test commands without explicit `--run`. -- Reject linked test command entries that invoke `pm test-all` (including global-flag and package-spec launcher variants such as `pm --json test-all`, `npx pm-cli@latest --json test-all`, `pnpm dlx pm-cli@latest --json test-all`, and `npm exec -- pm-cli@latest --json test-all`) to prevent recursive orchestration loops. +- Reject linked test command entries that invoke `pm test-all` (including global-flag and package-spec launcher variants such as `pm --json test-all`, `npx @unbrained/pm-cli@latest --json test-all`, `pnpm dlx @unbrained/pm-cli@latest --json test-all`, and `npm exec -- @unbrained/pm-cli@latest --json test-all`) to prevent recursive orchestration loops. - `pm test --run` defensively skips legacy linked command entries that invoke `pm test-all` (including global-flag and package-spec launcher variants such as `npx`, `pnpm dlx`, and `npm exec` launcher forms) and records deterministic skipped results. - Reject linked test-runner command entries (for example `pnpm test`, `pnpm test:coverage`, `npm test`, `npm run test`, `pnpm run test`, `yarn run test`, `bun run test`, `vitest`) unless they use `node scripts/run-tests.mjs ...` or explicitly set both `PM_PATH` and `PM_GLOBAL_PATH`; chained direct test-runner segments are validated independently and rejected when not explicitly sandboxed. - `pm test-all` executes each unique linked command/path key at most once per run; duplicate entries are reported as skipped to keep totals deterministic while avoiding redundant execution. Duplicate-key timeout conflicts resolve deterministically to the maximum `timeout_seconds` value for that key. @@ -1206,7 +1206,7 @@ Sandbox safety requirements (hard): - Every test suite uses temporary sandbox storage via `PM_PATH`. - PM-driven test execution MUST use a sandbox wrapper command (`node scripts/run-tests.mjs test|coverage`) that creates a temporary directory, sets both `PM_PATH` and `PM_GLOBAL_PATH`, runs the requested test command, and cleans up the sandbox afterward. - `pm test --add` MUST enforce this by rejecting sandbox-unsafe test-runner command entries at add-time unless they use `node scripts/run-tests.mjs ...` or explicitly set both `PM_PATH` and `PM_GLOBAL_PATH`; this includes unsandboxed direct package-manager run-script variants (for example `npm run test` and `pnpm run test`) and chained direct test-runner segments that are not explicitly sandboxed. -- `pm test --run` MUST defensively skip legacy linked command entries that invoke `pm test-all` (including global-flag and package-spec launcher variants such as `pm --json test-all`, `npx pm-cli@latest --json test-all`, `pnpm dlx pm-cli@latest --json test-all`, and `npm exec -- pm-cli@latest --json test-all`) and surface deterministic skipped diagnostics. +- `pm test --run` MUST defensively skip legacy linked command entries that invoke `pm test-all` (including global-flag and package-spec launcher variants such as `pm --json test-all`, `npx @unbrained/pm-cli@latest --json test-all`, `pnpm dlx @unbrained/pm-cli@latest --json test-all`, and `npm exec -- @unbrained/pm-cli@latest --json test-all`) and surface deterministic skipped diagnostics. - Integration tests spawn built CLI subprocesses (`node dist/cli.js ...`) with explicit `PM_PATH`, `PM_GLOBAL_PATH`, and `PM_AUTHOR`. - Temporary sandbox directories must be cleaned up after each test/suite. diff --git a/README.md b/README.md index 9366689e1..d78df7c7b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # pm-cli (`pm`) [![CI](https://github.com/unbraind/pm-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/unbraind/pm-cli/actions/workflows/ci.yml) -[![npm version](https://img.shields.io/npm/v/pm-cli)](https://www.npmjs.com/package/pm-cli) -[![Node >=20](https://img.shields.io/node/v/pm-cli)](https://nodejs.org) +[![npm version](https://img.shields.io/npm/v/%40unbrained%2Fpm-cli)](https://www.npmjs.com/package/%40unbrained%2Fpm-cli) +[![Node >=20](https://img.shields.io/node/v/%40unbrained%2Fpm-cli)](https://nodejs.org) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) Agent-friendly, git-native project management for humans and coding agents. @@ -24,7 +24,7 @@ Agent-friendly, git-native project management for humans and coding agents. ### npm (recommended) ```bash -npm i -g pm-cli +npm i -g @unbrained/pm-cli pm --help pm --version ``` @@ -32,13 +32,13 @@ pm --version Update to latest: ```bash -npm i -g pm-cli@latest +npm i -g @unbrained/pm-cli@latest ``` ### Project-local invocation ```bash -npx pm-cli --help +npx @unbrained/pm-cli --help ``` ### Installer scripts @@ -53,6 +53,7 @@ pwsh scripts/install.ps1 Both installers are idempotent for update flows; rerun them to move to a newer version. Each installer verifies post-install CLI availability by resolving `pm` and running `pm --version` before reporting success. +Installer default package target is `@unbrained/pm-cli`. Set `PM_CLI_PACKAGE` to override the package source when smoke-testing installer flows. Scoped package names such as `@scope/pkg` still honor `--version`, while literal specs (`file:`, URLs, local paths/tarballs, or already versioned package specs) are passed to npm unchanged. @@ -203,6 +204,7 @@ docs/ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — Internal architecture, source layout, mutation contract, history/restore, search, and testing. - [docs/EXTENSIONS.md](docs/EXTENSIONS.md) — Extension development guide: manifest format, API reference, hook lifecycle, built-in extensions. +- [docs/RELEASING.md](docs/RELEASING.md) — Maintainer release runbook for calendar versioning, CI gates, npm publish, and GitHub Releases. ## Item File Format @@ -251,7 +253,7 @@ Format: - `pm completion ` (generate shell tab-completion script) - extension-only command paths return not-found when no handler is registered, and generic failure when a matched handler throws; profile diagnostics include deterministic warning codes like `extension_command_handler_failed:::` - extension command names are canonicalized (trimmed, lowercased, repeated internal whitespace collapsed) before registration and dispatch so equivalent command paths resolve deterministically -- `pm test --add` rejects linked commands that invoke `pm test-all` (including global-flag and package-spec launcher forms like `pm --json test-all`, `npx pm-cli@latest --json test-all`, `pnpm dlx pm-cli@latest --json test-all`, and `npm exec -- pm-cli@latest --json test-all`) to prevent recursive orchestration +- `pm test --add` rejects linked commands that invoke `pm test-all` (including global-flag and package-spec launcher forms like `pm --json test-all`, `npx @unbrained/pm-cli@latest --json test-all`, `pnpm dlx @unbrained/pm-cli@latest --json test-all`, and `npm exec -- @unbrained/pm-cli@latest --json test-all`) to prevent recursive orchestration - `pm test --run` skips legacy linked commands that invoke `pm test-all` (including global-flag and package-spec launcher forms such as `npx`, `pnpm dlx`, and `npm exec` launcher variants) and reports deterministic skip diagnostics - `pm test --add` rejects sandbox-unsafe test-runner commands (for example `pnpm test`, `pnpm test:coverage`, `npm test`, `npm run test`, `pnpm run test`, `yarn run test`, `bun run test`, `vitest`) unless they use `node scripts/run-tests.mjs ...` or explicitly set both `PM_PATH` and `PM_GLOBAL_PATH`; chained direct test-runner segments are validated independently, so each direct runner segment must be explicitly sandboxed - `pm test-all` deduplicates identical linked command/path entries per invocation (keyed by scope+normalized command or scope+path), reports duplicates as skipped, and uses the maximum `timeout_seconds` when duplicate keys disagree on timeout metadata @@ -598,7 +600,7 @@ Restore replays append-only history to the target point, rewrites the item atomi - All tests must run with a sandbox `PM_PATH` (never the repository's real `.agents/pm`). - PM-linked test execution should use `node scripts/run-tests.mjs [-- ]` so both `PM_PATH` and `PM_GLOBAL_PATH` are sandboxed per run; forwarded args target Vitest directly (for example: `node scripts/run-tests.mjs test -- tests/unit/health-command.spec.ts`). -- `pm test ` linked command entries must not invoke `pm test-all` (including global-flag and package-spec launcher forms like `pm --json test-all`, `npx pm-cli@latest --json test-all`, `pnpm dlx pm-cli@latest --json test-all`, and `npm exec -- pm-cli@latest --json test-all`); the CLI rejects recursive orchestration entries at add-time. +- `pm test ` linked command entries must not invoke `pm test-all` (including global-flag and package-spec launcher forms like `pm --json test-all`, `npx @unbrained/pm-cli@latest --json test-all`, `pnpm dlx @unbrained/pm-cli@latest --json test-all`, and `npm exec -- @unbrained/pm-cli@latest --json test-all`); the CLI rejects recursive orchestration entries at add-time. - `pm test --run` defensively skips legacy linked command entries that invoke `pm test-all` (including global-flag and package-spec launcher forms such as `npx`, `pnpm dlx`, and `npm exec` launcher variants) and reports deterministic skipped results. - `pm test ` linked test-runner command entries must use `node scripts/run-tests.mjs ...` or explicitly set both `PM_PATH` and `PM_GLOBAL_PATH`; the CLI rejects sandbox-unsafe variants at add-time, including unsandboxed package-manager run-script forms like `npm run test` / `pnpm run test` and chained direct test-runner segments that are not explicitly sandboxed. - `pm test-all` runs each unique linked command/path key once per invocation and marks duplicates as skipped for deterministic orchestration output; duplicate-key timeout conflicts resolve to the maximum `timeout_seconds` for that key. @@ -624,8 +626,10 @@ pnpm build pnpm typecheck pnpm test pnpm test:coverage +pnpm version:check +pnpm security:scan node scripts/run-tests.mjs coverage -npm pack +pnpm smoke:npx ``` Manual smoke checks: @@ -639,11 +643,11 @@ Manual smoke checks: Pushing a version tag triggers the automated npm publish workflow: ```bash -git tag v0.1.0 -git push origin v0.1.0 +git tag v2026.3.9 +git push origin v2026.3.9 ``` -The `.github/workflows/release.yml` workflow runs the full CI suite and publishes to npm when all checks pass. Requires `NPM_TOKEN` secret configured in the repository settings. +The `.github/workflows/release.yml` workflow runs full validation, enforces calendar version policy, and publishes `@unbrained/pm-cli` to npm only when all checks pass. It uses the `release` GitHub Environment and requires the `NPM_TOKEN` environment secret. ## Project Status diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 000000000..ac28c2c06 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,65 @@ +# Releasing `@unbrained/pm-cli` + +This repository uses a tag-driven GitHub Actions release pipeline that publishes to npm and creates a GitHub Release. + +## Version policy + +`pm-cli` uses a calendar SemVer-compatible scheme: + +- `YYYY.M.D` for the first release on a date +- `YYYY.M.D-N` for additional releases on the same date (`N >= 2`) + +Examples: + +- First release on 2026-03-09: `2026.3.9` +- Second release on 2026-03-09: `2026.3.9-2` + +Use: + +```bash +pnpm version:next +``` + +to compute the next expected release version from the npm registry. + +## One-time GitHub setup + +- Create GitHub Environment `release` +- Add environment secret `NPM_TOKEN` (npm automation token with publish rights for `@unbrained/pm-cli`) + +## Release checklist + +1) Update `package.json` version and `CHANGELOG.md` + +2) Run release gates locally: + +```bash +pnpm install +pnpm build +pnpm typecheck +pnpm test +pnpm test:coverage +pnpm version:check +pnpm security:scan +pnpm smoke:npx +``` + +3) Commit and tag: + +```bash +git tag v +git push origin main +git push origin v +``` + +4) Verify Actions: + +- `CI` passes for commit +- `Release` workflow passes for tag +- npm package published at `@unbrained/pm-cli` +- GitHub Release created with generated notes + +## Notes + +- Do not run manual `npm publish`; publishing is owned by `.github/workflows/release.yml`. +- Release workflow enforces tag/version alignment and calendar version sequencing before publish. diff --git a/package.json b/package.json index 8ed5437a2..b510950ab 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "pm-cli", - "version": "0.1.0", + "name": "@unbrained/pm-cli", + "version": "2026.3.9", "description": "Git-native project management CLI for humans and agents.", "type": "module", - "author": "unbraind", + "author": "unbrained", "homepage": "https://github.com/unbraind/pm-cli#readme", "repository": { "type": "git", @@ -33,6 +33,10 @@ "dev": "tsx src/cli.ts", "test": "pnpm build && vitest run", "test:coverage": "pnpm build && vitest run --coverage", + "version:check": "node scripts/release-version.mjs check", + "version:next": "node scripts/release-version.mjs next", + "security:scan": "node scripts/check-secrets.mjs", + "smoke:npx": "node scripts/smoke-npx-from-pack.mjs", "prepublishOnly": "pnpm build" }, "keywords": [ @@ -46,6 +50,9 @@ "coding-agents" ], "license": "MIT", + "publishConfig": { + "access": "public" + }, "engines": { "node": ">=20" }, diff --git a/scripts/check-secrets.mjs b/scripts/check-secrets.mjs new file mode 100644 index 000000000..27f7535bd --- /dev/null +++ b/scripts/check-secrets.mjs @@ -0,0 +1,104 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const RULES = [ + { name: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g }, + { name: "github-token", regex: /\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g }, + { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g }, + { name: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/g }, + { name: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g }, + { name: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }, + { name: "jwt-like-token", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g }, + { name: "npm-auth-token-assignment", regex: /_authToken\s*=\s*\S+/g }, +]; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function gitTrackedFiles() { + const output = execFileSync("git", ["ls-files", "-z"], { encoding: "utf8" }); + return output.split("\0").filter((entry) => entry.length > 0); +} + +function isLikelyBinary(buffer) { + const sampleSize = Math.min(buffer.length, 4096); + for (let index = 0; index < sampleSize; index += 1) { + if (buffer[index] === 0) { + return true; + } + } + return false; +} + +function lineNumberFromIndex(content, index) { + let line = 1; + for (let i = 0; i < index; i += 1) { + if (content.charCodeAt(i) === 10) { + line += 1; + } + } + return line; +} + +function excerptAround(content, index, width = 80) { + const start = Math.max(0, index - width); + const end = Math.min(content.length, index + width); + return content + .slice(start, end) + .replaceAll("\n", "\\n") + .replaceAll("\r", "") + .trim(); +} + +function run() { + const findings = []; + const files = gitTrackedFiles(); + + for (const file of files) { + let buffer; + try { + buffer = readFileSync(file); + } catch (error) { + if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") { + // Skip tracked paths that are intentionally deleted in the current worktree. + continue; + } + const message = error instanceof Error ? error.message : String(error); + fail(`Failed to read ${file}: ${message}`); + } + + if (buffer.length === 0 || isLikelyBinary(buffer)) { + continue; + } + + const content = buffer.toString("utf8"); + for (const rule of RULES) { + const matches = content.matchAll(rule.regex); + for (const match of matches) { + const index = match.index ?? 0; + findings.push({ + file, + rule: rule.name, + line: lineNumberFromIndex(content, index), + excerpt: excerptAround(content, index), + }); + } + } + } + + if (findings.length > 0) { + console.error("Potential secrets detected:"); + for (const finding of findings) { + console.error(`- ${finding.file}:${finding.line} [${finding.rule}] ${finding.excerpt}`); + } + process.exit(1); + } + + console.log("No credential-like secrets detected in tracked files."); +} + +run(); diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 85fdbba34..c64780b0c 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -10,7 +10,7 @@ $ErrorActionPreference = "Stop" $envPackageName = $env:PM_CLI_PACKAGE if ([string]::IsNullOrWhiteSpace($PackageName)) { if ([string]::IsNullOrWhiteSpace($envPackageName)) { - $PackageName = "pm-cli" + $PackageName = "@unbrained/pm-cli" } else { $PackageName = $envPackageName } diff --git a/scripts/install.sh b/scripts/install.sh index 737450a86..4f4841e8c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash set -euo pipefail -PACKAGE_NAME="${PM_CLI_PACKAGE:-pm-cli}" +PACKAGE_NAME="${PM_CLI_PACKAGE:-@unbrained/pm-cli}" TARGET_VERSION="latest" PREFIX="" usage() { cat <<'EOF' -Install or update pm-cli globally via npm. +Install or update @unbrained/pm-cli globally via npm. Usage: bash scripts/install.sh [--version ] [--prefix ] diff --git a/scripts/release-version.mjs b/scripts/release-version.mjs new file mode 100644 index 000000000..1f166a145 --- /dev/null +++ b/scripts/release-version.mjs @@ -0,0 +1,260 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const VERSION_PATTERN = /^([1-9]\d{3})\.([1-9]\d*)\.([1-9]\d*)(?:-([1-9]\d*))?$/; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function usage() { + console.log(`Usage: + node scripts/release-version.mjs check [--tag ] [--verify-next] [--date ] + node scripts/release-version.mjs next [--date ] + +Rules: + - Version format: YYYY.M.D or YYYY.M.D-N + - N is the release number for that day and must be >= 2 when present + - Month/day must be valid calendar values +`); +} + +function parseDateKey(input) { + const match = input.match(VERSION_PATTERN); + if (!match) { + return null; + } + + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) { + return null; + } + if (month < 1 || month > 12 || day < 1 || day > 31) { + return null; + } + + const date = new Date(Date.UTC(year, month - 1, day)); + if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) { + return null; + } + + return `${year}.${month}.${day}`; +} + +function parseVersion(version) { + const match = version.match(VERSION_PATTERN); + if (!match) { + fail( + `Invalid version "${version}". Expected YYYY.M.D or YYYY.M.D-N (SemVer compatible, no zero padding).`, + ); + } + + const dateKey = parseDateKey(`${match[1]}.${match[2]}.${match[3]}`); + if (!dateKey) { + fail(`Version "${version}" uses an invalid calendar date.`); + } + + const ordinal = match[4] ? Number(match[4]) : null; + if (ordinal === 1) { + fail(`Version "${version}" is invalid: omit suffix for first release of a day (use YYYY.M.D).`); + } + + return { + version, + dateKey, + ordinal, + }; +} + +function readPackageJson() { + const currentFile = fileURLToPath(import.meta.url); + const repoRoot = path.resolve(path.dirname(currentFile), ".."); + const raw = readFileSync(path.join(repoRoot, "package.json"), "utf8"); + const parsed = JSON.parse(raw); + if (typeof parsed.name !== "string" || parsed.name.trim().length === 0) { + fail("package.json is missing a valid \"name\"."); + } + if (typeof parsed.version !== "string" || parsed.version.trim().length === 0) { + fail("package.json is missing a valid \"version\"."); + } + return parsed; +} + +function getUtcDateKey() { + const now = new Date(); + return `${now.getUTCFullYear()}.${now.getUTCMonth() + 1}.${now.getUTCDate()}`; +} + +function resolveDateKey(flagValue) { + if (!flagValue) { + return getUtcDateKey(); + } + const dateKey = parseDateKey(flagValue); + if (!dateKey) { + fail(`Invalid --date value "${flagValue}". Expected YYYY.M.D with a valid calendar date.`); + } + return dateKey; +} + +function npmCommand() { + return process.platform === "win32" ? "npm.cmd" : "npm"; +} + +function listPublishedVersions(packageName) { + let output = ""; + try { + output = execFileSync(npmCommand(), ["view", packageName, "versions", "--json"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (error) { + const stderrRaw = typeof error === "object" && error !== null && "stderr" in error ? error.stderr : ""; + const stderr = + typeof stderrRaw === "string" + ? stderrRaw + : stderrRaw instanceof Buffer + ? stderrRaw.toString("utf8") + : ""; + if (stderr.includes("E404") || stderr.includes("404 Not Found")) { + return []; + } + const message = error instanceof Error ? error.message : String(error); + fail(`Failed to query npm registry for ${packageName}: ${message}`); + } + + const trimmed = output.trim(); + if (trimmed.length === 0) { + return []; + } + + let parsed; + try { + parsed = JSON.parse(trimmed); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + fail(`Failed to parse npm versions JSON: ${message}`); + } + + if (Array.isArray(parsed)) { + return parsed.filter((entry) => typeof entry === "string"); + } + if (typeof parsed === "string") { + return [parsed]; + } + return []; +} + +function nextVersionForDate(packageName, dateKey) { + const publishedVersions = listPublishedVersions(packageName); + const releasesOnDate = publishedVersions.filter((version) => { + const match = version.match(VERSION_PATTERN); + if (!match) { + return false; + } + const candidateDate = `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`; + return candidateDate === dateKey; + }); + + if (releasesOnDate.length === 0) { + return dateKey; + } + return `${dateKey}-${releasesOnDate.length + 1}`; +} + +function parseFlags(args) { + const flags = { + tag: null, + verifyNext: false, + date: null, + }; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === "--verify-next") { + flags.verifyNext = true; + continue; + } + if (arg === "--tag") { + const value = args[i + 1]; + if (!value) { + fail("--tag requires a value."); + } + flags.tag = value; + i += 1; + continue; + } + if (arg === "--date") { + const value = args[i + 1]; + if (!value) { + fail("--date requires a value."); + } + flags.date = value; + i += 1; + continue; + } + if (arg === "-h" || arg === "--help") { + usage(); + process.exit(0); + } + fail(`Unknown flag "${arg}". Use --help for usage.`); + } + + return flags; +} + +function runCheck(flags) { + const pkg = readPackageJson(); + const parsedVersion = parseVersion(pkg.version); + + if (flags.tag) { + const expectedTag = `v${pkg.version}`; + if (flags.tag !== expectedTag) { + fail(`Tag/version mismatch: tag=${flags.tag}, expected=${expectedTag}.`); + } + } + + if (flags.verifyNext) { + const expectedDate = resolveDateKey(flags.date); + if (parsedVersion.dateKey !== expectedDate) { + fail( + `Version date mismatch: package version date is ${parsedVersion.dateKey}, expected ${expectedDate}.`, + ); + } + + const expectedNext = nextVersionForDate(pkg.name, expectedDate); + if (pkg.version !== expectedNext) { + fail( + `Version sequencing mismatch: package.json has ${pkg.version}, expected next release version ${expectedNext}.`, + ); + } + } + + console.log(`Version policy check passed (${pkg.version}).`); +} + +function runNext(flags) { + const pkg = readPackageJson(); + const dateKey = resolveDateKey(flags.date); + const next = nextVersionForDate(pkg.name, dateKey); + console.log(next); +} + +const command = process.argv[2] ?? "check"; +const flags = parseFlags(process.argv.slice(3)); + +if (command === "check") { + runCheck(flags); +} else if (command === "next") { + runNext(flags); +} else if (command === "-h" || command === "--help") { + usage(); +} else { + fail(`Unknown command "${command}". Use "check" or "next".`); +} diff --git a/scripts/smoke-npx-from-pack.mjs b/scripts/smoke-npx-from-pack.mjs new file mode 100644 index 000000000..4d2605f0f --- /dev/null +++ b/scripts/smoke-npx-from-pack.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { rmSync } from "node:fs"; + +function resolveCommand(base) { + return process.platform === "win32" ? `${base}.cmd` : base; +} + +function run() { + const npm = resolveCommand("npm"); + const npx = resolveCommand("npx"); + + const packOutput = execFileSync(npm, ["pack", "--silent"], { encoding: "utf8" }) + .trim() + .split("\n") + .filter((line) => line.trim().length > 0); + const tarball = packOutput.at(-1); + if (!tarball) { + throw new Error("npm pack did not produce a tarball name."); + } + + try { + const version = execFileSync(npx, ["--yes", `./${tarball}`, "--version"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); + if (version.length === 0) { + throw new Error("npx smoke test returned empty version output."); + } + console.log(`npx smoke check passed (${version}).`); + } finally { + rmSync(tarball, { force: true }); + } +} + +run(); diff --git a/tests/integration/ci-workflow-contract.spec.ts b/tests/integration/ci-workflow-contract.spec.ts index eda37185f..354257307 100644 --- a/tests/integration/ci-workflow-contract.spec.ts +++ b/tests/integration/ci-workflow-contract.spec.ts @@ -54,10 +54,13 @@ describe("GitHub workflow contract", () => { "run: pnpm build", "run: pnpm typecheck", "run: pnpm test", + "run: pnpm version:check", + "run: pnpm security:scan", "if: matrix.os == 'ubuntu-latest' && matrix.node == 20", "run: pnpm test:coverage", "run: node scripts/run-tests.mjs coverage", "run: npm pack --dry-run", + "run: pnpm smoke:npx", "uses: actions/upload-artifact@v4", "if: always() && matrix.os == 'ubuntu-latest' && matrix.node == 20", "name: coverage-node${{ matrix.node }}-${{ matrix.os }}", @@ -85,6 +88,8 @@ describe("GitHub workflow contract", () => { "- 24", "- 25", "run: pnpm build", + "run: pnpm version:check", + "run: pnpm security:scan", "run: pnpm typecheck", "if: matrix.node == 20", "run: pnpm test:coverage", @@ -105,16 +110,22 @@ describe("GitHub workflow contract", () => { "tags:", "v*.*.*", "permissions:", - "contents: read", + "contents: write", "concurrency:", "cancel-in-progress: false", + "environment:", + "name: release", + "run: node scripts/release-version.mjs check --tag \"${GITHUB_REF_NAME}\" --verify-next", + "run: pnpm security:scan", "run: pnpm build", "run: pnpm typecheck", "run: pnpm test:coverage", "run: node scripts/run-tests.mjs coverage", "run: npm pack --dry-run", + "run: pnpm smoke:npx", "run: npm publish", "NPM_TOKEN", + "uses: softprops/action-gh-release@v2", "uses: actions/upload-artifact@v4", "path: coverage", "if-no-files-found: ignore", diff --git a/tests/integration/help-readme-contract.spec.ts b/tests/integration/help-readme-contract.spec.ts index b0d076dc9..bb431c456 100644 --- a/tests/integration/help-readme-contract.spec.ts +++ b/tests/integration/help-readme-contract.spec.ts @@ -109,6 +109,8 @@ describe("README/help contract (sandboxed)", () => { const readme = await readFile(path.resolve(process.cwd(), "README.md"), "utf8"); const installerSection = extractSection(readme, "### Installer scripts", "During development in this repo:"); + expect(readme).toContain("npm i -g @unbrained/pm-cli"); + expect(readme).toContain("npx @unbrained/pm-cli --help"); expect(installerSection).toContain("https://raw.githubusercontent.com/unbraind/pm-cli/main/scripts/install.sh"); expect(installerSection).toContain("https://raw.githubusercontent.com/unbraind/pm-cli/main/scripts/install.ps1"); expect(installerSection).toContain("`PM_CLI_PACKAGE`"); @@ -123,13 +125,13 @@ describe("README/help contract (sandboxed)", () => { expect(installerSection).toContain("Scoped package names such as `@scope/pkg` still honor `--version`"); expect(installerSection).toContain("literal specs"); - expect(shellInstaller).toContain('PACKAGE_NAME="${PM_CLI_PACKAGE:-pm-cli}"'); + expect(shellInstaller).toContain('PACKAGE_NAME="${PM_CLI_PACKAGE:-@unbrained/pm-cli}"'); expect(shellInstaller).toContain("is_literal_install_spec"); expect(shellInstaller).toContain('if is_literal_install_spec "$PACKAGE_NAME"; then'); expect(shellInstaller).toContain('[[ "$name" == @*/*@* ]]'); expect(powershellInstaller).toContain('[string]$PackageName = ""'); expect(powershellInstaller).toContain("$envPackageName = $env:PM_CLI_PACKAGE"); - expect(powershellInstaller).toContain('$PackageName = "pm-cli"'); + expect(powershellInstaller).toContain('$PackageName = "@unbrained/pm-cli"'); expect(powershellInstaller).toContain("$PackageName = $envPackageName"); expect(powershellInstaller).toContain("function Use-LiteralInstallSpec"); expect(powershellInstaller).toContain('if ($Name.StartsWith("@"))'); diff --git a/tests/integration/release-readiness-contract.spec.ts b/tests/integration/release-readiness-contract.spec.ts index 9d55c2492..2284eb8ff 100644 --- a/tests/integration/release-readiness-contract.spec.ts +++ b/tests/integration/release-readiness-contract.spec.ts @@ -143,6 +143,11 @@ function expectSectionContainsTokens(section: string, tokens: string[]): void { } } +function isValidCalendarDate(year: number, month: number, day: number): boolean { + const date = new Date(Date.UTC(year, month - 1, day)); + return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day; +} + function expectTopLevelKeyOrder(value: unknown, expectedKeys: string[]): void { expect(value).toBeTypeOf("object"); expect(value).not.toBeNull(); @@ -536,7 +541,23 @@ describe("release readiness baseline contract", () => { const expectedVersion = packageJson.version; expect(expectedVersion).toBeTypeOf("string"); - expect(expectedVersion).toMatch(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/); + const versionPolicyMatch = (expectedVersion as string).match(/^([1-9]\d{3})\.([1-9]\d*)\.([1-9]\d*)(?:-([1-9]\d*))?$/); + expect(versionPolicyMatch).not.toBeNull(); + if (!versionPolicyMatch) { + throw new Error("unreachable"); + } + const year = Number(versionPolicyMatch[1]); + const month = Number(versionPolicyMatch[2]); + const day = Number(versionPolicyMatch[3]); + const releaseOrdinal = versionPolicyMatch[4] ? Number(versionPolicyMatch[4]) : null; + expect(month).toBeGreaterThanOrEqual(1); + expect(month).toBeLessThanOrEqual(12); + expect(day).toBeGreaterThanOrEqual(1); + expect(day).toBeLessThanOrEqual(31); + expect(isValidCalendarDate(year, month, day)).toBe(true); + if (releaseOrdinal !== null) { + expect(releaseOrdinal).toBeGreaterThanOrEqual(2); + } await withTempPmPath(async (context) => { const versionResult = context.runCli(["--version"]); @@ -1431,12 +1452,14 @@ describe("release readiness baseline contract", () => { it("keeps npm packaging allowlist and prepublish guard aligned", async () => { const packageJson = JSON.parse(await readRepoText("package.json")) as { + name?: string; files?: string[]; scripts?: Record; repository?: { type?: string; url?: string }; bugs?: { url?: string }; homepage?: string; author?: string; + publishConfig?: { access?: string }; }; const requiredPublishFiles = [ @@ -1457,6 +1480,13 @@ describe("release readiness baseline contract", () => { } expect(packageJson.scripts?.prepublishOnly).toBe("pnpm build"); + expect(packageJson.scripts?.["version:check"]).toBe("node scripts/release-version.mjs check"); + expect(packageJson.scripts?.["version:next"]).toBe("node scripts/release-version.mjs next"); + expect(packageJson.scripts?.["security:scan"]).toBe("node scripts/check-secrets.mjs"); + expect(packageJson.scripts?.["smoke:npx"]).toBe("node scripts/smoke-npx-from-pack.mjs"); + + expect(packageJson.name).toBe("@unbrained/pm-cli"); + expect(packageJson.publishConfig?.access).toBe("public"); expect(packageJson.repository).toBeDefined(); expect(packageJson.repository?.type).toBe("git"); @@ -1523,6 +1553,9 @@ describe("release readiness baseline contract", () => { "scripts/run-tests.mjs", "scripts/install.sh", "scripts/install.ps1", + "scripts/release-version.mjs", + "scripts/check-secrets.mjs", + "scripts/smoke-npx-from-pack.mjs", ]; for (const requiredPath of requiredPaths) { @@ -1549,9 +1582,22 @@ describe("release readiness baseline contract", () => { expect(installSh).toContain("PM_BIN"); expect(installSh).toContain("$PM_BIN --version"); expect(installSh).toContain("Installed pm version:"); + expect(installSh).toContain("@unbrained/pm-cli"); const installPs1 = await readRepoText("scripts/install.ps1"); expect(installPs1).toContain("$pmExecutable --version"); expect(installPs1).toContain("Installed pm version:"); + expect(installPs1).toContain('@unbrained/pm-cli'); + + const releaseVersionScript = await readRepoText("scripts/release-version.mjs"); + expect(releaseVersionScript).toContain("YYYY.M.D"); + expect(releaseVersionScript).toContain("verify-next"); + + const securityScanScript = await readRepoText("scripts/check-secrets.mjs"); + expect(securityScanScript).toContain("No credential-like secrets detected"); + + const npxSmokeScript = await readRepoText("scripts/smoke-npx-from-pack.mjs"); + expect(npxSmokeScript).toContain("npx"); + expect(npxSmokeScript).toContain("npm pack"); }); });