@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,1538 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// metadata-sync — post-release metadata adapter for the verified-SHA GitHub Flow mode (#800).
|
|
3
|
+
//
|
|
4
|
+
// verified-sha-github-flow.md already requires: after the final tag, open a PR against current
|
|
5
|
+
// main that cuts the CHANGELOG section and synchronizes package version metadata, preserving
|
|
6
|
+
// newer unreleased work. Nothing ran it. identity v0.4.1 shipped with package.json still at
|
|
7
|
+
// 0.2.0 and no CHANGELOG [0.4.1] until a hand-rolled PR (identity#678).
|
|
8
|
+
//
|
|
9
|
+
// This script is that adapter:
|
|
10
|
+
// metadata-sync.mjs plan — compute the file edits (CHANGELOG + package versions)
|
|
11
|
+
// metadata-sync.mjs open — isolate and reconcile the exact branch, commit, and PR
|
|
12
|
+
// metadata-sync.mjs record-delivery — prove the merge, then record canonical merged state
|
|
13
|
+
// metadata-sync.mjs na — record an explicit n/a with reason (required for completeness)
|
|
14
|
+
//
|
|
15
|
+
// Pure helpers are exported so the unit suite can pin the planning/recording surface without
|
|
16
|
+
// a network. The CLI injects git/gh runners for the open path; tests pass stubs.
|
|
17
|
+
//
|
|
18
|
+
// FAIL CLOSED: open without --version/--sha is usage (exit 2). n/a without a reason is exit 2.
|
|
19
|
+
// An opened record without a PR number/url is incomplete (validateMetadataPr). A released
|
|
20
|
+
// attempt without metadataPr is incomplete in report.mjs — that is the handoff half.
|
|
21
|
+
//
|
|
22
|
+
// Doctrine: references/verified-sha-github-flow.md § Final claim and metadata ordering.
|
|
23
|
+
// Regression: identity-v0.4.1 · identity#677 (CF Insights) · identity#678 (metadata PR).
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs'
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
import os from 'node:os'
|
|
28
|
+
import { spawnSync } from 'node:child_process'
|
|
29
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
30
|
+
import { createHash } from 'node:crypto'
|
|
31
|
+
import { fileURLToPath } from 'node:url'
|
|
32
|
+
import {
|
|
33
|
+
insertSection,
|
|
34
|
+
renderChangelogSection,
|
|
35
|
+
} from './version.mjs'
|
|
36
|
+
import { METADATA_SYNC_SOURCE } from './metadata-pr-status.mjs'
|
|
37
|
+
import { RELEASE_STATE_CONTRACT } from '../../resolve-issues/scripts/release-state-contract.mjs'
|
|
38
|
+
|
|
39
|
+
const VERSION_SCRIPT = fileURLToPath(new URL('./version.mjs', import.meta.url))
|
|
40
|
+
|
|
41
|
+
export const USAGE = `metadata-sync — post-release metadata adapter (verified-sha-github-flow).
|
|
42
|
+
|
|
43
|
+
After the final release claim, open a PR against current main that cuts the CHANGELOG
|
|
44
|
+
section and bumps declared package version files, then record release.metadataPr on the
|
|
45
|
+
manifest. A released attempt without that record is incomplete unless status is n/a with
|
|
46
|
+
a reason (report.mjs).
|
|
47
|
+
plan --version <v> --date <YYYY-MM-DD> [--changes <json>] [--package-file <path>]...
|
|
48
|
+
[--cwd <dir>] [--changelog <path>] [--dry-run]
|
|
49
|
+
Compute the file edits. Prints JSON {version, branch, changelog, packages}.
|
|
50
|
+
|
|
51
|
+
open --manifest <run.json> --version <v> --sha <shipped> --date <YYYY-MM-DD>
|
|
52
|
+
[--changes <json-or-safe-relative-file>] [--package-file <path>]... [--cwd <dir>] [--changelog <path>]
|
|
53
|
+
[--repo <owner/name>] [--base <branch=main>] [--branch <name>]
|
|
54
|
+
[--title <t>] [--body <b>]
|
|
55
|
+
[--lock-file <path> --lock-command <repository-command>]
|
|
56
|
+
[--dry-run] [--record-plan]
|
|
57
|
+
Build from fresh origin/<base> in a disposable worktree. Reconcile exact repo,
|
|
58
|
+
base, branch, version, shipped SHA, expected content, commit, and PR identities;
|
|
59
|
+
refuse divergence without force. Metadata paths must be normalized, single-link
|
|
60
|
+
regular repository files (symlinks/non-regular files are rejected); a missing,
|
|
61
|
+
unparseable, or unsupported package fails closed, while a missing CHANGELOG is
|
|
62
|
+
created in the commit. Planned bytes go directly to a disposable Git index and
|
|
63
|
+
private execution tree, never caller metadata pathnames. Requested package/lock
|
|
64
|
+
versions are verified before and after ordinary commit hooks. Push is ordinary;
|
|
65
|
+
only a known Mergify pre-push rejection receives one --no-verify retry. The
|
|
66
|
+
manifest must live outside the caller repository.
|
|
67
|
+
--dry-run skips git/gh; with --record-plan also writes status:"planned".
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
record-delivery --manifest <run.json> --watch-result <watcher.json> [--cwd <dir>]
|
|
71
|
+
Consume terminal JSON from watch-pr-delivery.mjs, then independently re-read the
|
|
72
|
+
exact PR and prove commit->merge->fresh-base ancestry plus expected file hashes
|
|
73
|
+
before recording release.metadataPr.status="merged" through the canonical contract.
|
|
74
|
+
|
|
75
|
+
na --manifest <run.json> --reason <text> [--version <v>]
|
|
76
|
+
Record release.metadataPr = {status:"n/a", reason} (reason REQUIRED).
|
|
77
|
+
|
|
78
|
+
migrate --manifest <run.json>
|
|
79
|
+
Convert a legacy hand-written release.metadataSync record into a canonical
|
|
80
|
+
release.metadataPr with source metadata-sync.mjs. Reject-only by default; this
|
|
81
|
+
is the opt-in migration path.
|
|
82
|
+
|
|
83
|
+
Exit codes:
|
|
84
|
+
0 ok
|
|
85
|
+
1 open/plan failed (git/gh non-zero, incomplete PR record)
|
|
86
|
+
2 usage / unreadable input
|
|
87
|
+
|
|
88
|
+
Why: identity v0.4.1 shipped without the adapter; metadata landed as identity#678.
|
|
89
|
+
See references/verified-sha-github-flow.md and issue #800.`
|
|
90
|
+
|
|
91
|
+
// ---- pure planning / recording --------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export function defaultBranchName(version) {
|
|
94
|
+
return `chore/release-metadata-${String(version).replace(/^v/, '')}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- package-file parsers -------------------------------------------------------------------
|
|
98
|
+
//
|
|
99
|
+
// Supported version metadata: JSON (`{"version": ...}`) and TOML with a `version` key in
|
|
100
|
+
// the `[project]` or `[tool.poetry]` table (PEP 621 / Poetry). Anything else fails closed
|
|
101
|
+
// with an unsupported-format error — the adapter never guesses at a format it cannot verify.
|
|
102
|
+
|
|
103
|
+
const TOML_VERSION_TABLES = new Set(['project', 'tool.poetry'])
|
|
104
|
+
|
|
105
|
+
export function packageParserFor(rel) {
|
|
106
|
+
const base = path.basename(String(rel)).toLowerCase()
|
|
107
|
+
if (base.endsWith('.json')) return 'json'
|
|
108
|
+
// uv.lock / Cargo.lock are TOML; parsing them as TOML lets a lock requested directly
|
|
109
|
+
// fail with the actionable "use --lock-file/--lock-command" unsupported-format error.
|
|
110
|
+
if (base.endsWith('.toml') || base.endsWith('.lock')) return 'toml'
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Minimal line-based TOML reader for the version-metadata subset. Returns
|
|
115
|
+
// { version, name, line } (line = index of the version assignment) or null when no
|
|
116
|
+
// supported table carries a version key.
|
|
117
|
+
export function parseTomlMetadata(raw) {
|
|
118
|
+
const lines = String(raw).split('\n')
|
|
119
|
+
let table = null
|
|
120
|
+
let name = null
|
|
121
|
+
let hit = null
|
|
122
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
123
|
+
const header = lines[i].match(/^\s*\[([^\]]+)\]\s*$/)
|
|
124
|
+
if (header) {
|
|
125
|
+
table = header[1].trim()
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (table === 'project' && name == null) {
|
|
129
|
+
const nm = lines[i].match(/^\s*name\s*=\s*(["'])(.*?)\1/)
|
|
130
|
+
if (nm) name = nm[2]
|
|
131
|
+
}
|
|
132
|
+
if (hit == null && TOML_VERSION_TABLES.has(table)) {
|
|
133
|
+
const vm = lines[i].match(/^\s*version\s*=\s*(["'])(.*?)\1/)
|
|
134
|
+
if (vm) hit = { version: vm[2], line: i }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!hit) return null
|
|
138
|
+
return { version: hit.version, name, line: hit.line }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Rewrite only the version literal on its own line, preserving the rest of the file
|
|
142
|
+
// byte-for-byte. Returns null when the file has no supported version key.
|
|
143
|
+
export function updateTomlVersion(raw, nextVersion) {
|
|
144
|
+
const parsed = parseTomlMetadata(raw)
|
|
145
|
+
if (!parsed) return null
|
|
146
|
+
const lines = String(raw).split('\n')
|
|
147
|
+
lines[parsed.line] = lines[parsed.line].replace(
|
|
148
|
+
/^(\s*version\s*=\s*)(["']).*?\2/,
|
|
149
|
+
`$1$2${nextVersion}$2`,
|
|
150
|
+
)
|
|
151
|
+
return lines.join('\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// { version, name? } or null when the raw content cannot be read with the given parser.
|
|
155
|
+
function readPackageVersion(raw, parser) {
|
|
156
|
+
if (parser === 'json') {
|
|
157
|
+
try {
|
|
158
|
+
const json = JSON.parse(raw)
|
|
159
|
+
return {
|
|
160
|
+
version: json.version != null ? String(json.version) : null,
|
|
161
|
+
name: json.name != null ? String(json.name) : null,
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (parser === 'toml') {
|
|
168
|
+
const parsed = parseTomlMetadata(raw)
|
|
169
|
+
return parsed ? { version: parsed.version, name: parsed.name } : null
|
|
170
|
+
}
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Lock verification accepts uv.lock-style TOML and npm package-lock JSON. Both formats
|
|
175
|
+
// must identify the root project and hold the target version; an ambiguous or incomplete
|
|
176
|
+
// root entry never counts as proof.
|
|
177
|
+
export function verifyLockMetadata(raw, { name, version } = {}) {
|
|
178
|
+
if (!name) {
|
|
179
|
+
return { ok: false, reason: 'cannot verify lock metadata without a project name' }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const text = String(raw)
|
|
183
|
+
if (text.trimStart().startsWith('{')) {
|
|
184
|
+
let lock
|
|
185
|
+
try {
|
|
186
|
+
lock = JSON.parse(text)
|
|
187
|
+
} catch {
|
|
188
|
+
return { ok: false, reason: 'invalid npm package-lock JSON' }
|
|
189
|
+
}
|
|
190
|
+
const entries = []
|
|
191
|
+
if (lock && typeof lock === 'object' && !Array.isArray(lock) && ('name' in lock || 'version' in lock)) {
|
|
192
|
+
entries.push({ location: 'top-level', name: lock.name, version: lock.version })
|
|
193
|
+
}
|
|
194
|
+
const root = lock?.packages?.['']
|
|
195
|
+
if (root && typeof root === 'object' && !Array.isArray(root) && ('name' in root || 'version' in root)) {
|
|
196
|
+
entries.push({ location: 'packages[""]', name: root.name, version: root.version })
|
|
197
|
+
}
|
|
198
|
+
if (entries.length === 0) {
|
|
199
|
+
return { ok: false, reason: 'no npm root package metadata entry' }
|
|
200
|
+
}
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (entry.name == null) {
|
|
203
|
+
return { ok: false, reason: `npm root package metadata at ${entry.location} is missing name` }
|
|
204
|
+
}
|
|
205
|
+
if (String(entry.name) !== name) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
reason: `npm root package at ${entry.location} is named ${JSON.stringify(String(entry.name))}, expected ${JSON.stringify(name)}`,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (entry.version == null) {
|
|
212
|
+
return { ok: false, reason: `npm root package metadata at ${entry.location} is missing version` }
|
|
213
|
+
}
|
|
214
|
+
if (String(entry.version) !== version) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
reason: `${name} at ${entry.location} is ${JSON.stringify(String(entry.version))}, expected ${JSON.stringify(version)}`,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { ok: true }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let current = null
|
|
225
|
+
const blocks = []
|
|
226
|
+
for (const line of text.split('\n')) {
|
|
227
|
+
if (/^\s*\[\[package\]\]/.test(line)) {
|
|
228
|
+
current = {}
|
|
229
|
+
blocks.push(current)
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
if (/^\s*\[/.test(line)) {
|
|
233
|
+
current = null
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
if (current) {
|
|
237
|
+
const kv = line.match(/^\s*(name|version)\s*=\s*(["'])(.*?)\2/)
|
|
238
|
+
if (kv && current[kv[1]] == null) current[kv[1]] = kv[3]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const entry = blocks.find((b) => b.name === name)
|
|
242
|
+
if (!entry) {
|
|
243
|
+
return { ok: false, reason: `no [[package]] entry named ${JSON.stringify(name)}` }
|
|
244
|
+
}
|
|
245
|
+
if (entry.version !== version) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
reason: `${name} at ${JSON.stringify(entry.version ?? null)}, expected ${JSON.stringify(version)}`,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { ok: true }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function planMetadataSync({
|
|
255
|
+
cwd = process.cwd(),
|
|
256
|
+
version,
|
|
257
|
+
date,
|
|
258
|
+
changes = [],
|
|
259
|
+
packageFiles = ['package.json'],
|
|
260
|
+
changelogPath = 'CHANGELOG.md',
|
|
261
|
+
repo = null,
|
|
262
|
+
branch = null,
|
|
263
|
+
sources = null,
|
|
264
|
+
} = {}) {
|
|
265
|
+
if (!version) throw new Error('planMetadataSync: --version is required')
|
|
266
|
+
if (!date) throw new Error('planMetadataSync: --date is required')
|
|
267
|
+
const ver = String(version).replace(/^v/, '')
|
|
268
|
+
if (!sources) sources = filesystemSources(cwd, metadataDeclarations(packageFiles, changelogPath))
|
|
269
|
+
const absChangelog = path.resolve(cwd, changelogPath)
|
|
270
|
+
const currentChangelog = sources.get(changelogPath)?.raw ?? ''
|
|
271
|
+
const section = renderChangelogSection({ version: ver, date, changes, repo })
|
|
272
|
+
const inserted = insertSection(currentChangelog, section, ver)
|
|
273
|
+
|
|
274
|
+
const packages = []
|
|
275
|
+
for (const rel of packageFiles) {
|
|
276
|
+
const abs = path.resolve(cwd, rel)
|
|
277
|
+
const parser = packageParserFor(rel)
|
|
278
|
+
const source = sources.get(rel)
|
|
279
|
+
if (!source || source.missing) {
|
|
280
|
+
packages.push({
|
|
281
|
+
path: rel, abs, parser, from: null, to: ver,
|
|
282
|
+
missing: true, noop: false, error: `missing: ${rel}`,
|
|
283
|
+
})
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
const raw = source.raw
|
|
287
|
+
if (parser == null) {
|
|
288
|
+
packages.push({
|
|
289
|
+
path: rel, abs, parser, from: null, to: ver, noop: false,
|
|
290
|
+
error: `unsupported-format: ${rel} (supported: .json version field, .toml [project]/[tool.poetry] version)`,
|
|
291
|
+
})
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
const current = readPackageVersion(raw, parser)
|
|
295
|
+
if (!current || current.version == null) {
|
|
296
|
+
packages.push({
|
|
297
|
+
path: rel, abs, parser, from: null, to: ver, noop: false,
|
|
298
|
+
error: parser === 'toml'
|
|
299
|
+
? `unsupported-format: ${rel} has no version key in [project] or [tool.poetry]; declare a repository command via --lock-file/--lock-command or drop the file`
|
|
300
|
+
: `unparseable: ${rel} is not valid ${parser} version metadata`,
|
|
301
|
+
})
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
const from = current.version
|
|
305
|
+
if (from === ver) {
|
|
306
|
+
packages.push({ path: rel, abs, parser, from, to: ver, name: current.name, noop: true, next: raw })
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
const next = parser === 'toml'
|
|
310
|
+
? updateTomlVersion(raw, ver)
|
|
311
|
+
: `${JSON.stringify({ ...JSON.parse(raw), version: ver }, null, 2)}\n`
|
|
312
|
+
if (next == null) {
|
|
313
|
+
packages.push({
|
|
314
|
+
path: rel, abs, parser, from, to: ver, noop: false,
|
|
315
|
+
error: `unsupported-format: could not rewrite version in ${rel}`,
|
|
316
|
+
})
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
packages.push({ path: rel, abs, parser, from, to: ver, name: current.name, noop: false, next })
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
version: ver,
|
|
324
|
+
date,
|
|
325
|
+
branch: branch || defaultBranchName(ver),
|
|
326
|
+
changelogPath,
|
|
327
|
+
changelog: {
|
|
328
|
+
path: absChangelog,
|
|
329
|
+
rel: changelogPath,
|
|
330
|
+
next: inserted.text,
|
|
331
|
+
noop: !!inserted.noop,
|
|
332
|
+
},
|
|
333
|
+
packages,
|
|
334
|
+
cwd,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Requested package files that could not be planned. open refuses while this is non-empty;
|
|
339
|
+
// plan reports it and exits 1. A missing/unparseable/unsupported file is never a noop.
|
|
340
|
+
export function planPackageErrors(plan) {
|
|
341
|
+
return (plan?.packages || []).filter((p) => p.error || p.missing)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Post-apply gate: re-read every requested package file (and the declared lock, if any)
|
|
345
|
+
// from disk and prove it holds the target version. Runs before commit and before
|
|
346
|
+
// release.metadataPr is recorded — a PR record without this proof is the forex#198 bug.
|
|
347
|
+
export function verifyAppliedPlan(plan, { lock } = {}) {
|
|
348
|
+
const failures = []
|
|
349
|
+
for (const p of plan?.packages || []) {
|
|
350
|
+
if (p.error || p.missing) {
|
|
351
|
+
failures.push(`${p.path}: ${p.error || 'missing'}`)
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
let raw
|
|
355
|
+
try {
|
|
356
|
+
raw = fs.readFileSync(path.resolve(plan.cwd, p.path), 'utf8')
|
|
357
|
+
} catch (err) {
|
|
358
|
+
failures.push(`${p.path}: unreadable after apply (${err.message})`)
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
const current = readPackageVersion(raw, p.parser)
|
|
362
|
+
if (!current || current.version !== plan.version) {
|
|
363
|
+
failures.push(
|
|
364
|
+
`${p.path}: holds ${JSON.stringify(current?.version ?? null)} after apply, expected ${JSON.stringify(plan.version)}`,
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (lock) {
|
|
369
|
+
let raw = null
|
|
370
|
+
try {
|
|
371
|
+
raw = fs.readFileSync(path.resolve(plan.cwd, lock.rel), 'utf8')
|
|
372
|
+
} catch (err) {
|
|
373
|
+
failures.push(`${lock.rel}: unreadable after lock command (${err.message})`)
|
|
374
|
+
}
|
|
375
|
+
if (raw != null) {
|
|
376
|
+
const checked = verifyLockMetadata(raw, { name: lock.name, version: plan.version })
|
|
377
|
+
if (!checked.ok) failures.push(`${lock.rel}: ${checked.reason}`)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { ok: failures.length === 0, failures }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Post-commit gate: re-verify the COMMITTED tree. Commit hooks (pre-commit /
|
|
384
|
+
// commit-msg / prepare-commit-msg) run after the pre-commit verification and can
|
|
385
|
+
// rewrite — and re-stage — requested metadata, so HEAD is no longer proof by
|
|
386
|
+
// construction. `show(rel)` must run `git show HEAD:<rel>` and return the runner
|
|
387
|
+
// result ({ status, stdout, stderr }); fail closed when it is absent, when an entry
|
|
388
|
+
// is unreadable, or when any requested package file / declared lock in HEAD does not
|
|
389
|
+
// hold the target version (#960 round 2 — the recorded PR must never advertise a
|
|
390
|
+
// version the branch does not contain).
|
|
391
|
+
export function verifyCommittedPlan(plan, { lock, show } = {}) {
|
|
392
|
+
const failures = []
|
|
393
|
+
if (typeof show !== 'function') {
|
|
394
|
+
return { ok: false, failures: ['verifyCommittedPlan: a show runner (git show HEAD:<path>) is required'] }
|
|
395
|
+
}
|
|
396
|
+
for (const p of plan?.packages || []) {
|
|
397
|
+
if (p.error || p.missing) {
|
|
398
|
+
failures.push(`${p.path}: ${p.error || 'missing'}`)
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
const r = show(p.path)
|
|
402
|
+
if ((r?.status ?? 1) !== 0) {
|
|
403
|
+
failures.push(`${p.path}: unreadable in committed tree (${String(r?.stderr || r?.stdout || 'git show failed').trim()})`)
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
const current = readPackageVersion(r.stdout || '', p.parser)
|
|
407
|
+
if (!current || current.version !== plan.version) {
|
|
408
|
+
failures.push(
|
|
409
|
+
`${p.path}: committed tree holds ${JSON.stringify(current?.version ?? null)}, expected ${JSON.stringify(plan.version)}`,
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (lock) {
|
|
414
|
+
const r = show(lock.rel)
|
|
415
|
+
if ((r?.status ?? 1) !== 0) {
|
|
416
|
+
failures.push(`${lock.rel}: unreadable in committed tree (${String(r?.stderr || r?.stdout || 'git show failed').trim()})`)
|
|
417
|
+
} else {
|
|
418
|
+
const checked = verifyLockMetadata(r.stdout || '', { name: lock.name, version: plan.version })
|
|
419
|
+
if (!checked.ok) failures.push(`${lock.rel}: committed tree: ${checked.reason}`)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { ok: failures.length === 0, failures }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
export function applyMetadataFiles(plan, { runGit } = {}) {
|
|
427
|
+
if (!plan) throw new Error('applyMetadataFiles: plan required')
|
|
428
|
+
if (typeof runGit !== 'function') {
|
|
429
|
+
throw new Error('applyMetadataFiles: a disposable Git index runner is required')
|
|
430
|
+
}
|
|
431
|
+
stageMetadataPlan(runGit, plan)
|
|
432
|
+
return plan
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function validateMetadataPr(meta) {
|
|
436
|
+
if (!meta || typeof meta !== 'object') {
|
|
437
|
+
return { ok: false, reason: 'missing release.metadataPr' }
|
|
438
|
+
}
|
|
439
|
+
if (meta.status === 'n/a') {
|
|
440
|
+
if (!meta.reason || !String(meta.reason).trim()) {
|
|
441
|
+
return { ok: false, reason: 'n/a requires a reason' }
|
|
442
|
+
}
|
|
443
|
+
return { ok: true }
|
|
444
|
+
}
|
|
445
|
+
if (meta.status === 'opened' || meta.status === 'merged') {
|
|
446
|
+
if (meta.number == null && !meta.url) {
|
|
447
|
+
return { ok: false, reason: `${meta.status} requires number or url` }
|
|
448
|
+
}
|
|
449
|
+
return { ok: true }
|
|
450
|
+
}
|
|
451
|
+
if (meta.status === 'planned') {
|
|
452
|
+
// dry-run / record-plan intermediate — not complete for handoff, but structurally valid
|
|
453
|
+
return { ok: true, complete: false }
|
|
454
|
+
}
|
|
455
|
+
return { ok: false, reason: `unknown status ${JSON.stringify(meta.status)}` }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function recordMetadataPr(manifest, entry) {
|
|
459
|
+
const m = structuredClone(manifest || {})
|
|
460
|
+
if (!m.release || typeof m.release !== 'object') m.release = {}
|
|
461
|
+
const checked = validateMetadataPr(entry)
|
|
462
|
+
if (!checked.ok) throw new Error(`recordMetadataPr: ${checked.reason}`)
|
|
463
|
+
m.release.metadataPr = {
|
|
464
|
+
...entry,
|
|
465
|
+
source: entry.source || METADATA_SYNC_SOURCE,
|
|
466
|
+
recordedAt: entry.recordedAt || new Date().toISOString(),
|
|
467
|
+
}
|
|
468
|
+
return m
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Opt-in migration from a hand-written `release.metadataSync` to the canonical
|
|
472
|
+
// `release.metadataPr`. Hand-written records do NOT satisfy the completeness gate
|
|
473
|
+
// on their own; this helper lets an operator convert them under adapter control.
|
|
474
|
+
export function migrateMetadataSync(manifest) {
|
|
475
|
+
const m = manifest || {}
|
|
476
|
+
const r = m.release || {}
|
|
477
|
+
const legacy = r.metadataSync
|
|
478
|
+
if (legacy == null) {
|
|
479
|
+
throw new Error('migrateMetadataSync: no release.metadataSync to migrate')
|
|
480
|
+
}
|
|
481
|
+
if (typeof legacy === 'string') {
|
|
482
|
+
return {
|
|
483
|
+
status: 'opened',
|
|
484
|
+
number: parsePrNumber(legacy),
|
|
485
|
+
url: legacy,
|
|
486
|
+
source: METADATA_SYNC_SOURCE,
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (typeof legacy !== 'object' || Array.isArray(legacy)) {
|
|
490
|
+
throw new Error(`migrateMetadataSync: cannot migrate metadataSync of this shape (${typeof legacy})`)
|
|
491
|
+
}
|
|
492
|
+
if (legacy.status === 'n/a') {
|
|
493
|
+
return {
|
|
494
|
+
status: 'n/a',
|
|
495
|
+
reason: legacy.reason,
|
|
496
|
+
version: legacy.version || null,
|
|
497
|
+
source: METADATA_SYNC_SOURCE,
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
status: 'opened',
|
|
502
|
+
number: legacy.number != null ? legacy.number : null,
|
|
503
|
+
url: legacy.url || null,
|
|
504
|
+
version: legacy.version || null,
|
|
505
|
+
source: METADATA_SYNC_SOURCE,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function parsePrNumber(urlOrText) {
|
|
510
|
+
const text = String(urlOrText || '')
|
|
511
|
+
const m = text.match(/\/pull\/(\d+)/) || text.match(/#(\d+)\b/)
|
|
512
|
+
return m ? Number(m[1]) : null
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ---- CLI ------------------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
export function parseArgs(argv) {
|
|
518
|
+
const args = { packageFiles: [], positional: [] }
|
|
519
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
520
|
+
const a = argv[i]
|
|
521
|
+
if (a === '--help' || a === '-h') args.help = true
|
|
522
|
+
else if (a === '--manifest') args.manifest = argv[++i]
|
|
523
|
+
else if (a === '--version') args.version = argv[++i]
|
|
524
|
+
else if (a === '--sha') args.sha = argv[++i]
|
|
525
|
+
else if (a === '--date') args.date = argv[++i]
|
|
526
|
+
else if (a === '--changes') args.changes = argv[++i]
|
|
527
|
+
else if (a === '--package-file') args.packageFiles.push(argv[++i])
|
|
528
|
+
else if (a === '--cwd') args.cwd = argv[++i]
|
|
529
|
+
else if (a === '--changelog') args.changelog = argv[++i]
|
|
530
|
+
else if (a === '--base') args.base = argv[++i]
|
|
531
|
+
else if (a === '--branch') args.branch = argv[++i]
|
|
532
|
+
else if (a === '--title') args.title = argv[++i]
|
|
533
|
+
else if (a === '--body') args.body = argv[++i]
|
|
534
|
+
else if (a === '--reason') args.reason = argv[++i]
|
|
535
|
+
else if (a === '--repo') args.repo = argv[++i]
|
|
536
|
+
else if (a === '--watch-result') args.watchResult = argv[++i]
|
|
537
|
+
else if (a === '--lock-file') args.lockFile = argv[++i]
|
|
538
|
+
else if (a === '--lock-command') args.lockCommand = argv[++i]
|
|
539
|
+
else if (a === '--dry-run') args.dryRun = true
|
|
540
|
+
else if (a === '--record-plan') args.recordPlan = true
|
|
541
|
+
else if (a === '--json') args.json = true
|
|
542
|
+
else if (a.startsWith('-')) args.unknown = a
|
|
543
|
+
else args.positional.push(a)
|
|
544
|
+
}
|
|
545
|
+
args.cmd = args.positional[0] || null
|
|
546
|
+
if (args.packageFiles.length === 0) args.packageFiles = ['package.json']
|
|
547
|
+
return args
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function loadJson(file) {
|
|
551
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function writeJson(file, value) {
|
|
555
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function parseChanges(raw, cwd, runGit = null) {
|
|
559
|
+
if (!raw) return []
|
|
560
|
+
if (raw.startsWith('[') || raw.startsWith('{')) {
|
|
561
|
+
const parsed = JSON.parse(raw)
|
|
562
|
+
return Array.isArray(parsed) ? parsed : [parsed]
|
|
563
|
+
}
|
|
564
|
+
const rel = safeRelativePath(raw, '--changes file')
|
|
565
|
+
const declarations = [{ rel, label: '--changes file' }]
|
|
566
|
+
const materialized = filesystemSources(cwd, declarations).get(rel)
|
|
567
|
+
const source = runGit ? gitTreeSources(runGit, cwd, declarations).get(rel) : materialized
|
|
568
|
+
if (!source || source.missing) {
|
|
569
|
+
throw new Error(`metadata-sync: --changes file ${JSON.stringify(rel)} must exist as a regular repository file`)
|
|
570
|
+
}
|
|
571
|
+
const parsed = JSON.parse(source.raw)
|
|
572
|
+
return Array.isArray(parsed) ? parsed : [parsed]
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function defaultRun(cmd, args, { cwd, input } = {}) {
|
|
576
|
+
return spawnSync(cmd, args, { encoding: 'utf8', cwd, input })
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function safeRelativePath(value, label) {
|
|
580
|
+
if (typeof value !== 'string' || value === '' || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
|
|
581
|
+
throw new Error(`metadata-sync: ${label} must be a relative path inside the disposable worktree`)
|
|
582
|
+
}
|
|
583
|
+
const segments = value.split(/[\\/]/)
|
|
584
|
+
if (segments.includes('..')) throw new Error(`metadata-sync: ${label} may not contain .. segments`)
|
|
585
|
+
return value
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function assertManifestOutsideCaller(file, cwd, runGit) {
|
|
589
|
+
const root = checked(runGit(['rev-parse', '--show-toplevel']), 'git rev-parse --show-toplevel').stdout.trim()
|
|
590
|
+
const rootReal = fs.realpathSync(root)
|
|
591
|
+
const manifestReal = fs.realpathSync(file)
|
|
592
|
+
const relative = path.relative(rootReal, manifestReal)
|
|
593
|
+
const inside = relative === '' || (
|
|
594
|
+
relative !== '..' &&
|
|
595
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
596
|
+
!path.isAbsolute(relative)
|
|
597
|
+
)
|
|
598
|
+
if (inside) {
|
|
599
|
+
throw new Error('metadata-sync: --manifest must live outside the caller repository so caller bytes/status remain unchanged')
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function redact(value) {
|
|
604
|
+
return String(value || '')
|
|
605
|
+
.replace(/:\/\/[^\s/@:]+:[^\s/@]+@/g, '://[REDACTED]@')
|
|
606
|
+
.replace(/(gh[opsu]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})/g, '[REDACTED]')
|
|
607
|
+
.trim()
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function checked(result, label) {
|
|
611
|
+
if ((result.status ?? 1) !== 0) {
|
|
612
|
+
throw new Error(`${label} failed: ${redact(result.stderr || result.stdout || '')}`.trim())
|
|
613
|
+
}
|
|
614
|
+
return result
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function parseJsonOutput(result, label) {
|
|
618
|
+
checked(result, label)
|
|
619
|
+
try {
|
|
620
|
+
return JSON.parse(String(result.stdout || ''))
|
|
621
|
+
} catch (error) {
|
|
622
|
+
throw new Error(`${label} returned invalid JSON on stdout: ${error.message}`)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function sha256(buffer) {
|
|
627
|
+
return createHash('sha256').update(buffer).digest('hex')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function validateRepo(repo) {
|
|
631
|
+
if (typeof repo !== 'string' || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
632
|
+
throw new Error('metadata-sync: target repo must be an exact owner/name identity')
|
|
633
|
+
}
|
|
634
|
+
return repo
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function repoFromRemoteUrl(value) {
|
|
638
|
+
const text = String(value || '').trim().replace(/\.git$/, '')
|
|
639
|
+
const scp = /^[^@\s]+@[^:]+:(.+)$/.exec(text)
|
|
640
|
+
if (scp) return scp[1].split('/').slice(-2).join('/')
|
|
641
|
+
try {
|
|
642
|
+
const parsed = new URL(text)
|
|
643
|
+
return parsed.pathname.replace(/^\//, '').split('/').slice(-2).join('/')
|
|
644
|
+
} catch {}
|
|
645
|
+
const parts = text.split(/[\\/]/).filter(Boolean)
|
|
646
|
+
return parts.slice(-2).join('/')
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function resolveRef(runGit, ref, cwd) {
|
|
650
|
+
const result = runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { cwd })
|
|
651
|
+
return (result.status ?? 1) === 0 ? String(result.stdout || '').trim() : null
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function gitTreeSources(runGit, cwd, declarations) {
|
|
655
|
+
const sources = new Map()
|
|
656
|
+
const seen = new Set()
|
|
657
|
+
for (const declaration of declarations) {
|
|
658
|
+
const rel = safeRelativePath(declaration.rel, declaration.label)
|
|
659
|
+
const normalized = path.normalize(rel)
|
|
660
|
+
if (normalized === '.' || normalized !== rel || rel.includes('\0')) {
|
|
661
|
+
throw new Error(`metadata-sync: ${declaration.label} must be a normalized relative path`)
|
|
662
|
+
}
|
|
663
|
+
if (seen.has(normalized)) {
|
|
664
|
+
throw new Error(`metadata-sync: metadata path ${JSON.stringify(normalized)} is declared more than once`)
|
|
665
|
+
}
|
|
666
|
+
seen.add(normalized)
|
|
667
|
+
const result = runGit(['ls-tree', '-z', 'HEAD', '--', normalized], { cwd })
|
|
668
|
+
checked(result, `inspect ${declaration.label}`)
|
|
669
|
+
const row = String(result.stdout || '')
|
|
670
|
+
if (!row) {
|
|
671
|
+
sources.set(normalized, { missing: true, raw: '' })
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
const tab = row.indexOf('\t')
|
|
675
|
+
const fields = row.slice(0, tab).trim().split(/\s+/)
|
|
676
|
+
const foundPath = row.slice(tab + 1).replace(/\0$/, '')
|
|
677
|
+
if (tab < 0 || foundPath !== normalized || fields.length !== 3) {
|
|
678
|
+
throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} has ambiguous Git identity`)
|
|
679
|
+
}
|
|
680
|
+
const [mode, type, object] = fields
|
|
681
|
+
if (!['100644', '100755'].includes(mode) || type !== 'blob') {
|
|
682
|
+
throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} must be a regular repository file; symlinks and non-regular entries are rejected`)
|
|
683
|
+
}
|
|
684
|
+
const raw = checked(runGit(['cat-file', 'blob', object], { cwd }), `read ${declaration.label}`).stdout
|
|
685
|
+
sources.set(normalized, { missing: false, raw, mode })
|
|
686
|
+
}
|
|
687
|
+
return sources
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function filesystemSources(cwd, declarations) {
|
|
691
|
+
if (!fs.constants.O_NOFOLLOW) {
|
|
692
|
+
throw new Error('metadata-sync: this platform cannot enforce no-follow metadata reads')
|
|
693
|
+
}
|
|
694
|
+
const root = path.resolve(cwd)
|
|
695
|
+
const rootReal = fs.realpathSync(root)
|
|
696
|
+
const sources = new Map()
|
|
697
|
+
const seen = new Set()
|
|
698
|
+
for (const declaration of declarations) {
|
|
699
|
+
const rel = safeRelativePath(declaration.rel, declaration.label)
|
|
700
|
+
const normalized = path.normalize(rel)
|
|
701
|
+
if (normalized === '.' || normalized !== rel || rel.includes('\0')) {
|
|
702
|
+
throw new Error(`metadata-sync: ${declaration.label} must be a normalized relative path`)
|
|
703
|
+
}
|
|
704
|
+
if (seen.has(normalized)) {
|
|
705
|
+
throw new Error(`metadata-sync: metadata path ${JSON.stringify(normalized)} is declared more than once`)
|
|
706
|
+
}
|
|
707
|
+
seen.add(normalized)
|
|
708
|
+
const abs = path.join(root, normalized)
|
|
709
|
+
let parent = path.dirname(abs)
|
|
710
|
+
let missingParent = false
|
|
711
|
+
while (true) {
|
|
712
|
+
let stat
|
|
713
|
+
try {
|
|
714
|
+
stat = fs.lstatSync(parent, { bigint: true })
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (error.code !== 'ENOENT') throw error
|
|
717
|
+
missingParent = true
|
|
718
|
+
parent = path.dirname(parent)
|
|
719
|
+
continue
|
|
720
|
+
}
|
|
721
|
+
const relative = path.relative(rootReal, fs.realpathSync(parent))
|
|
722
|
+
const contained = relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
723
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || !contained) {
|
|
724
|
+
throw new Error(`metadata-sync: parent of ${declaration.label} ${JSON.stringify(normalized)} must be a contained real directory`)
|
|
725
|
+
}
|
|
726
|
+
if (parent === root) break
|
|
727
|
+
parent = path.dirname(parent)
|
|
728
|
+
}
|
|
729
|
+
if (missingParent) {
|
|
730
|
+
sources.set(normalized, { missing: true, raw: '' })
|
|
731
|
+
continue
|
|
732
|
+
}
|
|
733
|
+
let before
|
|
734
|
+
try {
|
|
735
|
+
before = fs.lstatSync(abs, { bigint: true })
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (error.code !== 'ENOENT') throw error
|
|
738
|
+
sources.set(normalized, { missing: true, raw: '' })
|
|
739
|
+
continue
|
|
740
|
+
}
|
|
741
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1n) {
|
|
742
|
+
throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} must be a single-link regular file; symlinks and non-regular files are rejected`)
|
|
743
|
+
}
|
|
744
|
+
const targetRelative = path.relative(rootReal, fs.realpathSync(abs))
|
|
745
|
+
if (targetRelative === '..' || targetRelative.startsWith(`..${path.sep}`) || path.isAbsolute(targetRelative)) {
|
|
746
|
+
throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} escapes the workspace`)
|
|
747
|
+
}
|
|
748
|
+
const fd = fs.openSync(abs, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
|
|
749
|
+
try {
|
|
750
|
+
const opened = fs.fstatSync(fd, { bigint: true })
|
|
751
|
+
if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
752
|
+
throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} changed during validation`)
|
|
753
|
+
}
|
|
754
|
+
sources.set(normalized, { missing: false, raw: fs.readFileSync(fd, 'utf8') })
|
|
755
|
+
} finally {
|
|
756
|
+
fs.closeSync(fd)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return sources
|
|
760
|
+
}
|
|
761
|
+
function writePlannedMetadataFiles(plan, cwd) {
|
|
762
|
+
const entries = [
|
|
763
|
+
{ rel: plan.changelog.rel, next: plan.changelog.next, noop: plan.changelog.noop },
|
|
764
|
+
...plan.packages.map((item) => ({ rel: item.path, next: item.next, noop: item.noop || item.missing || item.error })),
|
|
765
|
+
]
|
|
766
|
+
for (const entry of entries) {
|
|
767
|
+
if (entry.noop) continue
|
|
768
|
+
const target = path.resolve(cwd, entry.rel)
|
|
769
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
770
|
+
fs.writeFileSync(target, entry.next)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function createDryRunExecutionTree(cwd, plan) {
|
|
775
|
+
const executionTree = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-sync-dry-run-'))
|
|
776
|
+
try {
|
|
777
|
+
fs.cpSync(cwd, executionTree, {
|
|
778
|
+
recursive: true,
|
|
779
|
+
filter(source) {
|
|
780
|
+
const relative = path.relative(cwd, source)
|
|
781
|
+
const firstSegment = relative.split(path.sep)[0]
|
|
782
|
+
return !['.git', 'node_modules'].includes(firstSegment)
|
|
783
|
+
},
|
|
784
|
+
})
|
|
785
|
+
writePlannedMetadataFiles(plan, executionTree)
|
|
786
|
+
return executionTree
|
|
787
|
+
} catch (error) {
|
|
788
|
+
fs.rmSync(executionTree, { recursive: true, force: true })
|
|
789
|
+
throw error
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function runLockCommandAndVerify({ lockFile, lockCommand, plan, cwd, exec, stageLock = null }) {
|
|
794
|
+
if (!lockFile) return null
|
|
795
|
+
const lockRel = safeRelativePath(lockFile, '--lock-file')
|
|
796
|
+
const source = filesystemSources(cwd, [{ rel: lockRel, label: '--lock-file' }]).get(lockRel)
|
|
797
|
+
const lockBefore = source?.missing ? null : source.raw
|
|
798
|
+
const lockResult = exec(lockCommand, { cwd })
|
|
799
|
+
if ((lockResult.status ?? 1) !== 0) {
|
|
800
|
+
throw new Error(`metadata-sync: lock command failed (${lockCommand}): ${redact(lockResult.stderr || lockResult.stdout)}`)
|
|
801
|
+
}
|
|
802
|
+
const generated = filesystemSources(cwd, [{ rel: lockRel, label: '--lock-file' }]).get(lockRel)
|
|
803
|
+
if (!generated || generated.missing) {
|
|
804
|
+
throw new Error(`metadata-sync: declared lock ${lockRel} is missing after lock command`)
|
|
805
|
+
}
|
|
806
|
+
const name = plan.packages.find((item) => item.name)?.name || null
|
|
807
|
+
const lock = { rel: lockRel, raw: generated.raw, name }
|
|
808
|
+
if (generated.raw !== lockBefore && stageLock) stageLock(lock)
|
|
809
|
+
return lock
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function versionChangelogPlan({ cwd, version, date, changes, repo, base, changelogRaw, runVersion }) {
|
|
813
|
+
const result = runVersion([
|
|
814
|
+
VERSION_SCRIPT, 'changelog', '--target-branch', base, '--production-branch', base,
|
|
815
|
+
'--version', version, '--date', date, '--changes', JSON.stringify(changes),
|
|
816
|
+
'--repo', repo, '--file', '-', '--dry-run', '--format', 'json',
|
|
817
|
+
], { cwd, input: Buffer.from(changelogRaw) })
|
|
818
|
+
const payload = parseJsonOutput(result, 'version changelog --dry-run --format json')
|
|
819
|
+
const normalizedVersion = String(version).replace(/^v/, '')
|
|
820
|
+
if (payload.version !== normalizedVersion || payload.file !== '-' || typeof payload.markdown !== 'string') {
|
|
821
|
+
throw new Error('metadata-sync: version changelog JSON does not match the requested version/file contract')
|
|
822
|
+
}
|
|
823
|
+
return payload
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function planFromSources({ cwd, version, date, changes, packageFiles, changelogPath, repo, branch, base, runVersion, sources }) {
|
|
827
|
+
const safeChangelog = safeRelativePath(changelogPath, '--changelog')
|
|
828
|
+
const safePackages = packageFiles.map((item) => safeRelativePath(item, '--package-file'))
|
|
829
|
+
const changelog = versionChangelogPlan({
|
|
830
|
+
cwd, version, date, changes, repo, base,
|
|
831
|
+
changelogRaw: sources.get(safeChangelog)?.raw ?? '', runVersion,
|
|
832
|
+
})
|
|
833
|
+
const plan = planMetadataSync({
|
|
834
|
+
cwd, version, date, changes: [], packageFiles: safePackages,
|
|
835
|
+
changelogPath: safeChangelog, repo, branch, sources,
|
|
836
|
+
})
|
|
837
|
+
plan.cwd = cwd
|
|
838
|
+
plan.changelog.next = changelog.markdown
|
|
839
|
+
plan.changelog.noop = changelog.noop
|
|
840
|
+
return plan
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function metadataDeclarations(packageFiles, changelogPath, lockFile = null) {
|
|
844
|
+
return [
|
|
845
|
+
{ rel: changelogPath, label: '--changelog' },
|
|
846
|
+
...packageFiles.map((rel) => ({ rel, label: '--package-file' })),
|
|
847
|
+
...(lockFile ? [{ rel: lockFile, label: '--lock-file' }] : []),
|
|
848
|
+
]
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function planInWorkspace(options) {
|
|
852
|
+
const sources = filesystemSources(
|
|
853
|
+
options.cwd,
|
|
854
|
+
metadataDeclarations(options.packageFiles, options.changelogPath, options.lockFile),
|
|
855
|
+
)
|
|
856
|
+
return planFromSources({ ...options, sources })
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function planFromGitTree(runGit, options) {
|
|
860
|
+
const declarations = metadataDeclarations(options.packageFiles, options.changelogPath, options.lockFile)
|
|
861
|
+
filesystemSources(options.cwd, declarations)
|
|
862
|
+
const sources = gitTreeSources(runGit, options.cwd, declarations)
|
|
863
|
+
return planFromSources({ ...options, sources })
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function stageMetadataPlan(runGit, plan) {
|
|
867
|
+
const entries = [
|
|
868
|
+
{ rel: plan.changelog.rel, next: plan.changelog.next, noop: plan.changelog.noop },
|
|
869
|
+
...plan.packages.map((item) => ({ rel: item.path, next: item.next, noop: item.noop || item.missing || item.error })),
|
|
870
|
+
]
|
|
871
|
+
for (const entry of entries) {
|
|
872
|
+
if (entry.noop) continue
|
|
873
|
+
const blob = checked(
|
|
874
|
+
runGit(['hash-object', '-w', '--stdin'], { cwd: plan.cwd, input: entry.next }),
|
|
875
|
+
`stage metadata blob ${entry.rel}`,
|
|
876
|
+
).stdout.trim()
|
|
877
|
+
if (!/^[0-9a-f]{40,64}$/.test(blob)) {
|
|
878
|
+
throw new Error(`metadata-sync: invalid blob identity for ${entry.rel}`)
|
|
879
|
+
}
|
|
880
|
+
checked(
|
|
881
|
+
runGit(['update-index', '--add', '--cacheinfo', `100644,${blob},${entry.rel}`], { cwd: plan.cwd }),
|
|
882
|
+
`stage metadata path ${entry.rel}`,
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function contentIdentity(plan, lock = null) {
|
|
888
|
+
const entries = [
|
|
889
|
+
{ path: plan.changelog.rel, content: plan.changelog.next },
|
|
890
|
+
...plan.packages
|
|
891
|
+
.filter((item) => !item.missing && !item.error)
|
|
892
|
+
.map((item) => ({ path: item.path, content: item.next })),
|
|
893
|
+
...(lock ? [{ path: lock.rel, content: lock.raw }] : []),
|
|
894
|
+
]
|
|
895
|
+
return entries.sort((left, right) => left.path.localeCompare(right.path)).map((entry) => ({
|
|
896
|
+
path: entry.path,
|
|
897
|
+
sha256: sha256(Buffer.from(entry.content)),
|
|
898
|
+
}))
|
|
899
|
+
}
|
|
900
|
+
function assertCommitIdentity(runGit, { cwd, commit, baseSha, changedPaths, content, title, body }) {
|
|
901
|
+
const row = checked(runGit(['rev-list', '--parents', '-n', '1', commit], { cwd }), `inspect commit ${commit}`).stdout.trim().split(/\s+/)
|
|
902
|
+
if (row.length !== 2) {
|
|
903
|
+
throw new Error(`metadata-sync: commit ${commit} must have exactly one parent`)
|
|
904
|
+
}
|
|
905
|
+
if ((runGit(['merge-base', '--is-ancestor', row[1], baseSha], { cwd }).status ?? 1) !== 0) {
|
|
906
|
+
throw new Error(`metadata-sync: commit ${commit} is not based on an ancestor of fresh base ${baseSha}`)
|
|
907
|
+
}
|
|
908
|
+
const changed = checked(
|
|
909
|
+
runGit(['diff-tree', '--no-commit-id', '--name-only', '-r', commit], { cwd }),
|
|
910
|
+
`inspect changed paths ${commit}`,
|
|
911
|
+
).stdout.split('\n').filter(Boolean).sort()
|
|
912
|
+
const expectedPaths = [...changedPaths].sort()
|
|
913
|
+
if (!sameJson(changed, expectedPaths)) {
|
|
914
|
+
throw new Error(`metadata-sync: commit ${commit} changed paths do not match expected metadata content`)
|
|
915
|
+
}
|
|
916
|
+
for (const expected of content) {
|
|
917
|
+
const blob = checked(runGit(['show', `${commit}:${expected.path}`], { cwd }), `read ${commit}:${expected.path}`).stdout
|
|
918
|
+
if (sha256(Buffer.from(blob)) !== expected.sha256) {
|
|
919
|
+
throw new Error(`metadata-sync: commit ${commit} content diverges at ${expected.path}`)
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const actualMessage = checked(
|
|
923
|
+
runGit(['show', '-s', '--format=%B', commit], { cwd }),
|
|
924
|
+
`inspect message ${commit}`,
|
|
925
|
+
).stdout.trim()
|
|
926
|
+
if (actualMessage !== `${title}\n\n${body}`.trim()) {
|
|
927
|
+
throw new Error(`metadata-sync: commit ${commit} message does not match the exact metadata identity`)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function sameJson(left, right) {
|
|
932
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function assertRecordedIdentity(meta, expected) {
|
|
936
|
+
if (!meta) return
|
|
937
|
+
for (const field of ['repo', 'base', 'branch', 'version', 'sha']) {
|
|
938
|
+
if (meta[field] != null && meta[field] !== expected[field]) {
|
|
939
|
+
throw new Error(`metadata-sync: recorded ${field} ${JSON.stringify(meta[field])} does not match ${JSON.stringify(expected[field])}`)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (meta.content != null && !sameJson(meta.content, expected.content)) {
|
|
943
|
+
throw new Error('metadata-sync: recorded expected content does not match the current metadata plan')
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function normalizePr(raw) {
|
|
948
|
+
const headOwner = raw?.headRepositoryOwner?.login || null
|
|
949
|
+
const headName = raw?.headRepository?.name || null
|
|
950
|
+
return {
|
|
951
|
+
number: raw?.number ?? null,
|
|
952
|
+
url: raw?.url || null,
|
|
953
|
+
state: raw?.state || null,
|
|
954
|
+
base: raw?.baseRefName || null,
|
|
955
|
+
branch: raw?.headRefName || null,
|
|
956
|
+
commit: raw?.headRefOid || null,
|
|
957
|
+
repo: raw?.headRepository?.nameWithOwner || (headOwner && headName ? `${headOwner}/${headName}` : null),
|
|
958
|
+
mergeCommit: raw?.mergeCommit?.oid || null,
|
|
959
|
+
mergedAt: raw?.mergedAt || null,
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function assertPrCoordinates(pr, expected) {
|
|
964
|
+
if (pr.repo !== expected.repo || pr.base !== expected.base || pr.branch !== expected.branch) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
`metadata-sync: PR identity mismatch for #${pr.number ?? 'unknown'}; ` +
|
|
967
|
+
`expected ${expected.repo} ${expected.base}<-${expected.branch}`,
|
|
968
|
+
)
|
|
969
|
+
}
|
|
970
|
+
return pr
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function assertPrIdentity(pr, expected) {
|
|
974
|
+
assertPrCoordinates(pr, expected)
|
|
975
|
+
if (pr.commit !== expected.commit) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`metadata-sync: PR identity mismatch for #${pr.number ?? 'unknown'}; ` +
|
|
978
|
+
`expected ${expected.repo} ${expected.base}<-${expected.branch}@${expected.commit}`,
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
return pr
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const PR_FIELDS = 'number,url,state,baseRefName,headRefName,headRefOid,headRepository,headRepositoryOwner,mergeCommit,mergedAt'
|
|
985
|
+
|
|
986
|
+
function findExistingPr({ gh, cwd, repo, branch, recordedNumber }) {
|
|
987
|
+
if (recordedNumber != null) {
|
|
988
|
+
return normalizePr(parseJsonOutput(
|
|
989
|
+
gh(['pr', 'view', String(recordedNumber), '--repo', repo, '--json', PR_FIELDS], { cwd }),
|
|
990
|
+
`gh pr view ${recordedNumber}`,
|
|
991
|
+
))
|
|
992
|
+
}
|
|
993
|
+
const rows = parseJsonOutput(
|
|
994
|
+
gh(['pr', 'list', '--repo', repo, '--state', 'all', '--head', branch, '--json', PR_FIELDS], { cwd }),
|
|
995
|
+
'gh pr list',
|
|
996
|
+
)
|
|
997
|
+
if (!Array.isArray(rows)) throw new Error('metadata-sync: gh pr list did not return an array')
|
|
998
|
+
if (rows.length > 1) {
|
|
999
|
+
throw new Error(`metadata-sync: multiple PRs claim head ${branch}; refusing ambiguous adoption`)
|
|
1000
|
+
}
|
|
1001
|
+
return rows.length === 1 ? normalizePr(rows[0]) : null
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function createPr({ gh, cwd, repo, base, branch, title, body }) {
|
|
1005
|
+
const created = checked(gh([
|
|
1006
|
+
'pr', 'create', '--repo', repo, '--base', base, '--head', branch,
|
|
1007
|
+
'--title', title, '--body', body,
|
|
1008
|
+
], { cwd }), 'gh pr create')
|
|
1009
|
+
const url = String(created.stdout || '').trim().split('\n').filter(Boolean).pop()
|
|
1010
|
+
const number = parsePrNumber(url)
|
|
1011
|
+
if (number == null) throw new Error('metadata-sync: gh pr create returned no parseable PR URL')
|
|
1012
|
+
return findExistingPr({ gh, cwd, repo, branch, recordedNumber: number })
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function recordMergedDelivery({ args, cwd, git, gh, now }) {
|
|
1016
|
+
if (!args.manifest || !args.watchResult) {
|
|
1017
|
+
throw new Error('metadata-sync: record-delivery requires --manifest and --watch-result')
|
|
1018
|
+
}
|
|
1019
|
+
const runGit = (gitArgs) => git(gitArgs, { cwd })
|
|
1020
|
+
assertManifestOutsideCaller(args.manifest, cwd, runGit)
|
|
1021
|
+
const originRepo = validateRepo(repoFromRemoteUrl(
|
|
1022
|
+
checked(runGit(['remote', 'get-url', 'origin']), 'git remote get-url origin').stdout,
|
|
1023
|
+
))
|
|
1024
|
+
if (!RELEASE_STATE_CONTRACT.fields.release.includes('metadataPr')) {
|
|
1025
|
+
throw new Error('metadata-sync: canonical release-state contract does not declare release.metadataPr')
|
|
1026
|
+
}
|
|
1027
|
+
const manifest = loadJson(args.manifest)
|
|
1028
|
+
const meta = manifest?.release?.metadataPr
|
|
1029
|
+
if (!meta || meta.source !== METADATA_SYNC_SOURCE || !['opened', 'merged'].includes(meta.status)) {
|
|
1030
|
+
throw new Error('metadata-sync: record-delivery requires a canonical opened or merged metadataPr record')
|
|
1031
|
+
}
|
|
1032
|
+
for (const field of ['repo', 'base', 'branch', 'commit', 'version', 'sha']) {
|
|
1033
|
+
if (typeof meta[field] !== 'string' || meta[field].trim() === '') {
|
|
1034
|
+
throw new Error(`metadata-sync: opened metadataPr is missing exact ${field} identity`)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (!Number.isInteger(meta.number) || !Array.isArray(meta.content) || meta.content.length === 0) {
|
|
1038
|
+
throw new Error('metadata-sync: opened metadataPr requires PR number and expected content')
|
|
1039
|
+
}
|
|
1040
|
+
if (meta.repo !== originRepo) {
|
|
1041
|
+
throw new Error(`metadata-sync: recorded repo ${meta.repo} does not match origin identity ${originRepo}`)
|
|
1042
|
+
}
|
|
1043
|
+
const watcher = loadJson(args.watchResult)
|
|
1044
|
+
if (watcher.outcome !== 'MERGED' || typeof watcher.mergedSha !== 'string' || !/^[0-9a-f]{40}$/.test(watcher.mergedSha)) {
|
|
1045
|
+
throw new Error('metadata-sync: watcher result is not a terminal MERGED verdict with mergedSha')
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const pr = assertPrIdentity(findExistingPr({
|
|
1049
|
+
gh, cwd, repo: validateRepo(meta.repo), branch: meta.branch, recordedNumber: meta.number,
|
|
1050
|
+
}), meta)
|
|
1051
|
+
if (pr.state !== 'MERGED' || pr.mergeCommit !== watcher.mergedSha || !pr.mergedAt) {
|
|
1052
|
+
throw new Error('metadata-sync: watcher result does not match the fresh merged PR observation')
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
checked(runGit([
|
|
1056
|
+
'fetch', '--quiet', '--no-tags', 'origin',
|
|
1057
|
+
`refs/heads/${meta.base}:refs/remotes/origin/${meta.base}`,
|
|
1058
|
+
]), `fetch origin ${meta.base}`)
|
|
1059
|
+
const baseSha = resolveRef(runGit, `refs/remotes/origin/${meta.base}`, cwd)
|
|
1060
|
+
const commit = resolveRef(runGit, meta.commit, cwd)
|
|
1061
|
+
const mergedSha = resolveRef(runGit, watcher.mergedSha, cwd)
|
|
1062
|
+
if (commit !== meta.commit || mergedSha !== watcher.mergedSha || !baseSha) {
|
|
1063
|
+
throw new Error('metadata-sync: recorded commit, merge commit, or fresh base does not resolve exactly')
|
|
1064
|
+
}
|
|
1065
|
+
if ((runGit(['merge-base', '--is-ancestor', commit, mergedSha]).status ?? 1) !== 0) {
|
|
1066
|
+
throw new Error('metadata-sync: metadata commit is not an ancestor of the watcher merge commit')
|
|
1067
|
+
}
|
|
1068
|
+
if ((runGit(['merge-base', '--is-ancestor', mergedSha, baseSha]).status ?? 1) !== 0) {
|
|
1069
|
+
throw new Error(`metadata-sync: watcher merge commit is not an ancestor of fresh origin/${meta.base}`)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const contentProof = meta.content.map((expected) => {
|
|
1073
|
+
if (typeof expected?.path !== 'string' || !/^[0-9a-f]{64}$/.test(expected?.sha256 || '')) {
|
|
1074
|
+
throw new Error('metadata-sync: malformed expected content identity')
|
|
1075
|
+
}
|
|
1076
|
+
safeRelativePath(expected.path, 'recorded content path')
|
|
1077
|
+
const blob = checked(runGit(['show', `${mergedSha}:${expected.path}`]), `read merged ${expected.path}`).stdout
|
|
1078
|
+
const actual = sha256(Buffer.from(blob))
|
|
1079
|
+
if (actual !== expected.sha256) {
|
|
1080
|
+
throw new Error(`metadata-sync: merged content mismatch for ${expected.path}`)
|
|
1081
|
+
}
|
|
1082
|
+
return { path: expected.path, sha256: actual }
|
|
1083
|
+
})
|
|
1084
|
+
const entry = {
|
|
1085
|
+
...meta,
|
|
1086
|
+
status: 'merged',
|
|
1087
|
+
mergeCommit: mergedSha,
|
|
1088
|
+
mergedAt: pr.mergedAt,
|
|
1089
|
+
recordedAt: now(),
|
|
1090
|
+
delivery: {
|
|
1091
|
+
watcher: { outcome: 'MERGED', mergedSha },
|
|
1092
|
+
ancestry: { commit, mergeCommit: mergedSha, base: meta.base, baseSha },
|
|
1093
|
+
content: contentProof,
|
|
1094
|
+
},
|
|
1095
|
+
}
|
|
1096
|
+
writeJson(args.manifest, recordMetadataPr(manifest, entry))
|
|
1097
|
+
console.log(JSON.stringify({ ok: true, metadataPr: entry }, null, 2))
|
|
1098
|
+
return 0
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function openIsolated({ args, cwd, date, changes, git, gh, exec, runVersion, now }) {
|
|
1102
|
+
const base = args.base || 'main'
|
|
1103
|
+
const branch = args.branch || defaultBranchName(args.version)
|
|
1104
|
+
const runGit = (gitArgs, options = {}) => git(gitArgs, { cwd: options.cwd || cwd, input: options.input })
|
|
1105
|
+
if (!args.manifest) {
|
|
1106
|
+
throw new Error('metadata-sync: open requires --manifest outside the caller repository')
|
|
1107
|
+
}
|
|
1108
|
+
assertManifestOutsideCaller(args.manifest, cwd, runGit)
|
|
1109
|
+
const remoteUrl = checked(runGit(['remote', 'get-url', 'origin']), 'git remote get-url origin').stdout.trim()
|
|
1110
|
+
const derivedRepo = validateRepo(repoFromRemoteUrl(remoteUrl))
|
|
1111
|
+
const repo = validateRepo(args.repo || derivedRepo)
|
|
1112
|
+
if (repo !== derivedRepo) {
|
|
1113
|
+
throw new Error(`metadata-sync: requested repo ${repo} does not match origin identity ${derivedRepo}`)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
checked(runGit([
|
|
1117
|
+
'fetch', '--quiet', '--no-tags', 'origin',
|
|
1118
|
+
`refs/heads/${base}:refs/remotes/origin/${base}`,
|
|
1119
|
+
]), `fetch origin ${base}`)
|
|
1120
|
+
const baseSha = resolveRef(runGit, `refs/remotes/origin/${base}`, cwd)
|
|
1121
|
+
if (!baseSha) throw new Error(`metadata-sync: fetched base origin/${base} does not resolve`)
|
|
1122
|
+
const shippedSha = resolveRef(runGit, args.sha, cwd)
|
|
1123
|
+
if (!shippedSha) throw new Error(`metadata-sync: shipped SHA ${JSON.stringify(args.sha)} does not resolve`)
|
|
1124
|
+
if ((runGit(['merge-base', '--is-ancestor', shippedSha, baseSha]).status ?? 1) !== 0) {
|
|
1125
|
+
throw new Error(`metadata-sync: shipped SHA ${shippedSha} is not an ancestor of fresh origin/${base}`)
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const recorded = loadJson(args.manifest)?.release?.metadataPr || null
|
|
1129
|
+
const existingPr = findExistingPr({
|
|
1130
|
+
gh, cwd, repo, branch, recordedNumber: recorded?.number ?? null,
|
|
1131
|
+
})
|
|
1132
|
+
if (existingPr) {
|
|
1133
|
+
assertPrCoordinates(existingPr, { repo, base, branch })
|
|
1134
|
+
if (existingPr.state !== 'OPEN' || !/^[0-9a-f]{40}$/.test(existingPr.commit || '')) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`metadata-sync: existing PR #${existingPr.number} must be OPEN with an exact head commit; ` +
|
|
1137
|
+
'use record-delivery for a merged PR',
|
|
1138
|
+
)
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
runGit([
|
|
1142
|
+
'fetch', '--quiet', '--no-tags', 'origin',
|
|
1143
|
+
`refs/heads/${branch}:refs/remotes/origin/${branch}`,
|
|
1144
|
+
])
|
|
1145
|
+
runGit(['update-ref', '-d', `refs/remotes/origin/${branch}`])
|
|
1146
|
+
runGit(['fetch', '--quiet', '--no-tags', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`])
|
|
1147
|
+
const remoteCommit = resolveRef(runGit, `refs/remotes/origin/${branch}`, cwd)
|
|
1148
|
+
if (existingPr && !resolveRef(runGit, existingPr.commit, cwd)) {
|
|
1149
|
+
runGit(['fetch', '--quiet', '--no-tags', 'origin', `refs/pull/${existingPr.number}/head`])
|
|
1150
|
+
}
|
|
1151
|
+
const localCommit = resolveRef(runGit, `refs/heads/${branch}`, cwd)
|
|
1152
|
+
const recordedCommit = recorded?.commit || null
|
|
1153
|
+
const prCommit = existingPr?.commit || null
|
|
1154
|
+
const distinctCommits = new Set([localCommit, remoteCommit, recordedCommit, prCommit].filter(Boolean))
|
|
1155
|
+
if (distinctCommits.size > 1) {
|
|
1156
|
+
throw new Error(`metadata-sync: local, remote, recorded, and PR ${branch} artifacts diverge; refusing without force`)
|
|
1157
|
+
}
|
|
1158
|
+
if (prCommit && !resolveRef(runGit, prCommit, cwd)) {
|
|
1159
|
+
throw new Error(`metadata-sync: PR #${existingPr.number} head commit ${prCommit} cannot be fetched exactly`)
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const worktree = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-sync-worktree-'))
|
|
1163
|
+
let executionTree = null
|
|
1164
|
+
let keepWorktree = false
|
|
1165
|
+
let published = remoteCommit != null
|
|
1166
|
+
let createdCommit = false
|
|
1167
|
+
try {
|
|
1168
|
+
checked(runGit(['worktree', 'add', '--detach', worktree, baseSha]), 'git worktree add')
|
|
1169
|
+
const worktreeChanges = parseChanges(args.changes, worktree, runGit)
|
|
1170
|
+
const plan = planFromGitTree(runGit, {
|
|
1171
|
+
cwd: worktree,
|
|
1172
|
+
version: args.version,
|
|
1173
|
+
date,
|
|
1174
|
+
changes: worktreeChanges,
|
|
1175
|
+
packageFiles: args.packageFiles,
|
|
1176
|
+
changelogPath: args.changelog || 'CHANGELOG.md',
|
|
1177
|
+
repo,
|
|
1178
|
+
branch,
|
|
1179
|
+
base,
|
|
1180
|
+
runVersion,
|
|
1181
|
+
lockFile: args.lockFile || null,
|
|
1182
|
+
})
|
|
1183
|
+
const packageErrors = planPackageErrors(plan)
|
|
1184
|
+
if (packageErrors.length) {
|
|
1185
|
+
throw new Error(`metadata-sync: refusing to open: ${packageErrors.map((item) => `${item.path} (${item.error})`).join('; ')}`)
|
|
1186
|
+
}
|
|
1187
|
+
filesystemSources(
|
|
1188
|
+
worktree,
|
|
1189
|
+
metadataDeclarations(args.packageFiles, args.changelog || 'CHANGELOG.md', args.lockFile || null),
|
|
1190
|
+
)
|
|
1191
|
+
applyMetadataFiles(plan, { runGit })
|
|
1192
|
+
|
|
1193
|
+
// Commands and ordinary commit hooks run only in this second disposable tree.
|
|
1194
|
+
executionTree = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-sync-exec-'))
|
|
1195
|
+
const gitDir = checked(
|
|
1196
|
+
runGit(['rev-parse', '--git-dir'], { cwd: worktree }),
|
|
1197
|
+
'resolve disposable Git directory',
|
|
1198
|
+
).stdout.trim()
|
|
1199
|
+
fs.writeFileSync(path.join(executionTree, '.git'), `gitdir: ${path.resolve(worktree, gitDir)}\n`)
|
|
1200
|
+
checked(
|
|
1201
|
+
runGit(['checkout-index', '-a', '-f'], { cwd: executionTree }),
|
|
1202
|
+
'materialize private metadata execution tree',
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
const lock = runLockCommandAndVerify({
|
|
1206
|
+
lockFile: args.lockFile,
|
|
1207
|
+
lockCommand: args.lockCommand,
|
|
1208
|
+
plan,
|
|
1209
|
+
cwd: executionTree,
|
|
1210
|
+
exec,
|
|
1211
|
+
stageLock({ rel, raw }) {
|
|
1212
|
+
const lockBlob = checked(
|
|
1213
|
+
runGit(['hash-object', '-w', '--stdin'], { cwd: executionTree, input: raw }),
|
|
1214
|
+
`stage lock blob ${rel}`,
|
|
1215
|
+
).stdout.trim()
|
|
1216
|
+
if (!/^[0-9a-f]{40,64}$/.test(lockBlob)) {
|
|
1217
|
+
throw new Error(`metadata-sync: invalid blob identity for ${rel}`)
|
|
1218
|
+
}
|
|
1219
|
+
checked(
|
|
1220
|
+
runGit(['update-index', '--add', '--cacheinfo', `100644,${lockBlob},${rel}`], { cwd: executionTree }),
|
|
1221
|
+
`stage lock path ${rel}`,
|
|
1222
|
+
)
|
|
1223
|
+
},
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
const beforeCommit = verifyAppliedPlan({ ...plan, cwd: executionTree }, { lock })
|
|
1227
|
+
if (!beforeCommit.ok) {
|
|
1228
|
+
throw new Error(`metadata-sync: verification failed before commit: ${beforeCommit.failures.join('; ')}`)
|
|
1229
|
+
}
|
|
1230
|
+
const changedPaths = checked(
|
|
1231
|
+
runGit(['diff', '--cached', '--name-only'], { cwd: executionTree }),
|
|
1232
|
+
'inspect staged metadata paths',
|
|
1233
|
+
).stdout.split('\n').filter(Boolean).sort()
|
|
1234
|
+
if (changedPaths.length === 0) {
|
|
1235
|
+
if (args.manifest) {
|
|
1236
|
+
writeJson(args.manifest, recordMetadataPr(loadJson(args.manifest), {
|
|
1237
|
+
status: 'n/a',
|
|
1238
|
+
reason: 'metadata already present on base; nothing to commit',
|
|
1239
|
+
version: plan.version,
|
|
1240
|
+
sha: shippedSha,
|
|
1241
|
+
recordedAt: now(),
|
|
1242
|
+
}))
|
|
1243
|
+
}
|
|
1244
|
+
console.log(JSON.stringify({ ok: true, noop: true, reason: 'nothing to commit' }, null, 2))
|
|
1245
|
+
return 0
|
|
1246
|
+
}
|
|
1247
|
+
const content = contentIdentity(plan, lock)
|
|
1248
|
+
const title = args.title || `chore(release): metadata for v${plan.version}`
|
|
1249
|
+
const body = args.body || [
|
|
1250
|
+
`Post-release metadata sync for \`${plan.version}\` (shipped \`${shippedSha}\`).`,
|
|
1251
|
+
'',
|
|
1252
|
+
'Cuts the CHANGELOG section and bumps package version fields after the final',
|
|
1253
|
+
'release tag, preserving newer unreleased work on main.',
|
|
1254
|
+
'',
|
|
1255
|
+
'Opened by `metadata-sync.mjs` (resolve-release / verified-sha-github-flow).',
|
|
1256
|
+
'',
|
|
1257
|
+
'Refs: identity-v0.4.1 · #800 · identity#678',
|
|
1258
|
+
].join('\n')
|
|
1259
|
+
assertRecordedIdentity(args.manifest ? loadJson(args.manifest)?.release?.metadataPr : null, {
|
|
1260
|
+
repo, base, branch, version: plan.version, sha: shippedSha, content,
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
let commit = localCommit || remoteCommit || recordedCommit || prCommit
|
|
1264
|
+
if (commit) {
|
|
1265
|
+
assertCommitIdentity(runGit, { cwd: worktree, commit, baseSha, changedPaths, content, title, body })
|
|
1266
|
+
} else {
|
|
1267
|
+
checked(runGit(['commit', '-m', title, '-m', body], { cwd: executionTree }), 'git commit metadata')
|
|
1268
|
+
commit = resolveRef(runGit, 'HEAD', executionTree)
|
|
1269
|
+
createdCommit = true
|
|
1270
|
+
checked(
|
|
1271
|
+
runGit(['update-ref', `refs/heads/${branch}`, commit, '0'.repeat(40)]),
|
|
1272
|
+
`create local branch ${branch}`,
|
|
1273
|
+
)
|
|
1274
|
+
}
|
|
1275
|
+
assertCommitIdentity(runGit, { cwd: worktree, commit, baseSha, changedPaths, content, title, body })
|
|
1276
|
+
|
|
1277
|
+
const postCommitDisk = verifyAppliedPlan({ ...plan, cwd: executionTree }, { lock })
|
|
1278
|
+
const committed = verifyCommittedPlan(plan, {
|
|
1279
|
+
lock,
|
|
1280
|
+
show: (rel) => runGit(['show', `${commit}:${rel}`], { cwd: executionTree }),
|
|
1281
|
+
})
|
|
1282
|
+
if (!postCommitDisk.ok || !committed.ok) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`metadata-sync: post-commit verification failed (a commit hook mutated staged metadata?): ${[...postCommitDisk.failures, ...committed.failures].join('; ')}`,
|
|
1285
|
+
)
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (!localCommit && !remoteCommit && (recordedCommit || prCommit)) {
|
|
1289
|
+
checked(
|
|
1290
|
+
runGit(['update-ref', `refs/heads/${branch}`, commit, '0'.repeat(40)]),
|
|
1291
|
+
`restore local branch ${branch}`,
|
|
1292
|
+
)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
let pushHookBypass = null
|
|
1296
|
+
if (!remoteCommit) {
|
|
1297
|
+
const pushArgs = ['push', 'origin', `refs/heads/${branch}:refs/heads/${branch}`]
|
|
1298
|
+
let pushed = runGit(pushArgs, { cwd: executionTree })
|
|
1299
|
+
if ((pushed.status ?? 1) !== 0) {
|
|
1300
|
+
const output = `${pushed.stderr || ''}\n${pushed.stdout || ''}`.trim()
|
|
1301
|
+
if (/mergify/i.test(output)) {
|
|
1302
|
+
pushHookBypass = 'pre-push'
|
|
1303
|
+
pushed = runGit([...pushArgs, '--no-verify'], { cwd: executionTree })
|
|
1304
|
+
if ((pushed.status ?? 1) !== 0) {
|
|
1305
|
+
throw new Error(`metadata-sync: git push --no-verify failed after known-hook retry: ${redact(pushed.stderr || pushed.stdout)}`)
|
|
1306
|
+
}
|
|
1307
|
+
} else {
|
|
1308
|
+
throw new Error(`metadata-sync: branch push rejected without force: ${redact(output)}`)
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
published = true
|
|
1312
|
+
} else if (remoteCommit !== commit) {
|
|
1313
|
+
throw new Error(`metadata-sync: remote ${branch} points at ${remoteCommit}, not exact commit ${commit}`)
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const expectedPr = { repo, base, branch, commit }
|
|
1317
|
+
let pr = existingPr
|
|
1318
|
+
if (pr) pr = assertPrIdentity(pr, expectedPr)
|
|
1319
|
+
else pr = assertPrIdentity(createPr({ gh, cwd: executionTree, repo, base, branch, title, body }), expectedPr)
|
|
1320
|
+
const entry = {
|
|
1321
|
+
status: 'opened',
|
|
1322
|
+
number: pr.number,
|
|
1323
|
+
url: pr.url,
|
|
1324
|
+
repo,
|
|
1325
|
+
base,
|
|
1326
|
+
branch,
|
|
1327
|
+
commit,
|
|
1328
|
+
content,
|
|
1329
|
+
version: plan.version,
|
|
1330
|
+
sha: shippedSha,
|
|
1331
|
+
recordedAt: now(),
|
|
1332
|
+
...(pushHookBypass ? { pushHookBypass } : {}),
|
|
1333
|
+
}
|
|
1334
|
+
if (args.manifest) writeJson(args.manifest, recordMetadataPr(loadJson(args.manifest), entry))
|
|
1335
|
+
console.log(JSON.stringify({
|
|
1336
|
+
ok: true,
|
|
1337
|
+
resumed: Boolean(localCommit || remoteCommit || recordedCommit || existingPr),
|
|
1338
|
+
metadataPr: entry,
|
|
1339
|
+
...(pushHookBypass ? { push: { hookBypass: pushHookBypass } } : {}),
|
|
1340
|
+
...(lock ? { lock: { file: lock.rel, command: args.lockCommand } } : {}),
|
|
1341
|
+
}, null, 2))
|
|
1342
|
+
return 0
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
const preservedCommit = resolveRef(runGit, `refs/heads/${branch}`, cwd)
|
|
1345
|
+
if (!published && createdCommit && !preservedCommit) keepWorktree = true
|
|
1346
|
+
if (keepWorktree) {
|
|
1347
|
+
throw new Error(`${error.message}; unpublished metadata worktree retained at ${JSON.stringify(worktree)}`)
|
|
1348
|
+
}
|
|
1349
|
+
if (!published && preservedCommit) {
|
|
1350
|
+
throw new Error(
|
|
1351
|
+
`${error.message}; unpublished metadata commit preserved at refs/heads/${branch} ${preservedCommit}`,
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1354
|
+
throw error
|
|
1355
|
+
} finally {
|
|
1356
|
+
if (!keepWorktree) {
|
|
1357
|
+
runGit(['worktree', 'remove', '--force', worktree])
|
|
1358
|
+
fs.rmSync(worktree, { recursive: true, force: true })
|
|
1359
|
+
}
|
|
1360
|
+
if (executionTree) fs.rmSync(executionTree, { recursive: true, force: true })
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
export function main(argv, deps = {}) {
|
|
1365
|
+
const args = parseArgs(argv)
|
|
1366
|
+
if (args.help || args.cmd === 'help') {
|
|
1367
|
+
console.log(USAGE)
|
|
1368
|
+
return 0
|
|
1369
|
+
}
|
|
1370
|
+
if (args.unknown) {
|
|
1371
|
+
console.error(`metadata-sync: unknown flag ${args.unknown}`)
|
|
1372
|
+
return 2
|
|
1373
|
+
}
|
|
1374
|
+
if (!args.cmd || !['plan', 'open', 'record-delivery', 'na', 'migrate'].includes(args.cmd)) {
|
|
1375
|
+
console.error('metadata-sync: use plan|open|record-delivery|na|migrate (see --help)')
|
|
1376
|
+
return 2
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const git = deps.git || ((a, o) => defaultRun('git', a, o))
|
|
1380
|
+
const gh = deps.gh || ((a, o) => defaultRun('gh', a, o))
|
|
1381
|
+
const exec = deps.exec || ((cmd, { cwd: commandCwd } = {}) => spawnSync(cmd, { encoding: 'utf8', cwd: commandCwd, shell: true }))
|
|
1382
|
+
const runVersion = deps.runVersion || ((a, o) => defaultRun(process.execPath, a, o))
|
|
1383
|
+
const now = deps.now || (() => new Date().toISOString())
|
|
1384
|
+
const cwd = args.cwd || process.cwd()
|
|
1385
|
+
|
|
1386
|
+
try {
|
|
1387
|
+
if (args.cmd === 'record-delivery') {
|
|
1388
|
+
return recordMergedDelivery({ args, cwd, git, gh, now })
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (args.cmd === 'migrate') {
|
|
1392
|
+
if (!args.manifest) {
|
|
1393
|
+
console.error('metadata-sync: migrate requires --manifest')
|
|
1394
|
+
return 2
|
|
1395
|
+
}
|
|
1396
|
+
const manifest = loadJson(args.manifest)
|
|
1397
|
+
const entry = migrateMetadataSync(manifest)
|
|
1398
|
+
const next = recordMetadataPr(manifest, entry)
|
|
1399
|
+
delete next.release.metadataSync
|
|
1400
|
+
writeJson(args.manifest, next)
|
|
1401
|
+
console.log(JSON.stringify({ ok: true, metadataPr: next.release.metadataPr }, null, 2))
|
|
1402
|
+
return 0
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (args.cmd === 'na') {
|
|
1406
|
+
if (!args.manifest) {
|
|
1407
|
+
console.error('metadata-sync: na requires --manifest')
|
|
1408
|
+
return 2
|
|
1409
|
+
}
|
|
1410
|
+
if (!args.reason || !String(args.reason).trim()) {
|
|
1411
|
+
console.error('metadata-sync: na requires --reason <text>')
|
|
1412
|
+
return 2
|
|
1413
|
+
}
|
|
1414
|
+
const manifest = loadJson(args.manifest)
|
|
1415
|
+
const next = recordMetadataPr(manifest, {
|
|
1416
|
+
status: 'n/a',
|
|
1417
|
+
reason: args.reason,
|
|
1418
|
+
version: args.version || null,
|
|
1419
|
+
recordedAt: now(),
|
|
1420
|
+
})
|
|
1421
|
+
writeJson(args.manifest, next)
|
|
1422
|
+
console.log(JSON.stringify({ ok: true, metadataPr: next.release.metadataPr }, null, 2))
|
|
1423
|
+
return 0
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (args.cmd === 'plan' || args.cmd === 'open') {
|
|
1427
|
+
if (!args.version) {
|
|
1428
|
+
console.error(`metadata-sync: ${args.cmd} requires --version`)
|
|
1429
|
+
return 2
|
|
1430
|
+
}
|
|
1431
|
+
if (args.cmd === 'open' && !args.sha) {
|
|
1432
|
+
console.error('metadata-sync: open requires --sha <shipped>')
|
|
1433
|
+
return 2
|
|
1434
|
+
}
|
|
1435
|
+
if ((args.lockFile == null) !== (args.lockCommand == null)) {
|
|
1436
|
+
console.error('metadata-sync: --lock-file and --lock-command must be declared together')
|
|
1437
|
+
return 2
|
|
1438
|
+
}
|
|
1439
|
+
const date = args.date || now().slice(0, 10)
|
|
1440
|
+
const changes = args.cmd === 'open' && !args.dryRun
|
|
1441
|
+
? null
|
|
1442
|
+
: parseChanges(args.changes, cwd)
|
|
1443
|
+
|
|
1444
|
+
if (args.cmd === 'plan') {
|
|
1445
|
+
const plan = planInWorkspace({
|
|
1446
|
+
cwd,
|
|
1447
|
+
version: args.version,
|
|
1448
|
+
date,
|
|
1449
|
+
changes,
|
|
1450
|
+
packageFiles: args.packageFiles,
|
|
1451
|
+
changelogPath: args.changelog || 'CHANGELOG.md',
|
|
1452
|
+
repo: args.repo || null,
|
|
1453
|
+
branch: args.branch || null,
|
|
1454
|
+
base: args.base || 'main',
|
|
1455
|
+
runVersion,
|
|
1456
|
+
lockFile: args.lockFile || null,
|
|
1457
|
+
})
|
|
1458
|
+
console.log(JSON.stringify(plan, null, 2))
|
|
1459
|
+
const packageErrors = planPackageErrors(plan)
|
|
1460
|
+
if (packageErrors.length) {
|
|
1461
|
+
console.error(`metadata-sync: ${packageErrors.length} requested package file(s) failed: ${packageErrors.map((item) => `${item.path} (${item.error})`).join('; ')}`)
|
|
1462
|
+
return 1
|
|
1463
|
+
}
|
|
1464
|
+
return 0
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const base = args.base || 'main'
|
|
1468
|
+
const branch = args.branch || defaultBranchName(args.version)
|
|
1469
|
+
|
|
1470
|
+
if (args.dryRun) {
|
|
1471
|
+
const plan = planInWorkspace({
|
|
1472
|
+
cwd,
|
|
1473
|
+
version: args.version,
|
|
1474
|
+
date,
|
|
1475
|
+
changes,
|
|
1476
|
+
packageFiles: args.packageFiles,
|
|
1477
|
+
changelogPath: args.changelog || 'CHANGELOG.md',
|
|
1478
|
+
repo: args.repo || null,
|
|
1479
|
+
branch,
|
|
1480
|
+
base,
|
|
1481
|
+
runVersion,
|
|
1482
|
+
lockFile: args.lockFile || null,
|
|
1483
|
+
})
|
|
1484
|
+
const packageErrors = planPackageErrors(plan)
|
|
1485
|
+
if (packageErrors.length) {
|
|
1486
|
+
console.log(JSON.stringify({ dryRun: true, plan }, null, 2))
|
|
1487
|
+
console.error(`metadata-sync: ${packageErrors.length} requested package file(s) failed: ${packageErrors.map((item) => `${item.path} (${item.error})`).join('; ')}`)
|
|
1488
|
+
return 1
|
|
1489
|
+
}
|
|
1490
|
+
let executionTree = null
|
|
1491
|
+
try {
|
|
1492
|
+
if (args.lockFile) {
|
|
1493
|
+
executionTree = createDryRunExecutionTree(cwd, plan)
|
|
1494
|
+
const lock = runLockCommandAndVerify({
|
|
1495
|
+
lockFile: args.lockFile,
|
|
1496
|
+
lockCommand: args.lockCommand,
|
|
1497
|
+
plan,
|
|
1498
|
+
cwd: executionTree,
|
|
1499
|
+
exec,
|
|
1500
|
+
})
|
|
1501
|
+
const verified = verifyAppliedPlan({ ...plan, cwd: executionTree }, { lock })
|
|
1502
|
+
if (!verified.ok) {
|
|
1503
|
+
throw new Error(`metadata-sync: dry-run lock verification failed: ${verified.failures.join('; ')}`)
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
} finally {
|
|
1507
|
+
if (executionTree) fs.rmSync(executionTree, { recursive: true, force: true })
|
|
1508
|
+
}
|
|
1509
|
+
if (args.recordPlan && args.manifest) {
|
|
1510
|
+
const manifest = loadJson(args.manifest)
|
|
1511
|
+
const next = recordMetadataPr(manifest, {
|
|
1512
|
+
status: 'planned',
|
|
1513
|
+
version: plan.version,
|
|
1514
|
+
sha: args.sha,
|
|
1515
|
+
branch: plan.branch,
|
|
1516
|
+
recordedAt: now(),
|
|
1517
|
+
})
|
|
1518
|
+
writeJson(args.manifest, next)
|
|
1519
|
+
}
|
|
1520
|
+
console.log(JSON.stringify({ dryRun: true, plan }, null, 2))
|
|
1521
|
+
return 0
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
return openIsolated({
|
|
1525
|
+
args, cwd, date, changes, git, gh, exec, runVersion, now,
|
|
1526
|
+
})
|
|
1527
|
+
|
|
1528
|
+
}
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
console.error(`metadata-sync: ${err.message || err}`)
|
|
1531
|
+
return err.message && /requires|unknown|use plan/.test(err.message) ? 2 : 1
|
|
1532
|
+
}
|
|
1533
|
+
return 2
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (isMainModule(import.meta.url)) {
|
|
1537
|
+
process.exit(main(process.argv.slice(2)))
|
|
1538
|
+
}
|