@intentius/chant 0.124.28 → 0.124.29
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/dist/build-params.d.ts +34 -1
- package/dist/build-params.d.ts.map +1 -1
- package/dist/build-params.js +31 -1
- package/dist/change-set-graph.d.ts +8 -1
- package/dist/change-set-graph.d.ts.map +1 -1
- package/dist/change-set-graph.js +4 -2
- package/dist/change-set.d.ts +10 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +4 -3
- package/dist/cli/build-params-cli.d.ts +7 -1
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/build-params-cli.js +17 -8
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -3
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +45 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -4
- package/dist/cli/handlers/fan-out.d.ts +9 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +40 -3
- package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
- package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-gc.js +143 -0
- package/dist/cli/handlers/pr.d.ts +11 -3
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +134 -26
- package/dist/cli/handlers/promote.d.ts +7 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +210 -33
- package/dist/cli/handlers/release.d.ts +62 -5
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +414 -28
- package/dist/cli/handlers/run.d.ts +11 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +67 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +61 -10
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +24 -2
- package/dist/cli/registry.d.ts +15 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +3 -2
- package/dist/cli/run-events.d.ts.map +1 -1
- package/dist/cli/run-events.js +5 -3
- package/dist/components/auto-release.d.ts +2 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +1 -0
- package/dist/components/capability.d.ts +9 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +35 -0
- package/dist/components/fan-out-inputs.d.ts +69 -0
- package/dist/components/fan-out-inputs.d.ts.map +1 -0
- package/dist/components/fan-out-inputs.js +169 -0
- package/dist/components/fan-out-record.d.ts +13 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +16 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +50 -0
- package/dist/components/fan-out.d.ts +4 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +25 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +22 -4
- package/dist/components/promote.d.ts +23 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +60 -2
- package/dist/components/release-compile.d.ts +9 -0
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +9 -3
- package/dist/components/release-gate.d.ts +11 -1
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +10 -4
- package/dist/components/wave-plan.d.ts +5 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +10 -2
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/flag-params.d.ts +54 -0
- package/dist/flag-params.d.ts.map +1 -0
- package/dist/flag-params.js +130 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
- package/dist/lifecycle/change-set-store.d.ts +2 -1
- package/dist/lifecycle/change-set-store.d.ts.map +1 -1
- package/dist/lifecycle/change-set-store.js +2 -1
- package/dist/lifecycle/delivery-metrics.d.ts +139 -0
- package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
- package/dist/lifecycle/delivery-metrics.js +145 -0
- package/dist/lifecycle/deploy-outcome.d.ts +81 -0
- package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
- package/dist/lifecycle/deploy-outcome.js +101 -0
- package/dist/lifecycle/gate-change-set.d.ts +10 -1
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
- package/dist/lifecycle/gate-change-set.js +35 -3
- package/dist/lifecycle/gate-ledger.d.ts +2 -3
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +30 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +31 -5
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle/mixed-environment.d.ts +95 -0
- package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
- package/dist/lifecycle/mixed-environment.js +154 -0
- package/dist/lifecycle/publish-attachments.d.ts +79 -0
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
- package/dist/lifecycle/publish-attachments.js +179 -0
- package/dist/lifecycle/publish-credentials.d.ts +37 -0
- package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
- package/dist/lifecycle/publish-credentials.js +69 -0
- package/dist/lifecycle/publish-declaration.d.ts +97 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.js +83 -0
- package/dist/lifecycle/publish-forge.d.ts +53 -0
- package/dist/lifecycle/publish-forge.d.ts.map +1 -0
- package/dist/lifecycle/publish-forge.js +298 -0
- package/dist/lifecycle/publish-npm.d.ts +61 -0
- package/dist/lifecycle/publish-npm.d.ts.map +1 -0
- package/dist/lifecycle/publish-npm.js +242 -0
- package/dist/lifecycle/publish-oci.d.ts +64 -0
- package/dist/lifecycle/publish-oci.d.ts.map +1 -0
- package/dist/lifecycle/publish-oci.js +383 -0
- package/dist/lifecycle/publish-record.d.ts +66 -0
- package/dist/lifecycle/publish-record.d.ts.map +1 -0
- package/dist/lifecycle/publish-record.js +84 -0
- package/dist/lifecycle/publish-targets.d.ts +45 -0
- package/dist/lifecycle/publish-targets.d.ts.map +1 -0
- package/dist/lifecycle/publish-targets.js +85 -0
- package/dist/lifecycle/release-cut.d.ts +48 -1
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +116 -23
- package/dist/lifecycle/release-join.d.ts +95 -0
- package/dist/lifecycle/release-join.d.ts.map +1 -0
- package/dist/lifecycle/release-join.js +139 -0
- package/dist/lifecycle/release-publish.d.ts +312 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -0
- package/dist/lifecycle/release-publish.js +335 -0
- package/dist/lifecycle/release-revocation.d.ts +80 -0
- package/dist/lifecycle/release-revocation.d.ts.map +1 -0
- package/dist/lifecycle/release-revocation.js +123 -0
- package/dist/lifecycle/release-store.d.ts +12 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +4 -2
- package/dist/lifecycle/release-verify.d.ts +61 -0
- package/dist/lifecycle/release-verify.d.ts.map +1 -0
- package/dist/lifecycle/release-verify.js +92 -0
- package/dist/lifecycle/release-workstation.d.ts +91 -0
- package/dist/lifecycle/release-workstation.d.ts.map +1 -0
- package/dist/lifecycle/release-workstation.js +180 -0
- package/dist/lifecycle/retention-policy.d.ts +45 -0
- package/dist/lifecycle/retention-policy.d.ts.map +1 -0
- package/dist/lifecycle/retention-policy.js +43 -0
- package/dist/lifecycle/retention.d.ts +185 -0
- package/dist/lifecycle/retention.d.ts.map +1 -0
- package/dist/lifecycle/retention.js +400 -0
- package/dist/plan-summary.d.ts.map +1 -1
- package/dist/plan-summary.js +8 -2
- package/dist/pr-loop.d.ts +7 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +23 -4
- package/dist/pr-report-url.d.ts +82 -0
- package/dist/pr-report-url.d.ts.map +1 -0
- package/dist/pr-report-url.js +174 -0
- package/dist/provenance.d.ts +2 -2
- package/dist/provenance.d.ts.map +1 -1
- package/dist/run-event-store.d.ts +29 -2
- package/dist/run-event-store.d.ts.map +1 -1
- package/dist/run-event-store.js +50 -3
- package/dist/run-event-stream.d.ts +2 -1
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +5 -1
- package/dist/run-event.d.ts +19 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +2 -0
- package/dist/wave-runtime.mjs +260 -229
- package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
- package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
- package/dist/workspace/change-set.schema.json +5 -1
- package/dist/workspace/check.schema.json +4 -2
- package/dist/workspace/checks/records.d.ts +9 -1
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks/records.js +35 -1
- package/dist/workspace/checks.d.ts +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +2 -1
- package/dist/workspace/ci-green.d.ts +8 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +10 -3
- package/dist/workspace/declaration.d.ts +16 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +26 -0
- package/dist/workspace/declaration.schema.json +64 -0
- package/dist/workspace/declared-kinds.d.ts +16 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/declared-kinds.js +25 -0
- package/dist/workspace/delivery-metrics.d.ts +91 -0
- package/dist/workspace/delivery-metrics.d.ts.map +1 -0
- package/dist/workspace/delivery-metrics.js +217 -0
- package/dist/workspace/delivery-metrics.schema.json +519 -0
- package/dist/workspace/flags-read.d.ts +37 -0
- package/dist/workspace/flags-read.d.ts.map +1 -0
- package/dist/workspace/flags-read.js +103 -0
- package/dist/workspace/flags.d.ts +100 -0
- package/dist/workspace/flags.d.ts.map +1 -0
- package/dist/workspace/flags.js +98 -0
- package/dist/workspace/graph-cli.js +1 -1
- package/dist/workspace/intent-cli.d.ts +11 -2
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.js +54 -5
- package/dist/workspace/intent-trace.d.ts +219 -0
- package/dist/workspace/intent-trace.d.ts.map +1 -0
- package/dist/workspace/intent-trace.js +314 -0
- package/dist/workspace/intent.d.ts +30 -6
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +141 -21
- package/dist/workspace/intent.schema.json +702 -6
- package/dist/workspace/overlap.d.ts +146 -0
- package/dist/workspace/overlap.d.ts.map +1 -0
- package/dist/workspace/overlap.js +276 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +3 -0
- package/dist/workspace/records.d.ts +15 -2
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +32 -0
- package/dist/workspace/records.schema.json +21 -2
- package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/dist/workspace/releases.d.ts +9 -0
- package/dist/workspace/releases.d.ts.map +1 -1
- package/dist/workspace/releases.js +9 -0
- package/dist/workspace/releases.schema.json +123 -1
- package/dist/workspace/run-event.schema.json +10 -3
- package/dist/workspace/status-gates.d.ts +8 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +78 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +170 -3
- package/dist/workspace/status.schema.json +198 -1
- package/dist/workspace/wip.d.ts.map +1 -1
- package/dist/workspace/wip.js +11 -3
- package/package.json +1 -1
- package/src/build-params.test.ts +46 -0
- package/src/build-params.ts +55 -1
- package/src/change-set-graph.ts +10 -2
- package/src/change-set.ts +14 -3
- package/src/cli/build-params-cli.ts +22 -9
- package/src/cli/commands/build.ts +6 -3
- package/src/cli/handlers/approve-release.test.ts +5 -1
- package/src/cli/handlers/approve-revoked.test.ts +155 -0
- package/src/cli/handlers/approve.ts +47 -0
- package/src/cli/handlers/build.test.ts +15 -0
- package/src/cli/handlers/build.ts +17 -4
- package/src/cli/handlers/fan-out.test.ts +180 -1
- package/src/cli/handlers/fan-out.ts +38 -3
- package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
- package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
- package/src/cli/handlers/lifecycle-gc.ts +146 -0
- package/src/cli/handlers/pr.test.ts +148 -1
- package/src/cli/handlers/pr.ts +139 -28
- package/src/cli/handlers/promote.test.ts +280 -2
- package/src/cli/handlers/promote.ts +210 -34
- package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
- package/src/cli/handlers/release-join.e2e.test.ts +185 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
- package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
- package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
- package/src/cli/handlers/release.e2e.test.ts +146 -32
- package/src/cli/handlers/release.ts +415 -28
- package/src/cli/handlers/run-auto-release.test.ts +9 -0
- package/src/cli/handlers/run-release.test.ts +55 -0
- package/src/cli/handlers/run.ts +69 -5
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +61 -10
- package/src/cli/parse-args.ts +19 -2
- package/src/cli/registry.ts +15 -3
- package/src/cli/run-events.ts +5 -2
- package/src/components/auto-release.test.ts +9 -0
- package/src/components/auto-release.ts +3 -0
- package/src/components/capability.ts +9 -0
- package/src/components/driver.test.ts +24 -0
- package/src/components/driver.ts +36 -0
- package/src/components/fan-out-inputs.test.ts +85 -0
- package/src/components/fan-out-inputs.ts +192 -0
- package/src/components/fan-out-record.test.ts +10 -0
- package/src/components/fan-out-record.ts +19 -0
- package/src/components/fan-out-run.ts +48 -0
- package/src/components/fan-out.ts +4 -0
- package/src/components/pr-pipeline.ts +35 -4
- package/src/components/promote.ts +68 -2
- package/src/components/release-compile.ts +17 -3
- package/src/components/release-gate.ts +18 -4
- package/src/components/wave-plan.ts +15 -2
- package/src/config.ts +8 -0
- package/src/flag-params.test.ts +144 -0
- package/src/flag-params.ts +134 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
- package/src/lifecycle/change-set-store.ts +2 -1
- package/src/lifecycle/delivery-metrics.ts +263 -0
- package/src/lifecycle/deploy-outcome.ts +147 -0
- package/src/lifecycle/gate-change-set.ts +37 -6
- package/src/lifecycle/gate-ledger.ts +2 -2
- package/src/lifecycle/git.ts +61 -8
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/mixed-environment.test.ts +97 -0
- package/src/lifecycle/mixed-environment.ts +204 -0
- package/src/lifecycle/publish-attachments.ts +196 -0
- package/src/lifecycle/publish-credentials.ts +84 -0
- package/src/lifecycle/publish-declaration.ts +95 -0
- package/src/lifecycle/publish-forge.ts +325 -0
- package/src/lifecycle/publish-npm.ts +259 -0
- package/src/lifecycle/publish-oci.ts +396 -0
- package/src/lifecycle/publish-record.ts +113 -0
- package/src/lifecycle/publish-resume.test.ts +168 -0
- package/src/lifecycle/publish-targets.test.ts +494 -0
- package/src/lifecycle/publish-targets.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +74 -2
- package/src/lifecycle/release-cut.ts +158 -22
- package/src/lifecycle/release-flag.test.ts +112 -0
- package/src/lifecycle/release-join.test.ts +68 -0
- package/src/lifecycle/release-join.ts +182 -0
- package/src/lifecycle/release-publish.test.ts +225 -0
- package/src/lifecycle/release-publish.ts +568 -0
- package/src/lifecycle/release-revocation.ts +147 -0
- package/src/lifecycle/release-store.ts +15 -4
- package/src/lifecycle/release-verify.test.ts +110 -0
- package/src/lifecycle/release-verify.ts +120 -0
- package/src/lifecycle/release-workstation.test.ts +204 -0
- package/src/lifecycle/release-workstation.ts +216 -0
- package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
- package/src/lifecycle/retention-policy.ts +63 -0
- package/src/lifecycle/retention.test.ts +363 -0
- package/src/lifecycle/retention.ts +508 -0
- package/src/plan-summary.test.ts +20 -0
- package/src/plan-summary.ts +8 -1
- package/src/pr-loop.ts +25 -4
- package/src/pr-report-url.test.ts +131 -0
- package/src/pr-report-url.ts +209 -0
- package/src/provenance.ts +2 -2
- package/src/run-event-store.ts +63 -3
- package/src/run-event-stream.ts +4 -1
- package/src/run-event.ts +21 -1
- package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
- package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
- package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
- package/src/workspace/change-set.schema.json +5 -1
- package/src/workspace/check.schema.json +4 -2
- package/src/workspace/checks/records.ts +35 -1
- package/src/workspace/checks.ts +2 -1
- package/src/workspace/ci-green.ts +14 -3
- package/src/workspace/declaration.schema.json +64 -0
- package/src/workspace/declaration.test.ts +28 -0
- package/src/workspace/declaration.ts +38 -0
- package/src/workspace/declared-kinds.ts +40 -1
- package/src/workspace/delivery-metrics.schema.json +519 -0
- package/src/workspace/delivery-metrics.test.ts +236 -0
- package/src/workspace/delivery-metrics.ts +286 -0
- package/src/workspace/flags-read.ts +121 -0
- package/src/workspace/flags.test.ts +141 -0
- package/src/workspace/flags.ts +157 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/intent-cli.ts +46 -5
- package/src/workspace/intent-trace.test.ts +173 -0
- package/src/workspace/intent-trace.ts +484 -0
- package/src/workspace/intent.schema.json +702 -6
- package/src/workspace/intent.ts +148 -25
- package/src/workspace/older-records.test.ts +412 -0
- package/src/workspace/overlap.test.ts +165 -0
- package/src/workspace/overlap.ts +340 -0
- package/src/workspace/read-contract.e2e.test.ts +2 -1
- package/src/workspace/reason-codes.test.ts +5 -0
- package/src/workspace/reason-codes.ts +3 -0
- package/src/workspace/record-assets.test.ts +2 -1
- package/src/workspace/records-contract.e2e.test.ts +2 -1
- package/src/workspace/records.schema.json +21 -2
- package/src/workspace/records.ts +36 -2
- package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/src/workspace/releases.schema.json +123 -1
- package/src/workspace/releases.ts +11 -0
- package/src/workspace/run-event.schema.json +10 -3
- package/src/workspace/status-contract.test.ts +29 -0
- package/src/workspace/status-gates.ts +8 -0
- package/src/workspace/status-mixed.test.ts +152 -0
- package/src/workspace/status-revoked.test.ts +153 -0
- package/src/workspace/status.schema.json +198 -1
- package/src/workspace/status.ts +254 -3
- package/src/workspace/wip.ts +11 -2
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forge release-asset target (#4047): files attached to the release a
|
|
3
|
+
* GitHub, GitLab or Forgejo repository has for the release's tag, through
|
|
4
|
+
* each forge's REST API and an injectable `fetch`.
|
|
5
|
+
*
|
|
6
|
+
* The tag is the declaration's `tag`, else the one the cut made
|
|
7
|
+
* (`release/<label>`, or `release/<member>/<label>`), so a release with no
|
|
8
|
+
* label and no declared tag is refused.
|
|
9
|
+
*
|
|
10
|
+
* A publish is two steps (#4048), so a consumer never sees a release with
|
|
11
|
+
* some of its assets:
|
|
12
|
+
*
|
|
13
|
+
* - GitHub and Forgejo stage on a draft release: the draft for the tag that
|
|
14
|
+
* `GET /repos/{repo}/releases` lists, else a new one,
|
|
15
|
+
* `POST /repos/{repo}/releases` with `draft: true` at the release's commit.
|
|
16
|
+
* GitHub uploads each asset to the draft's `upload_url`, Forgejo with a
|
|
17
|
+
* multipart `POST /repos/{repo}/releases/{id}/assets`. The reveal publishes
|
|
18
|
+
* the draft, `PATCH /repos/{repo}/releases/{id}` with `draft: false`, and
|
|
19
|
+
* the forge creates the tag then if it was not pushed.
|
|
20
|
+
* - GitLab has no draft release, so its stage puts each asset in the
|
|
21
|
+
* project's generic package registry (package `release`, version the
|
|
22
|
+
* label) and makes no release. The reveal creates the release at the
|
|
23
|
+
* commit with a link to each asset, in one `POST /projects/{id}/releases`.
|
|
24
|
+
*
|
|
25
|
+
* An asset already on the draft with the same name is left as it is when
|
|
26
|
+
* its `digest` matches (GitHub), so a stage run again uploads only the rest;
|
|
27
|
+
* one that differs, or one on a forge that reports no digest (Forgejo), is
|
|
28
|
+
* refused (`publish-forge-asset-exists`). A release already published for
|
|
29
|
+
* the tag is refused (`publish-forge-release-published`), since assets
|
|
30
|
+
* uploaded to it would show one at a time, unless it already holds every
|
|
31
|
+
* asset, which is what a publish revealed earlier leaves (on Forgejo and
|
|
32
|
+
* GitLab, which report no digest, by name).
|
|
33
|
+
*
|
|
34
|
+
* With the declared assets the stage attaches two more files (#4049): the
|
|
35
|
+
* SBOM (the file `sbom` names, else `sbom.spdx.json` written from the
|
|
36
|
+
* member's lockfile) and `release.intoto.json`, the release's provenance
|
|
37
|
+
* statement over the assets' digests, which carries the cut's signature
|
|
38
|
+
* (./publish-attachments.ts). They go on the draft with the assets, so
|
|
39
|
+
* nothing is visible before they are attached.
|
|
40
|
+
*
|
|
41
|
+
* The token is read from `tokenEnv`, else `GITHUB_TOKEN`, `GITLAB_TOKEN`
|
|
42
|
+
* then `CI_JOB_TOKEN`, or `FORGEJO_TOKEN`, and is masked in all output.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { readFileSync } from "node:fs";
|
|
46
|
+
import { basename } from "node:path";
|
|
47
|
+
import { memberSbom, releaseProvenance } from "./publish-attachments";
|
|
48
|
+
import { readCredential, type Credential } from "./publish-credentials";
|
|
49
|
+
import type { ForgePublishDeclaration } from "./publish-declaration";
|
|
50
|
+
import { bytesDigest, checkReleaseDigest, memberFile, publishTargetKey } from "./publish-targets";
|
|
51
|
+
import { PublishTargetError, type PublishContext, type PublishDeps, type PublishFetch, type PublishItem, type PublishTarget, type StagedTarget } from "./release-publish";
|
|
52
|
+
|
|
53
|
+
/** The variables a forge's token is read from, in order. */
|
|
54
|
+
export function forgeTokenEnv(decl: Pick<ForgePublishDeclaration, "forge" | "tokenEnv">): string[] {
|
|
55
|
+
if (decl.tokenEnv) return [decl.tokenEnv];
|
|
56
|
+
if (decl.forge === "github") return ["GITHUB_TOKEN"];
|
|
57
|
+
if (decl.forge === "gitlab") return ["GITLAB_TOKEN", "CI_JOB_TOKEN"];
|
|
58
|
+
return ["FORGEJO_TOKEN"];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The API root a forge is reached at. */
|
|
62
|
+
export function forgeApi(decl: Pick<ForgePublishDeclaration, "forge" | "api">, env: NodeJS.ProcessEnv): string | null {
|
|
63
|
+
const root = decl.api ?? (decl.forge === "github" ? (env.GITHUB_API_URL ?? "https://api.github.com") : decl.forge === "gitlab" ? (env.CI_API_V4_URL ?? "https://gitlab.com/api/v4") : (env.GITHUB_API_URL ?? null));
|
|
64
|
+
return root ? root.replace(/\/+$/, "") : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface Asset {
|
|
68
|
+
path: string;
|
|
69
|
+
name: string;
|
|
70
|
+
bytes: Uint8Array;
|
|
71
|
+
digest: string;
|
|
72
|
+
/** Set on the SBOM and the provenance statement chant attaches (#4049). */
|
|
73
|
+
attachment?: "sbom" | "provenance";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ForgeRelease {
|
|
77
|
+
id: string;
|
|
78
|
+
/** True for a GitHub or Forgejo draft, which no consumer sees. */
|
|
79
|
+
draft: boolean;
|
|
80
|
+
/** Asset names on the release, with the digest the forge reports when it reports one. */
|
|
81
|
+
assets: Map<string, string | null>;
|
|
82
|
+
uploadUrl?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class ForgeClient {
|
|
86
|
+
constructor(
|
|
87
|
+
private readonly decl: ForgePublishDeclaration,
|
|
88
|
+
private readonly api: string,
|
|
89
|
+
private readonly token: Credential,
|
|
90
|
+
private readonly fetch: PublishFetch,
|
|
91
|
+
private readonly ctx: PublishContext,
|
|
92
|
+
) {}
|
|
93
|
+
|
|
94
|
+
private headers(): Record<string, string> {
|
|
95
|
+
const t = this.token.value;
|
|
96
|
+
if (this.decl.forge === "github") return { authorization: `Bearer ${t}`, accept: "application/vnd.github+json", "user-agent": "chant" };
|
|
97
|
+
if (this.decl.forge === "forgejo") return { authorization: `token ${t}`, accept: "application/json" };
|
|
98
|
+
return this.token.from === "CI_JOB_TOKEN" ? { "job-token": t } : { "private-token": t };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async call(method: string, url: string, body?: Uint8Array | string, extra: Record<string, string> = {}): Promise<{ status: number; json: unknown; text: string }> {
|
|
102
|
+
const headers = { ...this.headers(), ...(typeof body === "string" ? { "content-type": "application/json" } : {}), ...extra };
|
|
103
|
+
const res = await this.fetch(url, { method, headers, ...(body !== undefined ? { body } : {}) });
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
let json: unknown = null;
|
|
106
|
+
try {
|
|
107
|
+
json = text ? JSON.parse(text) : null;
|
|
108
|
+
} catch {
|
|
109
|
+
json = null;
|
|
110
|
+
}
|
|
111
|
+
return { status: res.status, json, text };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fail(what: string, status: number, text: string): never {
|
|
115
|
+
throw new PublishTargetError("publish-forge-failed", this.ctx.redactor.redact(`${what} on ${this.decl.forge} ${this.decl.repo} answered ${status}${text ? `: ${text.slice(0, 300)}` : ""}`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private get repoPath(): string {
|
|
119
|
+
return this.decl.forge === "gitlab" ? `${this.api}/projects/${encodeURIComponent(this.decl.repo)}` : `${this.api}/repos/${this.decl.repo}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private toRelease(body: Record<string, unknown>, tag: string): ForgeRelease {
|
|
123
|
+
const assets = new Map<string, string | null>();
|
|
124
|
+
if (this.decl.forge === "gitlab") {
|
|
125
|
+
const links = (body.assets as { links?: Array<{ name: string }> } | undefined)?.links ?? [];
|
|
126
|
+
for (const l of links) assets.set(l.name, null);
|
|
127
|
+
} else {
|
|
128
|
+
for (const a of (body.assets as Array<{ name: string; digest?: string | null }> | undefined) ?? []) assets.set(a.name, typeof a.digest === "string" ? a.digest : null);
|
|
129
|
+
}
|
|
130
|
+
return { id: String(body.id ?? body.tag_name ?? tag), draft: body.draft === true, assets, ...(typeof body.upload_url === "string" ? { uploadUrl: body.upload_url } : {}) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The published release for `tag`, or null when the forge has none. A draft is not found here. */
|
|
134
|
+
async published(tag: string): Promise<ForgeRelease | null> {
|
|
135
|
+
const enc = encodeURIComponent(tag);
|
|
136
|
+
const found = await this.call("GET", this.decl.forge === "gitlab" ? `${this.repoPath}/releases/${enc}` : `${this.repoPath}/releases/tags/${enc}`);
|
|
137
|
+
if (found.status === 404) return null;
|
|
138
|
+
if (found.status !== 200 || !found.json) this.fail(`reading release ${tag}`, found.status, found.text);
|
|
139
|
+
return this.toRelease(found.json as Record<string, unknown>, tag);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** GitHub and Forgejo: the draft release for `tag`, made at `commit` when there is none. */
|
|
143
|
+
async draft(tag: string, commit: string, name: string): Promise<ForgeRelease> {
|
|
144
|
+
const list = await this.call("GET", `${this.repoPath}/releases?per_page=100`);
|
|
145
|
+
if (list.status !== 200) this.fail("listing releases", list.status, list.text);
|
|
146
|
+
const drafts = Array.isArray(list.json) ? (list.json as Array<Record<string, unknown>>) : [];
|
|
147
|
+
const mine = drafts.find((r) => r.draft === true && r.tag_name === tag);
|
|
148
|
+
if (mine) {
|
|
149
|
+
this.ctx.log(`using the draft ${this.decl.forge} release for ${tag}`);
|
|
150
|
+
return this.toRelease(mine, tag);
|
|
151
|
+
}
|
|
152
|
+
this.ctx.log(`creating a draft ${this.decl.forge} release for ${tag} at ${commit.slice(0, 12)}`);
|
|
153
|
+
const create = await this.call("POST", `${this.repoPath}/releases`, JSON.stringify({ tag_name: tag, target_commitish: commit, name, draft: true }));
|
|
154
|
+
if (create.status !== 201 || !create.json) this.fail(`creating a draft release ${tag}`, create.status, create.text);
|
|
155
|
+
return this.toRelease(create.json as Record<string, unknown>, tag);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** GitHub and Forgejo: publish the draft `id`. */
|
|
159
|
+
async publishDraft(id: string, tag: string): Promise<void> {
|
|
160
|
+
const res = await this.call("PATCH", `${this.repoPath}/releases/${id}`, JSON.stringify({ draft: false }));
|
|
161
|
+
if (res.status !== 200) this.fail(`publishing the draft release ${tag}`, res.status, res.text);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** GitLab: create the release for `tag` at `commit` with a link to each asset. */
|
|
165
|
+
async createWithLinks(tag: string, commit: string, name: string, links: Array<{ name: string; url: string }>): Promise<void> {
|
|
166
|
+
const res = await this.call(
|
|
167
|
+
"POST",
|
|
168
|
+
`${this.repoPath}/releases`,
|
|
169
|
+
JSON.stringify({ tag_name: tag, ref: commit, name, assets: { links: links.map((l) => ({ ...l, link_type: "package" })) } }),
|
|
170
|
+
);
|
|
171
|
+
if (res.status !== 201) this.fail(`creating release ${tag}`, res.status, res.text);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Upload one asset to `rel`, returning where it can be downloaded. */
|
|
175
|
+
async upload(rel: ForgeRelease | null, version: string, asset: Asset): Promise<string> {
|
|
176
|
+
if (this.decl.forge === "github") {
|
|
177
|
+
const base = (rel?.uploadUrl ?? `${this.repoPath}/releases/${rel?.id}/assets`).replace(/\{[^}]*\}$/, "");
|
|
178
|
+
const res = await this.call("POST", `${base}?name=${encodeURIComponent(asset.name)}`, asset.bytes, { "content-type": "application/octet-stream" });
|
|
179
|
+
if (res.status !== 201) this.fail(`uploading ${asset.name}`, res.status, res.text);
|
|
180
|
+
return String((res.json as { browser_download_url?: string } | null)?.browser_download_url ?? asset.name);
|
|
181
|
+
}
|
|
182
|
+
if (this.decl.forge === "forgejo") {
|
|
183
|
+
const boundary = `chant-${bytesDigest(asset.bytes).slice(7, 31)}`;
|
|
184
|
+
const head = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="attachment"; filename="${asset.name.replace(/"/g, "")}"\r\nContent-Type: application/octet-stream\r\n\r\n`);
|
|
185
|
+
const tail = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
186
|
+
const body = new Uint8Array(Buffer.concat([head, Buffer.from(asset.bytes), tail]));
|
|
187
|
+
const res = await this.call("POST", `${this.repoPath}/releases/${rel?.id}/assets?name=${encodeURIComponent(asset.name)}`, body, { "content-type": `multipart/form-data; boundary=${boundary}` });
|
|
188
|
+
if (res.status !== 201) this.fail(`uploading ${asset.name}`, res.status, res.text);
|
|
189
|
+
return String((res.json as { browser_download_url?: string } | null)?.browser_download_url ?? asset.name);
|
|
190
|
+
}
|
|
191
|
+
const url = `${this.repoPath}/packages/generic/release/${encodeURIComponent(version)}/${encodeURIComponent(asset.name)}`;
|
|
192
|
+
const put = await this.call("PUT", url, asset.bytes, { "content-type": "application/octet-stream" });
|
|
193
|
+
if (put.status !== 201 && put.status !== 200) this.fail(`uploading ${asset.name} to the generic package registry`, put.status, put.text);
|
|
194
|
+
return url;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A GitLab generic package version: the label with what GitLab refuses replaced. */
|
|
199
|
+
export function gitlabPackageVersion(label: string): string {
|
|
200
|
+
return label.replace(/[^0-9A-Za-z.\-_+]/g, "-");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function forgeTarget(decl: ForgePublishDeclaration, deps: PublishDeps): PublishTarget {
|
|
204
|
+
const fetchImpl: PublishFetch = deps.fetch ?? ((url, init) => fetch(url, init as RequestInit));
|
|
205
|
+
const tokenNames = forgeTokenEnv(decl);
|
|
206
|
+
const setup = (ctx: PublishContext) => {
|
|
207
|
+
const tag = decl.tag ?? ctx.releaseTag;
|
|
208
|
+
if (!tag) {
|
|
209
|
+
throw new PublishTargetError("publish-forge-no-tag", `a ${decl.forge} release is named by a tag, and release ${ctx.release.releaseDigest} has no label`, "Cut the release with a label, or set tag in the declaration.");
|
|
210
|
+
}
|
|
211
|
+
const api = forgeApi(decl, ctx.env);
|
|
212
|
+
if (!api) throw new PublishTargetError("publish-forge-no-api", "a Forgejo target needs its API root, and neither api nor GITHUB_API_URL is set", "Set api, such as https://forgejo.example/api/v1.");
|
|
213
|
+
const token = readCredential(ctx.env, tokenNames, ctx.redactor);
|
|
214
|
+
if (!token) {
|
|
215
|
+
throw new PublishTargetError("publish-credentials-missing", `the ${decl.forge} target reads its token from ${tokenNames.join(" or ")}, and none is set`, `Set ${tokenNames[0]} in the job's or the shell's environment.`);
|
|
216
|
+
}
|
|
217
|
+
const assets: Asset[] = decl.assets.map((p) => {
|
|
218
|
+
const abs = memberFile(ctx.cwd, p);
|
|
219
|
+
const bytes = new Uint8Array(readFileSync(abs));
|
|
220
|
+
const digest = bytesDigest(bytes);
|
|
221
|
+
checkReleaseDigest(ctx.release, "asset", ctx.cwd, p, [digest]);
|
|
222
|
+
return { path: p, name: basename(p), bytes, digest };
|
|
223
|
+
});
|
|
224
|
+
// #4049: the SBOM and the provenance statement over the assets go on the release with them.
|
|
225
|
+
const sbom = memberSbom(ctx, decl.sbom, `${decl.repo}@${tag}`, `the ${decl.forge} target`);
|
|
226
|
+
const provenance = releaseProvenance(
|
|
227
|
+
ctx,
|
|
228
|
+
assets.map((a) => ({ name: a.name, digest: a.digest })),
|
|
229
|
+
);
|
|
230
|
+
for (const a of [sbom, provenance]) if (a) assets.push({ path: a.name, name: a.name, bytes: a.bytes, digest: a.digest, attachment: a.kind });
|
|
231
|
+
const names = new Set<string>();
|
|
232
|
+
for (const a of assets) {
|
|
233
|
+
if (names.has(a.name)) throw new PublishTargetError("publish-forge-asset-name-taken", `two assets are named ${a.name}, and a release holds one asset per name`);
|
|
234
|
+
names.add(a.name);
|
|
235
|
+
}
|
|
236
|
+
return { tag, api, token, assets };
|
|
237
|
+
};
|
|
238
|
+
const att = (a: Asset): Pick<PublishItem, "attachment"> => (a.attachment ? { attachment: a.attachment } : {});
|
|
239
|
+
const where = (tag: string, name: string) => `${decl.forge} ${decl.repo} release ${tag}, asset ${name}`;
|
|
240
|
+
const exists = (tag: string, a: Asset, theirs: string | null | undefined, before: PublishItem[]) =>
|
|
241
|
+
new PublishTargetError(
|
|
242
|
+
"publish-forge-asset-exists",
|
|
243
|
+
`release ${tag} on ${decl.forge} ${decl.repo} already has an asset named ${a.name}${theirs ? ` with digest ${theirs}, not ${a.digest}` : ", and the forge reports no digest to compare"}${before.length > 0 ? `; ${before.map((i) => i.name).join(", ")} were staged before it` : ""}`,
|
|
244
|
+
"Delete the asset on the forge, or cut a new release.",
|
|
245
|
+
);
|
|
246
|
+
return {
|
|
247
|
+
kind: "forge",
|
|
248
|
+
name: `${decl.forge} release assets ${decl.repo}`,
|
|
249
|
+
destination: `${decl.forge} ${decl.repo}`,
|
|
250
|
+
credentialEnv: tokenNames,
|
|
251
|
+
key: publishTargetKey(decl),
|
|
252
|
+
async plan(ctx) {
|
|
253
|
+
const { tag, assets } = setup(ctx);
|
|
254
|
+
return assets.map((a) => ({ name: a.path, destination: where(tag, a.name), digest: a.digest, ...att(a) }));
|
|
255
|
+
},
|
|
256
|
+
async stage(ctx): Promise<StagedTarget> {
|
|
257
|
+
const { tag, api, token, assets } = setup(ctx);
|
|
258
|
+
ctx.log(`using the token from ${token.from}`);
|
|
259
|
+
const client = new ForgeClient(decl, api, token, fetchImpl, ctx);
|
|
260
|
+
const done = (items: PublishItem[], state: Record<string, string>): StagedTarget => ({ target: this.name, destination: this.destination, items, state });
|
|
261
|
+
|
|
262
|
+
// A release already published: done when it holds every asset, else refused, since uploads to it would show one at a time.
|
|
263
|
+
const published = await client.published(tag);
|
|
264
|
+
if (published) {
|
|
265
|
+
const items: PublishItem[] = [];
|
|
266
|
+
for (const a of assets) {
|
|
267
|
+
if (!published.assets.has(a.name)) {
|
|
268
|
+
throw new PublishTargetError(
|
|
269
|
+
"publish-forge-release-published",
|
|
270
|
+
`release ${tag} on ${decl.forge} ${decl.repo} is already published without ${a.name}, so the assets uploaded to it would show one at a time`,
|
|
271
|
+
"Delete the forge release so the publish makes it whole, or set tag in the declaration to a tag with no release.",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const theirs = published.assets.get(a.name);
|
|
275
|
+
if (theirs !== a.digest && !(decl.forge !== "github" && theirs === null)) throw exists(tag, a, theirs, items);
|
|
276
|
+
items.push({ name: a.path, destination: where(tag, a.name), digest: a.digest, ...att(a), present: true });
|
|
277
|
+
}
|
|
278
|
+
ctx.log(`release ${tag} is already published with every asset`);
|
|
279
|
+
return done(items, { published: "true" });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const version = gitlabPackageVersion(ctx.label ?? tag);
|
|
283
|
+
if (decl.forge === "gitlab") {
|
|
284
|
+
// No draft on GitLab: the assets go to the package registry, and the release is made at the reveal.
|
|
285
|
+
const items: PublishItem[] = [];
|
|
286
|
+
for (const a of assets) {
|
|
287
|
+
ctx.log(`uploading ${a.name} (${a.bytes.byteLength} bytes) to the generic package registry`);
|
|
288
|
+
items.push({ name: a.path, destination: await client.upload(null, version, a), digest: a.digest, ...att(a) });
|
|
289
|
+
}
|
|
290
|
+
return done(items, {});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const rel = await client.draft(tag, ctx.release.commit, ctx.label ?? tag);
|
|
294
|
+
const items: PublishItem[] = [];
|
|
295
|
+
for (const a of assets) {
|
|
296
|
+
if (rel.assets.has(a.name)) {
|
|
297
|
+
const theirs = rel.assets.get(a.name);
|
|
298
|
+
if (theirs !== a.digest) throw exists(tag, a, theirs, items);
|
|
299
|
+
items.push({ name: a.path, destination: where(tag, a.name), digest: a.digest, ...att(a), present: true });
|
|
300
|
+
ctx.log(`${a.name} is already on the draft release ${tag}`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
ctx.log(`uploading ${a.name} (${a.bytes.byteLength} bytes) to the draft`);
|
|
304
|
+
items.push({ name: a.path, destination: await client.upload(rel, version, a), digest: a.digest, ...att(a) });
|
|
305
|
+
}
|
|
306
|
+
return done(items, { releaseId: rel.id });
|
|
307
|
+
},
|
|
308
|
+
async reveal(ctx, staged) {
|
|
309
|
+
const { tag, api, token } = setup(ctx);
|
|
310
|
+
const client = new ForgeClient(decl, api, token, fetchImpl, ctx);
|
|
311
|
+
const items = staged.items.map((i) => ({ name: i.name, destination: i.destination, ...(i.digest ? { digest: i.digest } : {}), ...(i.attachment ? { attachment: i.attachment } : {}) }));
|
|
312
|
+
if (staged.state.published === "true") return { target: this.name, destination: this.destination, items: items.map((i) => ({ ...i, present: true })) };
|
|
313
|
+
if (decl.forge === "gitlab") {
|
|
314
|
+
ctx.log(`creating the gitlab release ${tag} at ${ctx.release.commit.slice(0, 12)} with ${items.length} asset link(s)`);
|
|
315
|
+
await client.createWithLinks(tag, ctx.release.commit, ctx.label ?? tag, items.map((i) => ({ name: basename(i.name), url: i.destination })));
|
|
316
|
+
} else {
|
|
317
|
+
const id = staged.state.releaseId;
|
|
318
|
+
if (!id) throw new PublishTargetError("publish-forge-failed", `the staged ${decl.forge} release for ${tag} has no draft id to publish`);
|
|
319
|
+
ctx.log(`publishing the draft ${decl.forge} release ${tag}`);
|
|
320
|
+
await client.publishDraft(id, tag);
|
|
321
|
+
}
|
|
322
|
+
return { target: this.name, destination: this.destination, items };
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The npm publish target (#4047): `npm publish` for each package the
|
|
3
|
+
* declaration lists, in the order it lists them, so a set of packages that
|
|
4
|
+
* depend on each other goes out dependencies first.
|
|
5
|
+
*
|
|
6
|
+
* Each package is a directory with a `package.json` or a `.tgz` that
|
|
7
|
+
* `npm pack` wrote, relative to the member.
|
|
8
|
+
*
|
|
9
|
+
* A publish is two steps (#4048), as chant's own releases are (#3903):
|
|
10
|
+
*
|
|
11
|
+
* - stage: for each package, `npm view <name>@<version> version` asks the
|
|
12
|
+
* registry whether it already holds the version, and when it does not,
|
|
13
|
+
*
|
|
14
|
+
* npm publish <package> --tag next [--registry <url>] [--access <access>] [--provenance]
|
|
15
|
+
*
|
|
16
|
+
* puts it out under the `next` dist-tag, where nothing that resolves
|
|
17
|
+
* `latest` or a range finds it. A version already there is not published
|
|
18
|
+
* again, so a stage run after a failure publishes only the rest.
|
|
19
|
+
* - reveal: `npm dist-tag add <name>@<version> latest` for each package in
|
|
20
|
+
* the declared order. The first package is the set's core: it moves first,
|
|
21
|
+
* so a consumer never resolves a new package whose new core is not `latest`
|
|
22
|
+
* yet. Order the list core first, then the packages that depend on it.
|
|
23
|
+
* `ctx.tag` names another tag to move in place of `latest`.
|
|
24
|
+
*
|
|
25
|
+
* `--provenance` is passed when the run has an OIDC token (`detectProvenance`
|
|
26
|
+
* in ./release-publish.ts), so a CI publish gets npm provenance, and npm's
|
|
27
|
+
* trusted publishing exchanges that token for its own. With that token, the
|
|
28
|
+
* reveal then checks what it published (#4049): in an empty directory it
|
|
29
|
+
* installs every package at its version with `npm install --ignore-scripts`
|
|
30
|
+
* and runs `npm audit signatures`, which fails on a package whose registry
|
|
31
|
+
* signature or provenance attestation does not verify. From a workstation
|
|
32
|
+
* there is no npm provenance, and the release's signature, which
|
|
33
|
+
* `chant release verify` checks, is the only provenance.
|
|
34
|
+
*
|
|
35
|
+
* Each package carries its SBOM (#4049): npm keeps no file beside a package,
|
|
36
|
+
* so the SBOM is a file at the package's root, the one `sbom` names or the
|
|
37
|
+
* first of `sbom.spdx.json`, `sbom.cdx.json`, `sbom.cyclonedx.json` it holds,
|
|
38
|
+
* such as `npm sbom` writes. A package with none is refused
|
|
39
|
+
* (`publish-sbom-missing`) unless the declaration says `sbom: false`. Moving a dist-tag over
|
|
40
|
+
* OIDC needs "Allow npm dist-tag" on the package's trusted-publisher record.
|
|
41
|
+
* npm makes the first version of a new package `latest` whatever tag it is
|
|
42
|
+
* published under, so a package's first release is visible when it stages.
|
|
43
|
+
*
|
|
44
|
+
* Credentials are npm's own. chant reads no npm token and writes no `.npmrc`:
|
|
45
|
+
* npm authenticates with the registry's `_authToken` in the user's or the
|
|
46
|
+
* CI's npm config (`actions/setup-node` writes one that reads
|
|
47
|
+
* `NODE_AUTH_TOKEN`), or by trusted publishing. npm runs with the run's
|
|
48
|
+
* environment, and everything it prints is redacted before chant prints it.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
52
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
53
|
+
import { tmpdir } from "node:os";
|
|
54
|
+
import { join } from "node:path";
|
|
55
|
+
import { packageSbom } from "./publish-attachments";
|
|
56
|
+
import type { NpmPublishDeclaration } from "./publish-declaration";
|
|
57
|
+
import { checkReleaseDigest, fileDigest, isDir, memberFile, publishTargetKey } from "./publish-targets";
|
|
58
|
+
import { NPM_STAGE_TAG, PublishTargetError, type PublishContext, type PublishDeps, type PublishItem, type PublishTarget } from "./release-publish";
|
|
59
|
+
|
|
60
|
+
interface NpmPackage {
|
|
61
|
+
path: string;
|
|
62
|
+
abs: string;
|
|
63
|
+
name: string;
|
|
64
|
+
version: string;
|
|
65
|
+
digest?: string;
|
|
66
|
+
/** The SBOM file at its root, or undefined under `sbom: false`. */
|
|
67
|
+
sbom?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Read a package's name and version, from its directory or its packed `.tgz`, and find the SBOM it carries. */
|
|
71
|
+
function readPackage(ctx: PublishContext, path: string, sbom: NpmPublishDeclaration["sbom"]): NpmPackage {
|
|
72
|
+
const abs = memberFile(ctx.cwd, path);
|
|
73
|
+
let text: string;
|
|
74
|
+
let digest: string | undefined;
|
|
75
|
+
const directory = isDir(abs);
|
|
76
|
+
if (directory) {
|
|
77
|
+
try {
|
|
78
|
+
text = readFileSync(join(abs, "package.json"), "utf-8");
|
|
79
|
+
} catch {
|
|
80
|
+
throw new PublishTargetError("publish-npm-not-a-package", `${path} has no package.json`);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
digest = fileDigest(abs);
|
|
84
|
+
checkReleaseDigest(ctx.release, "asset", ctx.cwd, path, [digest]);
|
|
85
|
+
try {
|
|
86
|
+
text = execFileSync("tar", ["-xzOf", abs, "package/package.json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
87
|
+
} catch {
|
|
88
|
+
throw new PublishTargetError("publish-npm-not-a-package", `${path} is not a package tarball with package/package.json`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let pkg: { name?: unknown; version?: unknown; private?: unknown };
|
|
92
|
+
try {
|
|
93
|
+
pkg = JSON.parse(text);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new PublishTargetError("publish-npm-not-a-package", `${path}'s package.json is not JSON`);
|
|
96
|
+
}
|
|
97
|
+
if (typeof pkg.name !== "string" || typeof pkg.version !== "string") {
|
|
98
|
+
throw new PublishTargetError("publish-npm-not-a-package", `${path}'s package.json has no name and version`);
|
|
99
|
+
}
|
|
100
|
+
if (pkg.private === true) {
|
|
101
|
+
throw new PublishTargetError("publish-npm-private", `${pkg.name} (${path}) is private, and npm refuses to publish it`, "Remove it from release.publish, or remove private: true.");
|
|
102
|
+
}
|
|
103
|
+
const carried = packageSbom(abs, directory, path, pkg.name, sbom);
|
|
104
|
+
return { path, abs, name: pkg.name, version: pkg.version, ...(digest ? { digest } : {}), ...(carried ? { sbom: carried.file } : {}) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The arguments of one `npm publish`, which stages under `tag`. */
|
|
108
|
+
export function npmPublishArgs(decl: Pick<NpmPublishDeclaration, "registry" | "access">, abs: string, ctx: Pick<PublishContext, "provenance">, tag: string = NPM_STAGE_TAG): string[] {
|
|
109
|
+
return [
|
|
110
|
+
"publish",
|
|
111
|
+
abs,
|
|
112
|
+
"--tag",
|
|
113
|
+
tag,
|
|
114
|
+
...(decl.registry ? ["--registry", decl.registry] : []),
|
|
115
|
+
...(decl.access ? ["--access", decl.access] : []),
|
|
116
|
+
...(ctx.provenance.oidc ? ["--provenance"] : []),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The arguments of the `npm dist-tag add` that reveals a staged version under `tag`. */
|
|
121
|
+
export function npmDistTagArgs(decl: Pick<NpmPublishDeclaration, "registry">, name: string, version: string, tag: string): string[] {
|
|
122
|
+
return ["dist-tag", "add", `${name}@${version}`, tag, ...(decl.registry ? ["--registry", decl.registry] : [])];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The arguments of the `npm install` that fetches the published packages into an empty directory for `npm audit signatures`. */
|
|
126
|
+
export function npmAuditInstallArgs(decl: Pick<NpmPublishDeclaration, "registry">): string[] {
|
|
127
|
+
return ["install", "--ignore-scripts", "--no-audit", "--no-fund", ...(decl.registry ? ["--registry", decl.registry] : [])];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The arguments of `npm audit signatures`. */
|
|
131
|
+
export function npmAuditSignaturesArgs(decl: Pick<NpmPublishDeclaration, "registry">): string[] {
|
|
132
|
+
return ["audit", "signatures", ...(decl.registry ? ["--registry", decl.registry] : [])];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function run(bin: string, args: string[], ctx: PublishContext, cwd: string = ctx.cwd): Promise<{ code: number; output: string[] }> {
|
|
136
|
+
return new Promise((resolvePromise, reject) => {
|
|
137
|
+
const child = spawn(bin, args, { cwd, env: ctx.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
138
|
+
const output: string[] = [];
|
|
139
|
+
let partial = { out: "", err: "" };
|
|
140
|
+
const take = (key: "out" | "err") => (chunk: Buffer) => {
|
|
141
|
+
const text = partial[key] + chunk.toString("utf-8");
|
|
142
|
+
const lines = text.split("\n");
|
|
143
|
+
partial = { ...partial, [key]: lines.pop() ?? "" };
|
|
144
|
+
for (const l of lines) if (l.trim() !== "") output.push(ctx.redactor.redact(l));
|
|
145
|
+
};
|
|
146
|
+
child.stdout.on("data", take("out"));
|
|
147
|
+
child.stderr.on("data", take("err"));
|
|
148
|
+
child.on("error", reject);
|
|
149
|
+
child.on("close", (code) => {
|
|
150
|
+
for (const l of [partial.out, partial.err]) if (l.trim() !== "") output.push(ctx.redactor.redact(l));
|
|
151
|
+
resolvePromise({ code: code ?? 1, output });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function npmTarget(decl: NpmPublishDeclaration, deps: PublishDeps): PublishTarget {
|
|
157
|
+
const bin = deps.npm ?? "npm";
|
|
158
|
+
const registry = decl.registry ?? "the registry npm's config names";
|
|
159
|
+
const shown = (tag: string) => `${registry}, dist-tag ${tag}`;
|
|
160
|
+
const revealTag = (ctx: PublishContext) => ctx.tag ?? "latest";
|
|
161
|
+
const npm = async (ctx: PublishContext, args: string[], cwd?: string): Promise<{ code: number; output: string[] }> => {
|
|
162
|
+
try {
|
|
163
|
+
return await run(bin, args, ctx, cwd);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new PublishTargetError("publish-npm-failed", ctx.redactor.redact(`npm could not be run: ${err instanceof Error ? err.message : String(err)}`), "Install npm, or put it on PATH.");
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/** Whether the registry already holds the version. A failed lookup reads as no, and the publish that follows says why. */
|
|
169
|
+
const onRegistry = async (ctx: PublishContext, pkg: NpmPackage): Promise<boolean> => {
|
|
170
|
+
const r = await npm(ctx, ["view", `${pkg.name}@${pkg.version}`, "version", ...(decl.registry ? ["--registry", decl.registry] : [])]);
|
|
171
|
+
return r.code === 0 && r.output.some((l) => l.trim() === pkg.version || l.trim() === JSON.stringify(pkg.version));
|
|
172
|
+
};
|
|
173
|
+
const read = (ctx: PublishContext) => decl.packages.map((p) => readPackage(ctx, p, decl.sbom));
|
|
174
|
+
const extra = (pkg: NpmPackage) => ({ ...(pkg.digest ? { digest: pkg.digest } : {}), ...(pkg.sbom ? { sbom: pkg.sbom } : {}) });
|
|
175
|
+
const plan = async (ctx: PublishContext): Promise<PublishItem[]> =>
|
|
176
|
+
read(ctx).map((pkg) => ({ name: `${pkg.name}@${pkg.version}`, destination: `${shown(NPM_STAGE_TAG)}, then ${revealTag(ctx)}`, ...extra(pkg) }));
|
|
177
|
+
/**
|
|
178
|
+
* In CI, install what was published into an empty directory and run
|
|
179
|
+
* `npm audit signatures` there (#4049), which checks each package's
|
|
180
|
+
* registry signature and provenance attestation.
|
|
181
|
+
*/
|
|
182
|
+
const audit = async (ctx: PublishContext, pkgs: NpmPackage[]): Promise<void> => {
|
|
183
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-npm-audit-"));
|
|
184
|
+
try {
|
|
185
|
+
const dependencies = Object.fromEntries(pkgs.map((p) => [p.name, p.version]));
|
|
186
|
+
writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "chant-publish-audit", version: "0.0.0", private: true, dependencies }, null, 2)}\n`);
|
|
187
|
+
for (const args of [npmAuditInstallArgs(decl), npmAuditSignaturesArgs(decl)]) {
|
|
188
|
+
ctx.log(`npm ${args.join(" ")}`);
|
|
189
|
+
const result = await npm(ctx, args, dir);
|
|
190
|
+
for (const line of result.output) ctx.log(` ${line}`);
|
|
191
|
+
if (result.code !== 0) {
|
|
192
|
+
throw new PublishTargetError(
|
|
193
|
+
"publish-npm-audit-failed",
|
|
194
|
+
`npm ${args.slice(0, 2).join(" ")} exited ${result.code} for ${pkgs.map((p) => `${p.name}@${p.version}`).join(", ")}, after latest moved: ${result.output.at(-1) ?? "npm printed nothing"}`,
|
|
195
|
+
"Each package was published; check its signature and provenance on the registry. Running the publish again moves no tag that moved and audits again.",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
rmSync(dir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
kind: "npm",
|
|
205
|
+
name: `npm (${decl.packages.length} package${decl.packages.length === 1 ? "" : "s"})`,
|
|
206
|
+
destination: `npm ${registry}`,
|
|
207
|
+
credentialEnv: [],
|
|
208
|
+
key: publishTargetKey(decl),
|
|
209
|
+
plan,
|
|
210
|
+
async stage(ctx) {
|
|
211
|
+
const pkgs = read(ctx);
|
|
212
|
+
const items: PublishItem[] = [];
|
|
213
|
+
for (const pkg of pkgs) {
|
|
214
|
+
const item: PublishItem = { name: `${pkg.name}@${pkg.version}`, destination: shown(NPM_STAGE_TAG), ...extra(pkg) };
|
|
215
|
+
if (await onRegistry(ctx, pkg)) {
|
|
216
|
+
ctx.log(`${item.name} is already on the registry; not published again`);
|
|
217
|
+
items.push({ ...item, present: true });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const args = npmPublishArgs(decl, pkg.abs, ctx);
|
|
221
|
+
ctx.log(`npm publish ${pkg.path} ${args.slice(2).join(" ")}`);
|
|
222
|
+
const result = await npm(ctx, args);
|
|
223
|
+
for (const line of result.output) ctx.log(` ${line}`);
|
|
224
|
+
if (result.code !== 0) {
|
|
225
|
+
const done = items.map((i) => i.name);
|
|
226
|
+
throw new PublishTargetError(
|
|
227
|
+
"publish-npm-failed",
|
|
228
|
+
`npm publish of ${item.name} exited ${result.code}${done.length > 0 ? `, after ${done.join(", ")} were staged under ${NPM_STAGE_TAG}` : ", before any package was staged"}: ${result.output.at(-1) ?? "npm printed nothing"}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
items.push(item);
|
|
232
|
+
}
|
|
233
|
+
return { target: this.name, destination: this.destination, items, state: {} };
|
|
234
|
+
},
|
|
235
|
+
async reveal(ctx) {
|
|
236
|
+
const tag = revealTag(ctx);
|
|
237
|
+
const pkgs = read(ctx);
|
|
238
|
+
const items: PublishItem[] = [];
|
|
239
|
+
// The declared order: the first package is the set's core, and its tag moves first.
|
|
240
|
+
for (const pkg of pkgs) {
|
|
241
|
+
const args = npmDistTagArgs(decl, pkg.name, pkg.version, tag);
|
|
242
|
+
ctx.log(`npm ${args.join(" ")}`);
|
|
243
|
+
const result = await npm(ctx, args);
|
|
244
|
+
for (const line of result.output) ctx.log(` ${line}`);
|
|
245
|
+
if (result.code !== 0) {
|
|
246
|
+
const moved = items.map((i) => i.name);
|
|
247
|
+
throw new PublishTargetError(
|
|
248
|
+
"publish-npm-tag-failed",
|
|
249
|
+
`npm dist-tag add ${pkg.name}@${pkg.version} ${tag} exited ${result.code}${moved.length > 0 ? `, after ${tag} moved for ${moved.join(", ")}` : `, before ${tag} moved for any package`}: ${result.output.at(-1) ?? "npm printed nothing"}`,
|
|
250
|
+
`Moving a dist-tag needs npm credentials that may change dist-tags; over OIDC that is "Allow npm dist-tag" on the package's trusted-publisher record.`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
items.push({ name: `${pkg.name}@${pkg.version}`, destination: shown(tag), ...extra(pkg) });
|
|
254
|
+
}
|
|
255
|
+
if (ctx.provenance.oidc) await audit(ctx, pkgs);
|
|
256
|
+
return { target: this.name, destination: this.destination, items };
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|