@1aboveio/skills 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -0
- package/README.md +90 -0
- package/bin/1aboveio-skills.mjs +18 -0
- package/package.json +28 -0
- package/runtime/skills/distribution/generated/recipes.json +1189 -0
- package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
- package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/backend/airflow-dag-develop/LICENSE +3 -0
- package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
- package/skills/backend/app-debug/LICENSE +3 -0
- package/skills/backend/app-debug/SKILL.md +109 -0
- package/skills/backend/app-debug/references/common-errors.md +128 -0
- package/skills/backend/python-backend/LICENSE +3 -0
- package/skills/backend/python-backend/SKILL.md +326 -0
- package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
- package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
- package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
- package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd-pipeline/docker/LICENSE +3 -0
- package/skills/cicd-pipeline/docker/SKILL.md +126 -0
- package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
- package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
- package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
- package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
- package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
- package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
- package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
- package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd-pipeline/mergify/LICENSE +3 -0
- package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
- package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
- package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
- package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
- package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
- package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
- package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
- package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
- package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd-pipeline/podman/LICENSE +3 -0
- package/skills/cicd-pipeline/podman/SKILL.md +70 -0
- package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +156 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
- package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
- package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
- package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
- package/skills/engineering/e2e-test/references/debugging.md +36 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
- package/skills/engineering/e2e-test/references/reviewing.md +39 -0
- package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +10 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +136 -0
- package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
- package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
- package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
- package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +18 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/implement-and-pr/LICENSE +3 -0
- package/skills/engineering/implement-and-pr/SKILL.md +176 -0
- package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
- package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
- package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
- package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
- package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
- package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
- package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +167 -0
- package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
- package/skills/engineering/resolve-issues/references/breaker.md +82 -0
- package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
- package/skills/engineering/resolve-issues/references/delivery.md +108 -0
- package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
- package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
- package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
- package/skills/engineering/resolve-issues/references/finalization.md +68 -0
- package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
- package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
- package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
- package/skills/engineering/resolve-issues/references/intake.md +86 -0
- package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
- package/skills/engineering/resolve-issues/references/interference.md +87 -0
- package/skills/engineering/resolve-issues/references/loop.md +134 -0
- package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
- package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
- package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
- package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
- package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
- package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
- package/skills/engineering/resolve-issues/references/run-state.md +288 -0
- package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
- package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
- package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
- package/skills/engineering/resolve-issues/references/why.md +653 -0
- package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
- package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
- package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
- package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
- package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
- package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
- package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
- package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
- package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
- package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
- package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
- package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
- package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
- package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
- package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
- package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
- package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
- package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
- package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
- package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
- package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
- package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
- package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
- package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
- package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
- package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
- package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
- package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
- package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
- package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
- package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
- package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
- package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +112 -0
- package/skills/engineering/resolve-release/references/assembly.md +137 -0
- package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
- package/skills/engineering/resolve-release/references/candidate.md +167 -0
- package/skills/engineering/resolve-release/references/exposure.md +178 -0
- package/skills/engineering/resolve-release/references/handoff.md +24 -0
- package/skills/engineering/resolve-release/references/postmortem.md +230 -0
- package/skills/engineering/resolve-release/references/preflight.md +207 -0
- package/skills/engineering/resolve-release/references/principles.md +94 -0
- package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
- package/skills/engineering/resolve-release/references/related-skills.md +12 -0
- package/skills/engineering/resolve-release/references/routing.md +149 -0
- package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
- package/skills/engineering/resolve-release/references/versioning.md +202 -0
- package/skills/engineering/resolve-release/references/why.md +53 -0
- package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
- package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
- package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
- package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
- package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
- package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
- package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
- package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
- package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
- package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
- package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
- package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
- package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
- package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
- package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
- package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
- package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
- package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
- package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
- package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
- package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
- package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
- package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
- package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
- package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
- package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
- package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
- package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
- package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
- package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
- package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/README.md +60 -0
- package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
- package/skills/engineering/review-pr/LICENSE +3 -0
- package/skills/engineering/review-pr/SKILL.md +123 -0
- package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
- package/skills/engineering/review-pr/references/code-correctness.md +5 -0
- package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
- package/skills/engineering/review-pr/references/coordination.md +18 -0
- package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
- package/skills/engineering/review-pr/references/finding-themes.md +7 -0
- package/skills/engineering/review-pr/references/github-posting.md +98 -0
- package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
- package/skills/engineering/review-pr/references/incremental-output.md +16 -0
- package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
- package/skills/engineering/review-pr/references/output-format.md +99 -0
- package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
- package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
- package/skills/engineering/review-pr/references/re-review.md +18 -0
- package/skills/engineering/review-pr/references/review-method.md +199 -0
- package/skills/engineering/review-pr/references/review-mode.md +30 -0
- package/skills/engineering/review-pr/references/review-posture.md +53 -0
- package/skills/engineering/review-pr/references/round1-depth.md +62 -0
- package/skills/engineering/review-pr/references/scripts.md +17 -0
- package/skills/engineering/review-pr/references/workflow.md +16 -0
- package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
- package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
- package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
- package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
- package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
- package/skills/engineering/smoke/LICENSE +3 -0
- package/skills/engineering/smoke/SKILL.md +131 -0
- package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
- package/skills/engineering/smoke/references/manifest.md +192 -0
- package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
- package/skills/fullstack/better-auth/LICENSE +3 -0
- package/skills/fullstack/better-auth/SKILL.md +601 -0
- package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
- package/skills/fullstack/monorepo/LICENSE +3 -0
- package/skills/fullstack/monorepo/SKILL.md +465 -0
- package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
- package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
- package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
- package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
- package/skills/fullstack/prisma-setup/LICENSE +3 -0
- package/skills/fullstack/prisma-setup/SKILL.md +180 -0
- package/skills/fullstack/prisma-setup/nextjs.md +258 -0
- package/skills/fullstack/prisma-setup/turborepo.md +301 -0
- package/skills/fullstack/shadcn/LICENSE +3 -0
- package/skills/fullstack/shadcn/SKILL.md +119 -0
- package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
- package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
- package/skills/fullstack/shadcn/cli.md +411 -0
- package/skills/fullstack/shadcn/customization.md +224 -0
- package/skills/fullstack/shadcn/evals/evals.json +90 -0
- package/skills/fullstack/shadcn/mcp.md +101 -0
- package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
- package/skills/fullstack/shadcn/rules/composition.md +195 -0
- package/skills/fullstack/shadcn/rules/data-table.md +201 -0
- package/skills/fullstack/shadcn/rules/forms.md +255 -0
- package/skills/fullstack/shadcn/rules/icons.md +103 -0
- package/skills/fullstack/shadcn/rules/styling.md +167 -0
- package/skills/fullstack/zod-v4/LICENSE +3 -0
- package/skills/fullstack/zod-v4/SKILL.md +287 -0
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
// version — the mechanized versioning / tagging / CHANGELOG helper for the release lane.
|
|
2
|
+
//
|
|
3
|
+
// Versioning is a load-bearing release step, and a prose-described load-bearing step
|
|
4
|
+
// degrades to a lookalike: an agent that "computes the next version" from memory picks
|
|
5
|
+
// a minor for a breaking change, re-points a tag to a new SHA to "fix" it, or reuses the
|
|
6
|
+
// version of a rolled-back release. Each of those is unrecoverable once pushed — a tag
|
|
7
|
+
// is a public promise about an immutable SHA. So the invariants live here, mechanically:
|
|
8
|
+
//
|
|
9
|
+
// 1. PRODUCTION ONLY. Versions, tags and CHANGELOG cuts exist only on the production
|
|
10
|
+
// branch. A dev/staging release is unversioned and identified by its SHA. `plan`,
|
|
11
|
+
// `changelog`, `rc` and `tag` refuse (exit 3, a NORMAL outcome) when --target-branch is
|
|
12
|
+
// not the production branch — the release loop calls them unconditionally and branches on
|
|
13
|
+
// 3. The gate FAILS CLOSED: both branch flags are required (exit 2 without them), or
|
|
14
|
+
// an agent could skip the guard by simply omitting a flag.
|
|
15
|
+
// 2. TAGS ARE IMMUTABLE. `tag` never moves, deletes, or force-writes a tag. Re-tagging
|
|
16
|
+
// the same SHA is a resume-safe no-op; re-tagging a DIFFERENT SHA is exit 1. There is
|
|
17
|
+
// deliberately no --force: a version that reached a remote is burned, and the next
|
|
18
|
+
// attempt takes the next patch (references/versioning.md, "the invariant, the trigger,
|
|
19
|
+
// and the burn").
|
|
20
|
+
// The rc tags `rc` mints obey the same rule — a given rc number names one SHA forever.
|
|
21
|
+
// They are cheap because they are NUMBERED, not because they move: the next attempt
|
|
22
|
+
// takes the next rc, and the VERSION survives, because the final `v<version>` tag is
|
|
23
|
+
// minted only after production verification. That is the point of rc tags — a burn is
|
|
24
|
+
// then possible only on the legacy shape where the final tag itself triggers the
|
|
25
|
+
// deploy (references/versioning.md, "the invariant, the trigger, and the burn").
|
|
26
|
+
// 3. THE CHANGELOG IS APPEND-ONLY. A rolled-back release's section is never deleted —
|
|
27
|
+
// `withdraw` annotates it. Deleting it would erase the only public record that the
|
|
28
|
+
// version existed and was withdrawn.
|
|
29
|
+
//
|
|
30
|
+
// For the authoritative subcommand / flag / exit-code list, run `node version.mjs --help`
|
|
31
|
+
// (or read the USAGE const below, right beside the dispatch it describes). That help text is
|
|
32
|
+
// the SINGLE source of truth: restating the case list here would give it a second copy to
|
|
33
|
+
// drift against, which is the exact footgun this pointer removes — one list can't contradict
|
|
34
|
+
// itself. What stays here is the doctrine `--help` does not encode: the three invariants
|
|
35
|
+
// above, plus this one rule about the codes it prints — each exit code carries exactly ONE
|
|
36
|
+
// meaning, because an agent branches on them, so an overloaded code (3 meaning both "skip
|
|
37
|
+
// versioning" and "already cut") would make a resuming run silently ship production untagged.
|
|
38
|
+
// The policy behind all of it: references/versioning.md.
|
|
39
|
+
|
|
40
|
+
import fs from 'node:fs'
|
|
41
|
+
import { spawnSync } from 'node:child_process'
|
|
42
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
43
|
+
|
|
44
|
+
// ---- semver ----
|
|
45
|
+
|
|
46
|
+
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
47
|
+
|
|
48
|
+
export function parseSemver(input) {
|
|
49
|
+
const m = SEMVER_RE.exec(String(input).trim())
|
|
50
|
+
if (!m) return null
|
|
51
|
+
return {
|
|
52
|
+
major: Number(m[1]),
|
|
53
|
+
minor: Number(m[2]),
|
|
54
|
+
patch: Number(m[3]),
|
|
55
|
+
prerelease: m[4] || null,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatVersion(v) {
|
|
60
|
+
return `${v.major}.${v.minor}.${v.patch}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function compareVersions(a, b) {
|
|
64
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
65
|
+
if (a[k] !== b[k]) return a[k] < b[k] ? -1 : 1
|
|
66
|
+
}
|
|
67
|
+
// A prerelease sorts BEFORE its release (semver §11.3). The lane mints these itself now
|
|
68
|
+
// (`rc`), so this ordering is load-bearing rather than defensive.
|
|
69
|
+
if (a.prerelease && !b.prerelease) return -1
|
|
70
|
+
if (!a.prerelease && b.prerelease) return 1
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The latest RELEASE tag: a prerelease is never a release baseline. A `v1.5.0-rc.2` means a
|
|
75
|
+
// candidate for 1.5.0 that has NOT been verified in production — 1.5.0 is still unreleased,
|
|
76
|
+
// so baselining on it would propose 1.5.1 and skip a version that never shipped.
|
|
77
|
+
export function latestReleaseTag(tags) {
|
|
78
|
+
const parsed = tags
|
|
79
|
+
.map((t) => ({ tag: t, v: parseSemver(t) }))
|
|
80
|
+
.filter((x) => x.v && !x.v.prerelease)
|
|
81
|
+
if (parsed.length === 0) return null
|
|
82
|
+
parsed.sort((a, b) => compareVersions(a.v, b.v))
|
|
83
|
+
return parsed[parsed.length - 1]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- change classification (conventional commits, then labels) ----
|
|
87
|
+
|
|
88
|
+
const TITLE_RE = /^\s*([a-zA-Z]+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/
|
|
89
|
+
const BREAKING_LABELS = new Set(['breaking', 'breaking-change', 'semver-major'])
|
|
90
|
+
|
|
91
|
+
// Conventional type → CHANGELOG section. Types absent here are internal: excluded from
|
|
92
|
+
// the CHANGELOG unless --include-internal, because a changelog records notable changes,
|
|
93
|
+
// not every commit.
|
|
94
|
+
export const SECTION_BY_TYPE = {
|
|
95
|
+
feat: 'Added',
|
|
96
|
+
fix: 'Fixed',
|
|
97
|
+
perf: 'Changed',
|
|
98
|
+
refactor: 'Changed',
|
|
99
|
+
revert: 'Removed',
|
|
100
|
+
security: 'Security',
|
|
101
|
+
}
|
|
102
|
+
export const INTERNAL_TYPES = ['build', 'chore', 'ci', 'docs', 'style', 'test']
|
|
103
|
+
export const SECTION_ORDER = ['Breaking', 'Added', 'Changed', 'Removed', 'Fixed', 'Security', 'Internal']
|
|
104
|
+
|
|
105
|
+
export function classifyChange(change) {
|
|
106
|
+
const title = String(change.title || '')
|
|
107
|
+
const labels = (change.labels || []).map((l) => String(l).toLowerCase())
|
|
108
|
+
const m = TITLE_RE.exec(title)
|
|
109
|
+
|
|
110
|
+
let type = m ? m[1].toLowerCase() : null
|
|
111
|
+
if (!type) type = labels.find((l) => l in SECTION_BY_TYPE || INTERNAL_TYPES.includes(l)) || 'other'
|
|
112
|
+
|
|
113
|
+
const breaking =
|
|
114
|
+
Boolean(m && m[3]) ||
|
|
115
|
+
/(^|\n)BREAKING[ -]CHANGE:/.test(String(change.body || '')) ||
|
|
116
|
+
labels.some((l) => BREAKING_LABELS.has(l))
|
|
117
|
+
|
|
118
|
+
const internal = INTERNAL_TYPES.includes(type)
|
|
119
|
+
const section = breaking ? 'Breaking' : internal ? 'Internal' : SECTION_BY_TYPE[type] || 'Changed'
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
...change,
|
|
123
|
+
type,
|
|
124
|
+
breaking,
|
|
125
|
+
internal,
|
|
126
|
+
section,
|
|
127
|
+
scope: m ? m[2] || null : null,
|
|
128
|
+
summary: m ? m[4] : title,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The bump the release CONTENT demands. Under 0.x (`--strict` disables this), breaking
|
|
133
|
+
// changes land as a minor because the public contract is still unstable. Non-breaking
|
|
134
|
+
// features remain minor releases so version numbers communicate new capability.
|
|
135
|
+
export function bumpFor(changes, current, { strict = false } = {}) {
|
|
136
|
+
const classified = changes.map(classifyChange)
|
|
137
|
+
const zeroVer = !strict && current.major === 0
|
|
138
|
+
|
|
139
|
+
if (classified.some((c) => c.breaking)) {
|
|
140
|
+
return { bump: zeroVer ? 'minor' : 'major', reason: 'breaking change in the release content' }
|
|
141
|
+
}
|
|
142
|
+
if (classified.some((c) => c.type === 'feat')) {
|
|
143
|
+
return { bump: 'minor', reason: 'new feature in the release content' }
|
|
144
|
+
}
|
|
145
|
+
return { bump: 'patch', reason: 'fixes and internal changes only' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function nextVersion(current, bump) {
|
|
149
|
+
if (bump === 'major') return { major: current.major + 1, minor: 0, patch: 0, prerelease: null }
|
|
150
|
+
if (bump === 'minor') return { major: current.major, minor: current.minor + 1, patch: 0, prerelease: null }
|
|
151
|
+
return { major: current.major, minor: current.minor, patch: current.patch + 1, prerelease: null }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function tagName(version, prefix = 'v') {
|
|
155
|
+
return `${prefix}${typeof version === 'string' ? version : formatVersion(version)}`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The rc tags belonging to ONE exact version tag, ascending by number. Matching is exact,
|
|
159
|
+
// not prefix-wise: `v1.5.0` must not collect `v1.5.0-rc.1-hotfix` or anything under a
|
|
160
|
+
// neighbouring version, or the next rc number would be computed from a foreign series.
|
|
161
|
+
export function rcTags(tags, base) {
|
|
162
|
+
const re = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-rc\\.(\\d+)$`)
|
|
163
|
+
return tags
|
|
164
|
+
.map((t) => ({ tag: t, n: Number((re.exec(t) || [])[1]) }))
|
|
165
|
+
.filter((x) => Number.isInteger(x.n))
|
|
166
|
+
.sort((a, b) => a.n - b.n)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The rc tags a REMOTE holds for one version, each with the commit it names.
|
|
170
|
+
//
|
|
171
|
+
// Numbering an rc from local tags alone is wrong whenever the two can disagree, and they
|
|
172
|
+
// disagree in the most ordinary case there is: a fresh clone has no local tags, so the next
|
|
173
|
+
// number computes as rc.1 while the remote is already on rc.3. What makes that worse than a
|
|
174
|
+
// bad number is the ORDER it fails in — `git tag` succeeds locally, then `git push` is
|
|
175
|
+
// rejected, leaving a stale local tag pointing at the wrong commit and a non-zero exit that
|
|
176
|
+
// looks like a crash. Since it is the remote that fires the production pipeline, the remote
|
|
177
|
+
// is the authority on which numbers are spent. Same deref subtlety as remoteTagSha: an
|
|
178
|
+
// annotated tag's commit appears only on the `^{}` line, so prefer it when both are present.
|
|
179
|
+
export function remoteRcTags(remote, base, run = git) {
|
|
180
|
+
const out = run(['ls-remote', '--tags', remote, `refs/tags/${base}-rc.*`], { allowFail: true }).stdout
|
|
181
|
+
if (!out) return []
|
|
182
|
+
const shaByTag = new Map()
|
|
183
|
+
for (const line of out.split('\n').filter(Boolean)) {
|
|
184
|
+
const [sha, ref] = line.split(/\s+/)
|
|
185
|
+
if (!ref) continue
|
|
186
|
+
const isDeref = ref.endsWith('^{}')
|
|
187
|
+
const name = ref.replace(/^refs\/tags\//, '').replace(/\^\{\}$/, '')
|
|
188
|
+
if (isDeref || !shaByTag.has(name)) shaByTag.set(name, sha)
|
|
189
|
+
}
|
|
190
|
+
return rcTags([...shaByTag.keys()], base).map((x) => ({ ...x, sha: shaByTag.get(x.tag) }))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- CHANGELOG rendering ----
|
|
194
|
+
|
|
195
|
+
const SCAFFOLD = `# Changelog
|
|
196
|
+
|
|
197
|
+
All notable changes to this project are documented in this file. The format is based on
|
|
198
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
199
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
200
|
+
|
|
201
|
+
## [Unreleased]
|
|
202
|
+
`
|
|
203
|
+
|
|
204
|
+
function prLink(change, repo) {
|
|
205
|
+
if (!change.pr) return ''
|
|
206
|
+
const n = String(change.pr).replace(/^#/, '')
|
|
207
|
+
return repo ? ` ([#${n}](https://github.com/${repo}/pull/${n}))` : ` (#${n})`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function issueRefs(change) {
|
|
211
|
+
const issues = change.issues || []
|
|
212
|
+
if (issues.length === 0) return ''
|
|
213
|
+
return ` — closes ${issues.map((i) => `#${String(i).replace(/^#/, '')}`).join(', ')}`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// A release section (and the [Unreleased] body) is just `### Section` groups of bullets.
|
|
217
|
+
// Parsing back to that shape is what lets the cut MERGE hand-staged notes with derived
|
|
218
|
+
// ones instead of one clobbering the other.
|
|
219
|
+
export function parseGroups(body) {
|
|
220
|
+
const groups = new Map()
|
|
221
|
+
let current = null
|
|
222
|
+
for (const line of String(body).split('\n')) {
|
|
223
|
+
const heading = /^###\s+(.+?)\s*$/.exec(line)
|
|
224
|
+
if (heading) {
|
|
225
|
+
current = heading[1]
|
|
226
|
+
if (!groups.has(current)) groups.set(current, [])
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
230
|
+
const key = current || 'Changed' // a bare bullet under no heading still counts
|
|
231
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
232
|
+
groups.get(key).push(line.trim().replace(/^\*\s+/, '- '))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return groups
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Staged notes first (a human wrote them), then derived entries; exact duplicates collapse.
|
|
239
|
+
export function mergeGroups(staged, derived) {
|
|
240
|
+
const out = new Map()
|
|
241
|
+
const keys = [...SECTION_ORDER, ...[...staged.keys(), ...derived.keys()].filter((k) => !SECTION_ORDER.includes(k))]
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
if (out.has(key)) continue
|
|
244
|
+
const items = [...(staged.get(key) || []), ...(derived.get(key) || [])]
|
|
245
|
+
const seen = new Set()
|
|
246
|
+
const deduped = items.filter((i) => (seen.has(i) ? false : seen.add(i)))
|
|
247
|
+
if (deduped.length > 0) out.set(key, deduped)
|
|
248
|
+
}
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function renderGroups(version, date, groups) {
|
|
253
|
+
const lines = [`## [${version}] — ${date}`, '']
|
|
254
|
+
if (groups.size === 0) return `${lines.join('\n')}_No user-facing changes._\n`
|
|
255
|
+
for (const [section, items] of groups) {
|
|
256
|
+
lines.push(`### ${section}`, '', ...items, '')
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function groupsFromChanges(changes, { repo = null, includeInternal = false } = {}) {
|
|
262
|
+
const groups = new Map()
|
|
263
|
+
for (const c of changes.map(classifyChange)) {
|
|
264
|
+
if (!includeInternal && c.internal) continue
|
|
265
|
+
if (!groups.has(c.section)) groups.set(c.section, [])
|
|
266
|
+
const scope = c.scope ? `**${c.scope}**: ` : ''
|
|
267
|
+
groups.get(c.section).push(`- ${scope}${c.summary}${prLink(c, repo)}${issueRefs(c)}`)
|
|
268
|
+
}
|
|
269
|
+
// Emit in SECTION_ORDER so Breaking leads.
|
|
270
|
+
const ordered = new Map()
|
|
271
|
+
for (const s of SECTION_ORDER) if (groups.has(s)) ordered.set(s, groups.get(s))
|
|
272
|
+
return ordered
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function renderChangelogSection({ version, date, changes, repo = null, includeInternal = false }) {
|
|
276
|
+
return renderGroups(version, date, groupsFromChanges(changes, { repo, includeInternal }))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const headingRe = (version) => new RegExp(`^## \\[${version.replace(/\./g, '\\.')}\\]`)
|
|
280
|
+
|
|
281
|
+
function bodyAfter(lines, headingIdx) {
|
|
282
|
+
let end = headingIdx + 1
|
|
283
|
+
while (end < lines.length && !/^## /.test(lines[end])) end++
|
|
284
|
+
return { body: lines.slice(headingIdx + 1, end).join('\n'), end }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Cut the release: the [Unreleased] body — the unversioned zone where changes accumulate
|
|
288
|
+
// as they merge to dev — is CARRIED into a new versioned heading, merged with the entries
|
|
289
|
+
// derived from the release's PRs, and [Unreleased] is left empty for the next cycle. This
|
|
290
|
+
// is the whole reason dev needs no version: `Unreleased` IS the absence of one, and the
|
|
291
|
+
// version is assigned once, here, to the set of changes being promoted.
|
|
292
|
+
//
|
|
293
|
+
// Carrying (never discarding) is the invariant: a hand-written note that a PR title did
|
|
294
|
+
// not capture must survive the cut. Idempotent — re-cutting a version already present is
|
|
295
|
+
// a resume, not a double-cut.
|
|
296
|
+
export function insertSection(md, section, version) {
|
|
297
|
+
const text = md && md.trim() ? md : SCAFFOLD
|
|
298
|
+
const lines = text.split('\n')
|
|
299
|
+
|
|
300
|
+
if (lines.some((l) => headingRe(version).test(l))) return { text, noop: true }
|
|
301
|
+
|
|
302
|
+
const sectionLines = section.trimEnd().split('\n')
|
|
303
|
+
const heading = sectionLines[0]
|
|
304
|
+
const derived = parseGroups(sectionLines.slice(1).join('\n'))
|
|
305
|
+
const unreleasedAt = lines.findIndex((l) => /^## \[Unreleased\]/i.test(l))
|
|
306
|
+
|
|
307
|
+
const finish = (out) => ({ text: `${out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`, noop: false })
|
|
308
|
+
|
|
309
|
+
if (unreleasedAt === -1) {
|
|
310
|
+
// No Unreleased section: nothing to carry. Insert above the first release heading.
|
|
311
|
+
const firstRelease = lines.findIndex((l) => /^## /.test(l))
|
|
312
|
+
const at = firstRelease === -1 ? lines.length : firstRelease
|
|
313
|
+
lines.splice(at, 0, ...renderGroups(version, heading.split('—').pop().trim(), derived).split('\n'))
|
|
314
|
+
return finish(lines)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const { body, end } = bodyAfter(lines, unreleasedAt)
|
|
318
|
+
const merged = mergeGroups(parseGroups(body), derived)
|
|
319
|
+
const cut = [heading, '', ...renderGroups(version, 'x', merged).split('\n').slice(2)]
|
|
320
|
+
|
|
321
|
+
return finish([...lines.slice(0, unreleasedAt + 1), '', ...cut, '', ...lines.slice(end)])
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function extractSection(md, version) {
|
|
325
|
+
const lines = String(md).split('\n')
|
|
326
|
+
const start = lines.findIndex((l) => headingRe(version).test(l))
|
|
327
|
+
if (start === -1) return null
|
|
328
|
+
let end = start + 1
|
|
329
|
+
while (end < lines.length && !/^## /.test(lines[end])) end++
|
|
330
|
+
return lines.slice(start, end).join('\n').trim()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A rolled-back version is annotated, never deleted (invariant 3). The note is what tells
|
|
334
|
+
// a reader the tag exists but the version never served production.
|
|
335
|
+
export function markWithdrawn(md, version, incident) {
|
|
336
|
+
const lines = String(md).split('\n')
|
|
337
|
+
const at = lines.findIndex((l) => headingRe(version).test(l))
|
|
338
|
+
if (at === -1) return { text: md, noop: true, reason: `no [${version}] section` }
|
|
339
|
+
if (lines.slice(at, at + 4).some((l) => /^> \*\*Withdrawn\*\*/.test(l))) {
|
|
340
|
+
return { text: md, noop: true, reason: 'already withdrawn' }
|
|
341
|
+
}
|
|
342
|
+
const ref = incident ? ` See ${String(incident).startsWith('#') ? incident : `#${incident}`}.` : ''
|
|
343
|
+
// Wording covers BOTH doors to a burn: a failed build (never deployed) and a failed
|
|
344
|
+
// verification (deployed, rolled back). "Never served production traffic" is the claim
|
|
345
|
+
// that holds either way; "was rolled back" would be false for the build-failure case.
|
|
346
|
+
const note = `> **Withdrawn** — this release never served production traffic.${ref}`
|
|
347
|
+
lines.splice(at + 1, 0, '', note)
|
|
348
|
+
return { text: `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`, noop: false }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---- git ----
|
|
352
|
+
|
|
353
|
+
export function git(args, { allowFail = false, cwd } = {}) {
|
|
354
|
+
const r = spawnSync('git', args, { encoding: 'utf8', cwd })
|
|
355
|
+
if (r.status !== 0 && !allowFail) {
|
|
356
|
+
throw new Error(`git ${args.join(' ')} failed: ${(r.stderr || '').trim()}`)
|
|
357
|
+
}
|
|
358
|
+
return { status: r.status, stdout: (r.stdout || '').trim(), stderr: (r.stderr || '').trim() }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function tagExists(tag, run = git) {
|
|
362
|
+
return run(['rev-parse', '-q', '--verify', `refs/tags/${tag}`], { allowFail: true }).status === 0
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function shaForTag(tag, run = git) {
|
|
366
|
+
return run(['rev-list', '-n', '1', tag]).stdout
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Resolve a caller-supplied commit-ish, or null. Never throws.
|
|
370
|
+
//
|
|
371
|
+
// Every `--sha` here arrives from a human or a half-finished script in the middle of a
|
|
372
|
+
// release: a typo, a branch this clone never fetched, a SHA copied from another checkout.
|
|
373
|
+
// Letting git throw turns all of those into an identical stack trace, which at that moment
|
|
374
|
+
// reads as "the release tool is broken" rather than "that ref isn't in this repo" — and the
|
|
375
|
+
// difference decides whether the operator retries or starts debugging the wrong thing.
|
|
376
|
+
export function resolveSha(ref, run = git) {
|
|
377
|
+
const r = run(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { allowFail: true })
|
|
378
|
+
return r.status === 0 && r.stdout ? r.stdout : null
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// The COMMIT a remote tag names, or null if the remote has no such tag.
|
|
382
|
+
//
|
|
383
|
+
// An ANNOTATED tag is an object in its own right, so `ls-remote refs/tags/v1.5.0` returns
|
|
384
|
+
// the tag object's SHA, not the commit's. The commit only appears on the `^{}` deref line,
|
|
385
|
+
// and an exact-ref pattern does not match it — so both patterns must be requested. Compare
|
|
386
|
+
// the wrong one and every resume looks like a SHA conflict ("that version is burned") on a
|
|
387
|
+
// tag that is in fact correct. A lightweight tag has no deref line; its row is the commit.
|
|
388
|
+
export function remoteTagSha(remote, tag, run = git) {
|
|
389
|
+
const out = run(['ls-remote', '--tags', remote, `refs/tags/${tag}`, `refs/tags/${tag}^{}`], { allowFail: true }).stdout
|
|
390
|
+
if (!out) return null
|
|
391
|
+
const rows = out.split('\n').filter(Boolean).map((l) => l.split(/\s+/))
|
|
392
|
+
const deref = rows.find(([, ref]) => ref === `refs/tags/${tag}^{}`)
|
|
393
|
+
const exact = rows.find(([, ref]) => ref === `refs/tags/${tag}`)
|
|
394
|
+
const row = deref || exact
|
|
395
|
+
return row ? row[0] : null
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Push ONE annotated tag ref, hook-safe.
|
|
399
|
+
//
|
|
400
|
+
// Tags are not stack commits. A Mergify stacks pre-push hook that rewrites `git push` into
|
|
401
|
+
// `mergify stack push` is correct for branch heads and wrong for `refs/tags/*` — the tag is
|
|
402
|
+
// an immutable release artifact, not a stack-managed commit, and identity-v0.4.1's production
|
|
403
|
+
// cut stalled until the operator rediscovered `git push --no-verify origin refs/tags/…`.
|
|
404
|
+
// Always push the fully-qualified tag ref with `--no-verify` so a tag mint never depends on
|
|
405
|
+
// that rediscovery. Branch pushes are outside this helper on purpose.
|
|
406
|
+
export function pushTag(remote, tag, run = git) {
|
|
407
|
+
return run(['push', '--no-verify', remote, `refs/tags/${tag}`], { allowFail: true })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Resolve the production branch against a FRESH remote tip before ancestry checks.
|
|
411
|
+
//
|
|
412
|
+
// `rc`/`tag` ask "is this the ship SHA?" by testing ancestry of the production branch. A
|
|
413
|
+
// local `main` that has not been fetched since the promotion landed is the wrong answer to
|
|
414
|
+
// that question — identity-v0.4.1 refused a SHA that was already on `origin/main` because the
|
|
415
|
+
// local tip was stale. When a remote is configured: fetch `<remote> <branch>` first and
|
|
416
|
+
// resolve against `origin/<branch>` (or the already-qualified remote-tracking form the
|
|
417
|
+
// operator passed). Never use a potentially stale local branch tip for the ship-SHA check
|
|
418
|
+
// in a clone that has a remote. A failed fetch fails closed — a pre-existing remote-tracking
|
|
419
|
+
// ref is not a fresh tip, so returning it with `fetched: false` is the same class of wrong
|
|
420
|
+
// answer. A remote-less throwaway clone (unit tests) falls back to the local ref only
|
|
421
|
+
// because there is no remote tip to prefer.
|
|
422
|
+
export function resolveProductionAncestryRef(production, remote = 'origin', run = git) {
|
|
423
|
+
if (typeof production !== 'string' || production.length === 0) {
|
|
424
|
+
return { error: 'production branch is required for the ancestry check' }
|
|
425
|
+
}
|
|
426
|
+
const remotePrefix = `${remote}/`
|
|
427
|
+
const branch = production.startsWith(remotePrefix)
|
|
428
|
+
? production.slice(remotePrefix.length)
|
|
429
|
+
: production.startsWith('refs/remotes/' + remotePrefix)
|
|
430
|
+
? production.slice(('refs/remotes/' + remotePrefix).length)
|
|
431
|
+
: production
|
|
432
|
+
if (!branch || branch.includes('..')) {
|
|
433
|
+
return { error: `production branch "${production}" is not a usable ref name` }
|
|
434
|
+
}
|
|
435
|
+
const remotes = (run(['remote'], { allowFail: true }).stdout || '')
|
|
436
|
+
.split('\n')
|
|
437
|
+
.map((s) => s.trim())
|
|
438
|
+
.filter(Boolean)
|
|
439
|
+
const hasRemote = remotes.includes(remote)
|
|
440
|
+
const remoteRef = `${remote}/${branch}`
|
|
441
|
+
|
|
442
|
+
if (hasRemote) {
|
|
443
|
+
// `git fetch origin main` only writes FETCH_HEAD — it does NOT update origin/main. The
|
|
444
|
+
// explicit refspec is what makes the remote-tracking tip the one we then ancestry-check.
|
|
445
|
+
// `--no-tags` is load-bearing: without it, fetch auto-follows remote tags that happen to
|
|
446
|
+
// point at commits already in the local object store (e.g. a burned vX.Y.Z on another
|
|
447
|
+
// SHA), which would re-create a local tag the burn path just deleted and then mint past.
|
|
448
|
+
const fetch = run(
|
|
449
|
+
['fetch', '--quiet', '--no-tags', remote, `refs/heads/${branch}:refs/remotes/${remote}/${branch}`],
|
|
450
|
+
{ allowFail: true },
|
|
451
|
+
)
|
|
452
|
+
// Fail closed on a failed fetch. A pre-existing origin/<branch> tip may be stale, and
|
|
453
|
+
// shipping against it is exactly the class of wrong answer this helper exists to stop
|
|
454
|
+
// (identity-v0.4.1). Never return { ref: remoteRef, fetched: false }.
|
|
455
|
+
if (fetch.status !== 0) {
|
|
456
|
+
return {
|
|
457
|
+
error:
|
|
458
|
+
`could not fetch ${remote} ${branch}: ${fetch.stderr || 'fetch failed'}. ` +
|
|
459
|
+
`Refusing to mint against a potentially stale remote-tracking "${remoteRef}" — ` +
|
|
460
|
+
`fetch ${remote} ${branch} (or pass --production-branch ${remoteRef}) and re-run.`,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const remoteOk = run(['rev-parse', '--verify', '--quiet', `${remoteRef}^{commit}`], { allowFail: true })
|
|
464
|
+
if (remoteOk.status === 0 && remoteOk.stdout) {
|
|
465
|
+
return { ref: remoteRef, branch, fetched: true }
|
|
466
|
+
}
|
|
467
|
+
// Fetch claimed success but the remote-tracking tip is still unusable — refuse rather
|
|
468
|
+
// than fall through to a local tip that is exactly the stale state we stop trusting.
|
|
469
|
+
return {
|
|
470
|
+
error:
|
|
471
|
+
`remote-tracking ref "${remoteRef}" does not resolve after fetch. ` +
|
|
472
|
+
`Refusing to mint against a potentially stale local "${production}" — ` +
|
|
473
|
+
`fetch ${remote} ${branch} (or pass --production-branch ${remoteRef}) and re-run.`,
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// No remote configured: only the local ref exists. Unit-test clones hit this path.
|
|
478
|
+
const localOk = run(['rev-parse', '--verify', '--quiet', `${production}^{commit}`], { allowFail: true })
|
|
479
|
+
if (localOk.status === 0 && localOk.stdout) {
|
|
480
|
+
return { ref: production, branch, fetched: false, localOnly: true }
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
error:
|
|
484
|
+
`production branch "${production}" does not resolve, and no remote "${remote}" is configured ` +
|
|
485
|
+
`to fetch from. Pass a resolvable ref (e.g. ${remoteRef}) after fetching.`,
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Push the tag unless the remote already agrees. Returns {pushed, error}.
|
|
490
|
+
//
|
|
491
|
+
// The local tag existing is NOT evidence the remote has it: `git tag` then `git push` is
|
|
492
|
+
// two steps, and a run interrupted between them leaves a local-only tag. A resume that
|
|
493
|
+
// short-circuits on "local tag exists" would never push — and a later `gh release create`
|
|
494
|
+
// would then mint the remote tag at the default branch's HEAD, a DIFFERENT commit. So the
|
|
495
|
+
// resume path reconciles the remote rather than assuming it.
|
|
496
|
+
export function ensureRemoteTag(remote, tag, sha, run = git) {
|
|
497
|
+
const remoteSha = remoteTagSha(remote, tag, run)
|
|
498
|
+
if (remoteSha === sha) return { pushed: false }
|
|
499
|
+
if (remoteSha != null) {
|
|
500
|
+
return { error: `${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)} — that version is burned; take the next patch.` }
|
|
501
|
+
}
|
|
502
|
+
const pushed = pushTag(remote, tag, run)
|
|
503
|
+
if (pushed.status !== 0) {
|
|
504
|
+
return { error: `could not push ${tag} to ${remote}: ${pushed.stderr}` }
|
|
505
|
+
}
|
|
506
|
+
return { pushed: true }
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ---- CLI ----
|
|
510
|
+
|
|
511
|
+
function parseArgs(argv) {
|
|
512
|
+
const opts = {}
|
|
513
|
+
const positional = []
|
|
514
|
+
for (let i = 0; i < argv.length; i++) {
|
|
515
|
+
const a = argv[i]
|
|
516
|
+
if (a.startsWith('--')) {
|
|
517
|
+
const key = a.slice(2)
|
|
518
|
+
const next = argv[i + 1]
|
|
519
|
+
if (next === undefined || next.startsWith('--')) opts[key] = true
|
|
520
|
+
else {
|
|
521
|
+
opts[key] = next
|
|
522
|
+
i++
|
|
523
|
+
}
|
|
524
|
+
} else positional.push(a)
|
|
525
|
+
}
|
|
526
|
+
return { opts, positional }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function emitTagResult(opts, { tag, sha, pushed = false, resumed = false }) {
|
|
530
|
+
const result = { tag, sha, pushed, resumed }
|
|
531
|
+
console.log(opts.json ? JSON.stringify(result, null, 2) : tag)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function emitChangelogPlan(opts, { version, file, noop, markdown }) {
|
|
535
|
+
if (opts.format === 'json') {
|
|
536
|
+
console.log(JSON.stringify({ version, file, noop, markdown }, null, 2))
|
|
537
|
+
} else {
|
|
538
|
+
process.stdout.write(markdown)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Every parse here is a place an operator hands us text, so every one is a place they can hand us
|
|
543
|
+
// the wrong text. Bare `JSON.parse` reported that as a V8 stack trace: pi session 019faea6 hit
|
|
544
|
+
// `SyntaxError: Unexpected end of JSON input at readChanges (version.mjs:439)`, which names the
|
|
545
|
+
// line that threw and nothing anyone can act on. It does not say WHICH input was bad (file,
|
|
546
|
+
// literal, stdin), what its path was, or that "empty" is the likely cause. Mid-release that reads
|
|
547
|
+
// as "the release tooling is broken" rather than "your --changes file is empty", and the
|
|
548
|
+
// difference decides whether the next move is a one-line fix or an escalation.
|
|
549
|
+
//
|
|
550
|
+
// So each source names itself in the refusal — the same reason `preflight-probes.mjs` quotes a
|
|
551
|
+
// tool's own refusal id rather than paraphrasing it: the diagnosis has to survive to the person
|
|
552
|
+
// reading the error.
|
|
553
|
+
export function parseChangesJson(text, source) {
|
|
554
|
+
const trimmed = String(text ?? '').trim()
|
|
555
|
+
if (trimmed.length === 0) {
|
|
556
|
+
throw new Error(`version: ${source} is empty — expected a JSON array of changes ('[]' means none, which is not the same as an empty file)`)
|
|
557
|
+
}
|
|
558
|
+
let parsed
|
|
559
|
+
try {
|
|
560
|
+
parsed = JSON.parse(trimmed)
|
|
561
|
+
} catch (error) {
|
|
562
|
+
throw new Error(`version: ${source} is not valid JSON (${error.message}); expected a JSON array of changes`)
|
|
563
|
+
}
|
|
564
|
+
if (!Array.isArray(parsed)) {
|
|
565
|
+
throw new Error(`version: ${source} parsed as ${parsed === null ? 'null' : typeof parsed}, not an array — --changes takes a JSON array of changes`)
|
|
566
|
+
}
|
|
567
|
+
return parsed
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function readChanges(opts) {
|
|
571
|
+
const raw = opts.changes
|
|
572
|
+
if (!raw || raw === true) return []
|
|
573
|
+
if (raw === '-' || opts.stdin) return parseChangesJson(fs.readFileSync(0, 'utf8'), 'stdin (--changes -)')
|
|
574
|
+
if (raw.trim().startsWith('[')) return parseChangesJson(raw, 'the --changes literal')
|
|
575
|
+
let text
|
|
576
|
+
try {
|
|
577
|
+
text = fs.readFileSync(raw, 'utf8')
|
|
578
|
+
} catch (error) {
|
|
579
|
+
// A missing or unreadable path is its own diagnosis; reporting it as bad JSON sends the
|
|
580
|
+
// operator to inspect a file that is not there.
|
|
581
|
+
throw new Error(`version: --changes file ${JSON.stringify(raw)} could not be read (${error.code || error.message})`)
|
|
582
|
+
}
|
|
583
|
+
return parseChangesJson(text, `--changes file ${JSON.stringify(raw)}`)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// An RC tag invokes an external production trigger. The preflight artifact is deliberately
|
|
587
|
+
// checked again at the mutation boundary: a release transcript that mentions a probe is not a
|
|
588
|
+
// guard if `rc` can still run without its successful record.
|
|
589
|
+
export function validateRcTopologyProbe(file) {
|
|
590
|
+
if (typeof file !== 'string' || file.length === 0) {
|
|
591
|
+
return { error: 'missing --topology-probe <preflight-probes JSON>; run the tag-trigger topology probe before minting an RC' }
|
|
592
|
+
}
|
|
593
|
+
let report
|
|
594
|
+
try {
|
|
595
|
+
report = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
596
|
+
} catch (error) {
|
|
597
|
+
return { error: `could not read topology probe ${JSON.stringify(file)} (${error.code || error.message})` }
|
|
598
|
+
}
|
|
599
|
+
const topology = Array.isArray(report && report.probes)
|
|
600
|
+
? report.probes.find((p) => p && p.id === 'rc-tag-topology')
|
|
601
|
+
: null
|
|
602
|
+
if (!report || report.ready !== true || !topology || topology.state !== 'ok') {
|
|
603
|
+
return { error: 'topology probe does not contain a ready rc-tag-topology result' }
|
|
604
|
+
}
|
|
605
|
+
for (const [field, expected] of Object.entries({
|
|
606
|
+
prodBuildTrigger: 'tag-trigger',
|
|
607
|
+
versionSource: 'TAG_NAME',
|
|
608
|
+
candidateTagSource: 'TAG_NAME',
|
|
609
|
+
})) {
|
|
610
|
+
if (topology[field] !== expected) {
|
|
611
|
+
return { error: `topology probe ${field} must be ${expected}, got ${JSON.stringify(topology[field])}` }
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const substitutions = topology.substitutions
|
|
615
|
+
const tagMatch = substitutions && /^v?(\d+\.\d+\.\d+)-rc\.[1-9]\d*$/.exec(String(substitutions.TAG_NAME || ''))
|
|
616
|
+
if (!tagMatch || substitutions._DEPLOYED_VERSION !== tagMatch[1] || substitutions._CANDIDATE_TAG !== substitutions.TAG_NAME) {
|
|
617
|
+
return { error: 'topology probe substitutions do not prove TAG_NAME -> _DEPLOYED_VERSION (final SemVer) and _CANDIDATE_TAG (RC tag)' }
|
|
618
|
+
}
|
|
619
|
+
return { topology }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Invariant 1, mechanized. Exit 3 is NORMAL — a dev release legitimately has no version.
|
|
623
|
+
//
|
|
624
|
+
// This FAILS CLOSED: both flags are required. A guard that is skipped by simply not
|
|
625
|
+
// passing a flag is not a guard — an agent that forgets them would otherwise tag a
|
|
626
|
+
// dev-only SHA, minting a version that names a commit production will never run.
|
|
627
|
+
function requireProductionTarget(opts, command) {
|
|
628
|
+
const target = opts['target-branch']
|
|
629
|
+
const production = opts['production-branch']
|
|
630
|
+
if (typeof production !== 'string' || typeof target !== 'string') {
|
|
631
|
+
console.error(
|
|
632
|
+
`version: ${command} requires --target-branch <b> --production-branch <b>. ` +
|
|
633
|
+
'They decide whether this release may be versioned at all (only a production ' +
|
|
634
|
+
'promotion may), so they are never optional.',
|
|
635
|
+
)
|
|
636
|
+
return 2
|
|
637
|
+
}
|
|
638
|
+
if (target !== production) {
|
|
639
|
+
console.log(
|
|
640
|
+
JSON.stringify({
|
|
641
|
+
applicable: false,
|
|
642
|
+
target,
|
|
643
|
+
production,
|
|
644
|
+
reason:
|
|
645
|
+
`"${target}" is not the production branch ("${production}") — dev/staging releases are ` +
|
|
646
|
+
'unversioned and untagged, identified by SHA. No version, no tag, no CHANGELOG cut.',
|
|
647
|
+
}, null, 2),
|
|
648
|
+
)
|
|
649
|
+
return 3
|
|
650
|
+
}
|
|
651
|
+
return null
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// The synopsis `--help` prints and references/versioning.md points at — the SINGLE
|
|
655
|
+
// source of truth for subcommands, flags, and exit codes. The file-header contract
|
|
656
|
+
// above points HERE rather than restating them, so there is no second list to drift
|
|
657
|
+
// against; this text is the one that must stay accurate against the commands `main`
|
|
658
|
+
// dispatches below (the doc's "run --help" pointer is only true if this exists and
|
|
659
|
+
// is correct).
|
|
660
|
+
const USAGE = `version — mechanized versioning / tagging / CHANGELOG helper for the release lane.
|
|
661
|
+
|
|
662
|
+
Usage:
|
|
663
|
+
version plan <branch-flags> [--changes <json>] [--current <v>] [--strict]
|
|
664
|
+
version changelog <branch-flags> --version <v> --date <iso> [--changes <json>] [--repo owner/name] [--include-internal] [--file CHANGELOG.md] [--dry-run --format markdown|json]
|
|
665
|
+
version rc <branch-flags> --version <v> --sha <sha> [--push] [--remote origin] [--message <msg>] [--dry-run] [--json]
|
|
666
|
+
version tag <branch-flags> --version <v> --sha <sha> [--push] [--remote origin] [--message <msg>] [--dry-run] [--json]
|
|
667
|
+
version verify --version <v> --sha <sha> [--live-sha <sha>]
|
|
668
|
+
version notes --version <v> [--file CHANGELOG.md]
|
|
669
|
+
version withdraw --version <v> [--incident <ref>] [--file CHANGELOG.md] [--dry-run]
|
|
670
|
+
|
|
671
|
+
<branch-flags> --target-branch <b> --production-branch <b> (both REQUIRED on plan|changelog|rc|tag)
|
|
672
|
+
--changes <json> JSON array of { pr, title, labels?, body?, issues? }; "-" reads stdin.
|
|
673
|
+
Optional: absent means an empty change set (plan then bumps a patch; changelog
|
|
674
|
+
cuts a section from only the accumulated Unreleased body).
|
|
675
|
+
--include-internal keep internal-typed entries (chore/docs/…) in the cut, normally excluded.
|
|
676
|
+
--message <msg> annotation for the tag object minted by rc|tag (default "Release[ candidate] <tag>").
|
|
677
|
+
--retry-from <rc-tag> mint the next immutable RC after this exact failed RC, even when it
|
|
678
|
+
names the same SHA. A repeated invocation resumes the resulting retry tag.
|
|
679
|
+
--topology-probe <file> required on rc: JSON output from preflight-probes check, proving the
|
|
680
|
+
production tag trigger derives final SemVer and candidate tag from TAG_NAME.
|
|
681
|
+
--tag-prefix <p> default "v" --strict disable the 0.x bump convention
|
|
682
|
+
--json rc|tag: emit {tag, sha, pushed, resumed}; default stdout is only the tag.
|
|
683
|
+
--dry-run supported on changelog|rc|tag|withdraw: print the declared result, touch nothing.
|
|
684
|
+
--format <mode> changelog --dry-run output: markdown emits the complete planned file; json emits
|
|
685
|
+
{version, file, noop, markdown}. Payloads use stdout; diagnostics use stderr.
|
|
686
|
+
A changelog dry-run requires an explicit format; --file - reads stdin and is
|
|
687
|
+
rejected outside dry-run mode.
|
|
688
|
+
|
|
689
|
+
Exit codes (each has exactly one meaning — an agent branches on them):
|
|
690
|
+
0 desired state reached, including idempotent no-ops (re-cut, same-SHA re-tag, already-withdrawn)
|
|
691
|
+
1 violation or error — tag collision, immutable-tag conflict, a SHA not an ancestor of production,
|
|
692
|
+
unresolvable production ref, an invalid baseline (--current is not a semver tag), a verify against
|
|
693
|
+
a tag that does not exist, tag/live-SHA mismatch, notes/withdraw with no CHANGELOG file,
|
|
694
|
+
notes requested for a version never cut, or a rejected rc push (lost race; the local tag is rolled back)
|
|
695
|
+
2 usage — a required flag is missing (both branch flags fail closed), an unknown command,
|
|
696
|
+
or a --sha that resolves to no commit in this repo
|
|
697
|
+
3 not a production promotion — no version, tag, or CHANGELOG cut to make (normal on a dev release)
|
|
698
|
+
|
|
699
|
+
Doctrine (bump rules, the CHANGELOG's two zones, the burn case): references/versioning.md.`
|
|
700
|
+
|
|
701
|
+
export function main(argv) {
|
|
702
|
+
const { opts, positional } = parseArgs(argv)
|
|
703
|
+
const cmd = positional[0]
|
|
704
|
+
const prefix = typeof opts['tag-prefix'] === 'string' ? opts['tag-prefix'] : 'v'
|
|
705
|
+
const file = typeof opts.file === 'string' ? opts.file : 'CHANGELOG.md'
|
|
706
|
+
|
|
707
|
+
if (cmd === 'help' || cmd === '-h' || opts.help || opts.h) {
|
|
708
|
+
console.log(USAGE)
|
|
709
|
+
return 0
|
|
710
|
+
}
|
|
711
|
+
if (opts.json && !['rc', 'tag'].includes(cmd)) {
|
|
712
|
+
console.error('version: --json is only valid for rc|tag.')
|
|
713
|
+
return 2
|
|
714
|
+
}
|
|
715
|
+
if (opts.format && (cmd !== 'changelog' || !opts['dry-run'])) {
|
|
716
|
+
console.error('version: --format is only valid with changelog --dry-run.')
|
|
717
|
+
return 2
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
switch (cmd) {
|
|
721
|
+
case 'plan': {
|
|
722
|
+
const gate = requireProductionTarget(opts, 'plan')
|
|
723
|
+
if (gate !== null) return gate
|
|
724
|
+
|
|
725
|
+
const changes = readChanges(opts)
|
|
726
|
+
let currentTag = typeof opts.current === 'string' ? opts.current : null
|
|
727
|
+
if (!currentTag) {
|
|
728
|
+
// The baseline is the highest release tag reachable from the PRODUCTION branch —
|
|
729
|
+
// never from HEAD. `plan` runs at the pre-flight gate, where HEAD is whatever the
|
|
730
|
+
// human had checked out (often a feature branch forked before the last release).
|
|
731
|
+
// Reading HEAD there silently misses the released tags and proposes a version
|
|
732
|
+
// BELOW one that already exists — and the collision check cannot catch it, because
|
|
733
|
+
// the too-low version is genuinely free. Version numbers would go backwards.
|
|
734
|
+
const branch = opts['production-branch']
|
|
735
|
+
if (git(['rev-parse', '--verify', '--quiet', `${branch}^{commit}`], { allowFail: true }).status !== 0) {
|
|
736
|
+
// Silence here is the same regression by another route: an unresolvable ref makes
|
|
737
|
+
// `--merged` find no tags, and a 0.0.0 baseline looks exactly like a first release.
|
|
738
|
+
console.error(`version: production branch "${branch}" does not resolve. Fetch it, or pass the ref that exists (e.g. origin/main).`)
|
|
739
|
+
return 1
|
|
740
|
+
}
|
|
741
|
+
const tags = git(['tag', '--list', `${prefix}*`, '--merged', branch], { allowFail: true }).stdout
|
|
742
|
+
const latest = latestReleaseTag(tags ? tags.split('\n').filter(Boolean) : [])
|
|
743
|
+
currentTag = latest ? latest.tag : `${prefix}0.0.0`
|
|
744
|
+
}
|
|
745
|
+
const current = parseSemver(currentTag)
|
|
746
|
+
if (!current) {
|
|
747
|
+
console.error(`version: "${currentTag}" is not a semver release tag.`)
|
|
748
|
+
return 1
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const { bump, reason } = bumpFor(changes, current, { strict: Boolean(opts.strict) })
|
|
752
|
+
const next = nextVersion(current, bump)
|
|
753
|
+
const tag = tagName(next, prefix)
|
|
754
|
+
// Deliberately an EXACT-ref check, so `v1.5.0-rc.3` does not block `v1.5.0`. The rcs
|
|
755
|
+
// are this release's own deploy triggers — the candidates for exactly this version.
|
|
756
|
+
// Treating them as a collision would make every retried release skip a version.
|
|
757
|
+
const collision = tagExists(tag)
|
|
758
|
+
|
|
759
|
+
console.log(
|
|
760
|
+
JSON.stringify({
|
|
761
|
+
applicable: true,
|
|
762
|
+
current: formatVersion(current),
|
|
763
|
+
bump,
|
|
764
|
+
reason,
|
|
765
|
+
next: formatVersion(next),
|
|
766
|
+
tag,
|
|
767
|
+
collision,
|
|
768
|
+
changes: changes.map(classifyChange).map((c) => ({
|
|
769
|
+
pr: c.pr, type: c.type, breaking: c.breaking, section: c.section, summary: c.summary,
|
|
770
|
+
})),
|
|
771
|
+
}, null, 2),
|
|
772
|
+
)
|
|
773
|
+
// A collision means this version already reached the repo — burned, or already cut.
|
|
774
|
+
// Never silently bump past it: the human decides (resume vs. next patch).
|
|
775
|
+
return collision ? 1 : 0
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
case 'changelog': {
|
|
779
|
+
const gate = requireProductionTarget(opts, 'changelog')
|
|
780
|
+
if (gate !== null) return gate
|
|
781
|
+
if (typeof opts.version !== 'string' || typeof opts.date !== 'string') {
|
|
782
|
+
console.error('version: changelog requires --version <v> --date <YYYY-MM-DD>')
|
|
783
|
+
return 2
|
|
784
|
+
}
|
|
785
|
+
if (opts['dry-run'] && !['markdown', 'json'].includes(opts.format)) {
|
|
786
|
+
console.error('version: changelog --dry-run requires --format markdown|json.')
|
|
787
|
+
return 2
|
|
788
|
+
}
|
|
789
|
+
const section = renderChangelogSection({
|
|
790
|
+
version: opts.version,
|
|
791
|
+
date: opts.date,
|
|
792
|
+
changes: readChanges(opts),
|
|
793
|
+
repo: typeof opts.repo === 'string' ? opts.repo : null,
|
|
794
|
+
includeInternal: Boolean(opts['include-internal']),
|
|
795
|
+
})
|
|
796
|
+
if (file === '-' && !opts['dry-run']) {
|
|
797
|
+
console.error('version: changelog --file - requires --dry-run.')
|
|
798
|
+
return 2
|
|
799
|
+
}
|
|
800
|
+
const existing = file === '-'
|
|
801
|
+
? fs.readFileSync(0, 'utf8')
|
|
802
|
+
: (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '')
|
|
803
|
+
const { text, noop } = insertSection(existing, section, opts.version)
|
|
804
|
+
if (noop) {
|
|
805
|
+
if (opts['dry-run']) emitChangelogPlan(opts, { version: opts.version, file, noop, markdown: text })
|
|
806
|
+
// Exit 0, not 3: the desired state is already reached, exactly like a same-SHA
|
|
807
|
+
// re-tag. Exit 3 means ONLY "not a production promotion" — overloading it here
|
|
808
|
+
// would make a resuming agent read "already cut" as "skip versioning entirely".
|
|
809
|
+
console.error(`version: [${opts.version}] already in ${file} — nothing to cut (resume).`)
|
|
810
|
+
return 0
|
|
811
|
+
}
|
|
812
|
+
if (opts['dry-run']) emitChangelogPlan(opts, { version: opts.version, file, noop, markdown: text })
|
|
813
|
+
else fs.writeFileSync(file, text)
|
|
814
|
+
return 0
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
case 'notes': {
|
|
818
|
+
if (typeof opts.version !== 'string') {
|
|
819
|
+
console.error('version: notes requires --version <v>')
|
|
820
|
+
return 2
|
|
821
|
+
}
|
|
822
|
+
if (!fs.existsSync(file)) {
|
|
823
|
+
console.error(`version: no ${file}`)
|
|
824
|
+
return 1
|
|
825
|
+
}
|
|
826
|
+
const section = extractSection(fs.readFileSync(file, 'utf8'), opts.version)
|
|
827
|
+
if (!section) {
|
|
828
|
+
// An error, not "nothing to do": the caller is about to publish release notes and
|
|
829
|
+
// there are none. Better to fail than to create an empty GitHub Release.
|
|
830
|
+
console.error(`version: no [${opts.version}] section in ${file}`)
|
|
831
|
+
return 1
|
|
832
|
+
}
|
|
833
|
+
console.log(section)
|
|
834
|
+
return 0
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
case 'withdraw': {
|
|
838
|
+
if (typeof opts.version !== 'string') {
|
|
839
|
+
console.error('version: withdraw requires --version <v> [--incident <ref>]')
|
|
840
|
+
return 2
|
|
841
|
+
}
|
|
842
|
+
if (!fs.existsSync(file)) {
|
|
843
|
+
console.error(`version: no ${file}`)
|
|
844
|
+
return 1
|
|
845
|
+
}
|
|
846
|
+
const { text, noop, reason } = markWithdrawn(fs.readFileSync(file, 'utf8'), opts.version, opts.incident)
|
|
847
|
+
if (noop) {
|
|
848
|
+
// Already withdrawn → desired state, exit 0. No such section → nothing was ever
|
|
849
|
+
// cut for that version (the tag-after-verify path), which is also correct: exit 0.
|
|
850
|
+
console.error(`version: not withdrawn (${reason}).`)
|
|
851
|
+
return 0
|
|
852
|
+
}
|
|
853
|
+
if (opts['dry-run']) console.log(text)
|
|
854
|
+
else fs.writeFileSync(file, text)
|
|
855
|
+
return 0
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
case 'rc': {
|
|
859
|
+
const gate = requireProductionTarget(opts, 'rc')
|
|
860
|
+
if (gate !== null) return gate
|
|
861
|
+
if (typeof opts.version !== 'string' || typeof opts.sha !== 'string') {
|
|
862
|
+
console.error('version: rc requires --version <v> --sha <sha>')
|
|
863
|
+
return 2
|
|
864
|
+
}
|
|
865
|
+
const base = tagName(opts.version, prefix)
|
|
866
|
+
const sha = resolveSha(opts.sha)
|
|
867
|
+
if (sha == null) {
|
|
868
|
+
console.error(`version: --sha "${opts.sha}" does not resolve to a commit in this repo.`)
|
|
869
|
+
return 2
|
|
870
|
+
}
|
|
871
|
+
const topologyProbe = validateRcTopologyProbe(opts['topology-probe'])
|
|
872
|
+
if (topologyProbe.error) {
|
|
873
|
+
console.error(`version: refusing to mint an RC: ${topologyProbe.error}`)
|
|
874
|
+
return 1
|
|
875
|
+
}
|
|
876
|
+
const remote = typeof opts.remote === 'string' ? opts.remote : 'origin'
|
|
877
|
+
const listed = git(['tag', '--list', `${base}-rc.*`], { allowFail: true }).stdout
|
|
878
|
+
const existing = rcTags(listed ? listed.split('\n').filter(Boolean) : [], base)
|
|
879
|
+
const retryFrom = typeof opts['retry-from'] === 'string' ? opts['retry-from'] : null
|
|
880
|
+
const retryFromShape = retryFrom == null ? null : rcTags([retryFrom], base)[0] || null
|
|
881
|
+
if (retryFrom != null && retryFromShape == null) {
|
|
882
|
+
console.error(`version: --retry-from ${JSON.stringify(retryFrom)} is not an RC tag for ${base}.`)
|
|
883
|
+
return 2
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Resume safety. Pushing an rc FIRES the production pipeline, so "have I already
|
|
887
|
+
// minted one?" is not a cosmetic question: a resumed session that minted rc.4, rc.5,
|
|
888
|
+
// rc.6 for one unchanged SHA would deploy the same artifact three more times. An rc
|
|
889
|
+
// already naming this exact SHA is the desired state — say so and stop. The exception
|
|
890
|
+
// is an explicit retry of a terminal failed attempt: it receives a fresh immutable RC.
|
|
891
|
+
const at = existing.filter((x) => shaForTag(x.tag) === sha)
|
|
892
|
+
const localRetryFrom = retryFrom == null ? null : existing.find((x) => x.tag === retryFrom) || null
|
|
893
|
+
if (localRetryFrom != null && shaForTag(localRetryFrom.tag) !== sha) {
|
|
894
|
+
console.error(`version: --retry-from ${retryFrom} does not name ${sha.slice(0, 8)} — refusing to retry a different candidate.`)
|
|
895
|
+
return 1
|
|
896
|
+
}
|
|
897
|
+
const localRetry = localRetryFrom == null
|
|
898
|
+
? null
|
|
899
|
+
: at.filter((x) => x.n > localRetryFrom.n).sort((a, b) => a.n - b.n)[0] || null
|
|
900
|
+
if (localRetry != null) {
|
|
901
|
+
const tag = localRetry.tag
|
|
902
|
+
let pushed = false
|
|
903
|
+
// Same convergence as `tag`: a local rc that was never pushed never deployed.
|
|
904
|
+
if (opts.push && !opts['dry-run']) {
|
|
905
|
+
const remoteSha = remoteTagSha(remote, tag)
|
|
906
|
+
if (remoteSha != null && remoteSha !== sha) {
|
|
907
|
+
console.error(
|
|
908
|
+
`version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
|
|
909
|
+
'rc tags are immutable too — that number is spent; give this SHA the next one.',
|
|
910
|
+
)
|
|
911
|
+
return 1
|
|
912
|
+
}
|
|
913
|
+
if (remoteSha == null) {
|
|
914
|
+
const push = pushTag(remote, tag)
|
|
915
|
+
if (push.status !== 0) {
|
|
916
|
+
console.error(`version: could not push ${tag} to ${remote}: ${push.stderr}`)
|
|
917
|
+
return 1
|
|
918
|
+
}
|
|
919
|
+
pushed = true
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
emitTagResult(opts, { tag, sha, pushed, resumed: true })
|
|
923
|
+
console.error(`version: ${tag} already names ${sha.slice(0, 8)} — no-op (retry resume).`)
|
|
924
|
+
return 0
|
|
925
|
+
}
|
|
926
|
+
if (at.length > 0 && retryFrom == null) {
|
|
927
|
+
const tag = at[at.length - 1].tag
|
|
928
|
+
let pushed = false
|
|
929
|
+
// Same convergence as `tag`: a local rc that was never pushed never deployed.
|
|
930
|
+
if (opts.push && !opts['dry-run']) {
|
|
931
|
+
const remoteSha = remoteTagSha(remote, tag)
|
|
932
|
+
if (remoteSha != null && remoteSha !== sha) {
|
|
933
|
+
console.error(
|
|
934
|
+
`version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
|
|
935
|
+
'rc tags are immutable too — that number is spent; give this SHA the next one.',
|
|
936
|
+
)
|
|
937
|
+
return 1
|
|
938
|
+
}
|
|
939
|
+
if (remoteSha == null) {
|
|
940
|
+
const push = pushTag(remote, tag)
|
|
941
|
+
if (push.status !== 0) {
|
|
942
|
+
console.error(`version: could not push ${tag} to ${remote}: ${push.stderr}`)
|
|
943
|
+
return 1
|
|
944
|
+
}
|
|
945
|
+
pushed = true
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
emitTagResult(opts, { tag, sha, pushed, resumed: true })
|
|
949
|
+
console.error(`version: ${tag} already names ${sha.slice(0, 8)} — no-op (resume).`)
|
|
950
|
+
return 0
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// An rc must name a SHA that is already on the production branch — the same guard `tag`
|
|
954
|
+
// applies, and for a sharper reason. Both topologies require the merge to happen first
|
|
955
|
+
// (GitFlow merges the promotion, GitHub Flow merges the rebased release branch), so an
|
|
956
|
+
// unmerged SHA here means the order was broken. Without this, minting on an unmerged
|
|
957
|
+
// branch fires a real production build and deploys a candidate that `tag` will later
|
|
958
|
+
// refuse — the contradiction surfacing only at step 5, with traffic already shifted,
|
|
959
|
+
// live on an artifact that cannot be tagged. Fail now, while it is still a no-op.
|
|
960
|
+
// Resolve against a freshly-fetched remote tip — a stale local main is not "the ship SHA".
|
|
961
|
+
{
|
|
962
|
+
const production = opts['production-branch']
|
|
963
|
+
if (typeof production === 'string') {
|
|
964
|
+
const resolved = resolveProductionAncestryRef(production, remote)
|
|
965
|
+
if (resolved.error) {
|
|
966
|
+
console.error(`version: ${resolved.error}`)
|
|
967
|
+
return 1
|
|
968
|
+
}
|
|
969
|
+
const ok = git(['merge-base', '--is-ancestor', sha, resolved.ref], { allowFail: true }).status === 0
|
|
970
|
+
if (!ok) {
|
|
971
|
+
console.error(
|
|
972
|
+
`version: ${sha.slice(0, 8)} is not an ancestor of ${resolved.ref} — refusing to mint an rc. ` +
|
|
973
|
+
'Merge the promotion first; the rc names the SHA that will ship, not one that might.',
|
|
974
|
+
)
|
|
975
|
+
return 1
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Everything below only matters when we intend to push, because only a pushed rc
|
|
981
|
+
// fires a deploy. Without --push this stays a purely local operation.
|
|
982
|
+
const willPush = Boolean(opts.push) && !opts['dry-run']
|
|
983
|
+
const remoteRcs = willPush ? remoteRcTags(remote, base) : []
|
|
984
|
+
|
|
985
|
+
// The same resume the local branch above handles, but across clones: a session that
|
|
986
|
+
// died after pushing rc.2 resumes on a machine whose checkout never fetched it. The
|
|
987
|
+
// artifact for this SHA is already built and already deployed — minting rc.3 would
|
|
988
|
+
// deploy it a second time, which is exactly what the resume guard exists to prevent.
|
|
989
|
+
const remoteAt = remoteRcs.filter((x) => x.sha === sha)
|
|
990
|
+
const remoteRetryFrom = retryFrom == null ? null : remoteRcs.find((x) => x.tag === retryFrom) || null
|
|
991
|
+
if (retryFrom != null && localRetryFrom == null && remoteRetryFrom == null) {
|
|
992
|
+
console.error(`version: --retry-from ${retryFrom} does not exist locally or on ${remote}.`)
|
|
993
|
+
return 1
|
|
994
|
+
}
|
|
995
|
+
if (remoteRetryFrom != null && remoteRetryFrom.sha !== sha) {
|
|
996
|
+
console.error(`version: --retry-from ${retryFrom} on ${remote} does not name ${sha.slice(0, 8)} — refusing to retry a different candidate.`)
|
|
997
|
+
return 1
|
|
998
|
+
}
|
|
999
|
+
const remoteRetry = remoteRetryFrom == null
|
|
1000
|
+
? null
|
|
1001
|
+
: remoteAt.filter((x) => x.n > remoteRetryFrom.n).sort((a, b) => a.n - b.n)[0] || null
|
|
1002
|
+
if (remoteRetry != null) {
|
|
1003
|
+
emitTagResult(opts, { tag: remoteRetry.tag, sha, resumed: true })
|
|
1004
|
+
console.error(`version: ${remoteRetry.tag} already names ${sha.slice(0, 8)} on ${remote} — no-op (retry resume from another clone).`)
|
|
1005
|
+
return 0
|
|
1006
|
+
}
|
|
1007
|
+
if (remoteAt.length > 0 && retryFrom == null) {
|
|
1008
|
+
const tag = remoteAt[remoteAt.length - 1].tag
|
|
1009
|
+
emitTagResult(opts, { tag, sha, resumed: true })
|
|
1010
|
+
console.error(`version: ${tag} already names ${sha.slice(0, 8)} on ${remote} — no-op (resume from another clone).`)
|
|
1011
|
+
return 0
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const highest = Math.max(0, ...existing.map((x) => x.n), ...remoteRcs.map((x) => x.n))
|
|
1015
|
+
const tag = `${base}-rc.${highest + 1}`
|
|
1016
|
+
const message = typeof opts.message === 'string' ? opts.message : `Release candidate ${tag}`
|
|
1017
|
+
if (opts['dry-run']) {
|
|
1018
|
+
emitTagResult(opts, { tag, sha })
|
|
1019
|
+
console.error(`version: would tag ${sha.slice(0, 8)} as ${tag}${opts.push ? ` and push to ${remote}` : ''}.`)
|
|
1020
|
+
return 0
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Belt and braces over the union numbering above: check this exact tag before writing
|
|
1024
|
+
// anything local. A rejected push after a successful `git tag` is the failure mode that
|
|
1025
|
+
// strands a wrong-SHA tag in the working clone, and it surfaces as a stack trace rather
|
|
1026
|
+
// than a diagnosis. Fail here instead, with a sentence that says what to do.
|
|
1027
|
+
if (willPush) {
|
|
1028
|
+
const remoteSha = remoteTagSha(remote, tag)
|
|
1029
|
+
if (remoteSha != null && remoteSha !== sha) {
|
|
1030
|
+
console.error(
|
|
1031
|
+
`version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
|
|
1032
|
+
'rc tags are immutable too — that number is spent; re-run to take the next one.',
|
|
1033
|
+
)
|
|
1034
|
+
return 1
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
git(['tag', '-a', tag, sha, '-m', message])
|
|
1039
|
+
if (opts.push) {
|
|
1040
|
+
const pushed = pushTag(remote, tag)
|
|
1041
|
+
if (pushed.status !== 0) {
|
|
1042
|
+
// Lost a race, or the remote moved between the check and the push. Undo the local
|
|
1043
|
+
// tag so the clone does not keep a candidate the remote never accepted — a stale
|
|
1044
|
+
// local rc silently poisons the NEXT run's numbering.
|
|
1045
|
+
git(['tag', '-d', tag], { allowFail: true })
|
|
1046
|
+
console.error(`version: could not push ${tag} to ${remote}: ${pushed.stderr}`)
|
|
1047
|
+
return 1
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
emitTagResult(opts, { tag, sha, pushed: Boolean(opts.push) })
|
|
1051
|
+
return 0
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
case 'tag': {
|
|
1055
|
+
const gate = requireProductionTarget(opts, 'tag')
|
|
1056
|
+
if (gate !== null) return gate
|
|
1057
|
+
if (typeof opts.version !== 'string' || typeof opts.sha !== 'string') {
|
|
1058
|
+
console.error('version: tag requires --version <v> --sha <sha>')
|
|
1059
|
+
return 2
|
|
1060
|
+
}
|
|
1061
|
+
const tag = tagName(opts.version, prefix)
|
|
1062
|
+
const sha = resolveSha(opts.sha)
|
|
1063
|
+
if (sha == null) {
|
|
1064
|
+
console.error(`version: --sha "${opts.sha}" does not resolve to a commit in this repo.`)
|
|
1065
|
+
return 2
|
|
1066
|
+
}
|
|
1067
|
+
const remote = typeof opts.remote === 'string' ? opts.remote : 'origin'
|
|
1068
|
+
|
|
1069
|
+
// Invariant 2: a tag is immutable. Same SHA → resume-safe no-op. Different SHA → stop.
|
|
1070
|
+
if (tagExists(tag)) {
|
|
1071
|
+
const at = shaForTag(tag)
|
|
1072
|
+
if (at !== sha) {
|
|
1073
|
+
console.error(
|
|
1074
|
+
`version: ${tag} already exists at ${at.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
|
|
1075
|
+
'Tags are immutable and are never moved or deleted. If that release was rolled back, ' +
|
|
1076
|
+
'the version is burned — take the next patch (references/versioning.md).',
|
|
1077
|
+
)
|
|
1078
|
+
return 1
|
|
1079
|
+
}
|
|
1080
|
+
// Resume, but CONVERGE: a local tag is no evidence the remote has it.
|
|
1081
|
+
if (opts.push && !opts['dry-run']) {
|
|
1082
|
+
const { pushed, error } = ensureRemoteTag(remote, tag, sha)
|
|
1083
|
+
if (error) {
|
|
1084
|
+
console.error(`version: ${error}`)
|
|
1085
|
+
return 1
|
|
1086
|
+
}
|
|
1087
|
+
emitTagResult(opts, { tag, sha, pushed, resumed: true })
|
|
1088
|
+
return 0
|
|
1089
|
+
}
|
|
1090
|
+
emitTagResult(opts, { tag, sha, resumed: true })
|
|
1091
|
+
console.error(`version: ${tag} already points at ${sha.slice(0, 8)} — no-op (resume).`)
|
|
1092
|
+
return 0
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// A tag must name a SHA that is actually on the production branch — against a freshly
|
|
1096
|
+
// fetched remote tip, never a potentially stale local branch (identity-v0.4.1).
|
|
1097
|
+
const production = opts['production-branch']
|
|
1098
|
+
if (typeof production === 'string') {
|
|
1099
|
+
const resolved = resolveProductionAncestryRef(production, remote)
|
|
1100
|
+
if (resolved.error) {
|
|
1101
|
+
console.error(`version: ${resolved.error}`)
|
|
1102
|
+
return 1
|
|
1103
|
+
}
|
|
1104
|
+
const ok = git(['merge-base', '--is-ancestor', sha, resolved.ref], { allowFail: true }).status === 0
|
|
1105
|
+
if (!ok) {
|
|
1106
|
+
console.error(`version: ${sha.slice(0, 8)} is not an ancestor of ${resolved.ref} — refusing to tag.`)
|
|
1107
|
+
return 1
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const message = typeof opts.message === 'string' ? opts.message : `Release ${tag}`
|
|
1112
|
+
if (opts['dry-run']) {
|
|
1113
|
+
emitTagResult(opts, { tag, sha })
|
|
1114
|
+
console.error(`version: would tag ${sha.slice(0, 8)} as ${tag}${opts.push ? ` and push to ${remote}` : ''}.`)
|
|
1115
|
+
return 0
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// The remote is the source of truth for "was this version published?" — check before
|
|
1119
|
+
// minting a local tag that could never be pushed anyway.
|
|
1120
|
+
if (opts.push) {
|
|
1121
|
+
const remoteSha = remoteTagSha(remote, tag)
|
|
1122
|
+
if (remoteSha != null && remoteSha !== sha) {
|
|
1123
|
+
console.error(`version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)} — that version is burned; take the next patch.`)
|
|
1124
|
+
return 1
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
git(['tag', '-a', tag, sha, '-m', message])
|
|
1129
|
+
if (opts.push) {
|
|
1130
|
+
const pushed = pushTag(remote, tag)
|
|
1131
|
+
if (pushed.status !== 0) {
|
|
1132
|
+
git(['tag', '-d', tag], { allowFail: true })
|
|
1133
|
+
console.error(`version: could not push ${tag} to ${remote}: ${pushed.stderr}`)
|
|
1134
|
+
return 1
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
emitTagResult(opts, { tag, sha, pushed: Boolean(opts.push) })
|
|
1138
|
+
return 0
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
case 'verify': {
|
|
1142
|
+
if (typeof opts.version !== 'string' || typeof opts.sha !== 'string') {
|
|
1143
|
+
console.error('version: verify requires --version <v> --sha <shipped-sha>')
|
|
1144
|
+
return 2
|
|
1145
|
+
}
|
|
1146
|
+
const tag = tagName(opts.version, prefix)
|
|
1147
|
+
if (!tagExists(tag)) {
|
|
1148
|
+
console.error(`version: ${tag} does not exist.`)
|
|
1149
|
+
return 1
|
|
1150
|
+
}
|
|
1151
|
+
const at = shaForTag(tag)
|
|
1152
|
+
const sha = resolveSha(opts.sha)
|
|
1153
|
+
if (sha == null) {
|
|
1154
|
+
console.error(`version: --sha "${opts.sha}" does not resolve to a commit in this repo.`)
|
|
1155
|
+
return 2
|
|
1156
|
+
}
|
|
1157
|
+
const live = typeof opts['live-sha'] === 'string' ? git(['rev-parse', opts['live-sha']], { allowFail: true }).stdout || opts['live-sha'] : null
|
|
1158
|
+
|
|
1159
|
+
const tagMatches = at === sha
|
|
1160
|
+
const liveMatches = live == null ? null : live === sha
|
|
1161
|
+
console.log(JSON.stringify({ tag, tagSha: at, shippedSha: sha, liveSha: live, tagMatches, liveMatches }, null, 2))
|
|
1162
|
+
if (!tagMatches) {
|
|
1163
|
+
console.error(`version: ${tag} names ${at.slice(0, 8)}, but the shipped SHA is ${sha.slice(0, 8)}.`)
|
|
1164
|
+
return 1
|
|
1165
|
+
}
|
|
1166
|
+
if (liveMatches === false) {
|
|
1167
|
+
console.error(`version: the live revision (${live.slice(0, 8)}) is not the promoted SHA (${sha.slice(0, 8)}).`)
|
|
1168
|
+
return 1
|
|
1169
|
+
}
|
|
1170
|
+
return 0
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
default:
|
|
1174
|
+
console.error('version: unknown command. Use plan|changelog|notes|withdraw|rc|tag|verify (see --help).')
|
|
1175
|
+
return 2
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Only run the CLI when invoked directly (so tests can import the pure functions).
|
|
1180
|
+
//
|
|
1181
|
+
// The catch is not decoration. A refusal thrown out of `main` reaches the operator as a V8 stack
|
|
1182
|
+
// trace with the message buried in the middle of it, which is what pi session 019faea6 saw and
|
|
1183
|
+
// read as "the release tooling is broken". A refusal is a designed outcome of this tool, so it
|
|
1184
|
+
// exits like one: the message on stderr, exit 2, no stack. Genuine bugs are the other case and
|
|
1185
|
+
// still owe their stack, so an error carrying no message — or one raised before the operator's
|
|
1186
|
+
// input was even read — prints in full rather than being flattened into a tidy one-liner that
|
|
1187
|
+
// hides where it came from.
|
|
1188
|
+
if (isMainModule(import.meta.url)) {
|
|
1189
|
+
try {
|
|
1190
|
+
process.exit(main(process.argv.slice(2)))
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
const message = error && typeof error.message === 'string' ? error.message : ''
|
|
1193
|
+
if (message.startsWith('version: ')) {
|
|
1194
|
+
console.error(message)
|
|
1195
|
+
process.exit(2)
|
|
1196
|
+
}
|
|
1197
|
+
throw error
|
|
1198
|
+
}
|
|
1199
|
+
}
|