@intentius/chant 0.124.31 → 0.124.33
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/bin/chant +7 -2
- package/dist/ci-jobs.d.ts +4 -0
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +9 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +38 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
- package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-health.js +150 -0
- package/dist/cli/handlers/lifecycle.d.ts +26 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +16 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +610 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +19 -2
- package/dist/cli/handlers/release.d.ts +16 -1
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +83 -11
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +121 -33
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +37 -0
- package/dist/cli/registry.d.ts +29 -6
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/deploy-inputs.d.ts +91 -0
- package/dist/components/deploy-inputs.d.ts.map +1 -0
- package/dist/components/deploy-inputs.js +184 -0
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +56 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/gate-plan.d.ts +8 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +6 -0
- package/dist/components/pr-pipeline.d.ts +98 -12
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +146 -23
- package/dist/components/pr-run.d.ts +128 -30
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/release-compile.d.ts +37 -2
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +64 -8
- package/dist/components/release-promote.d.ts +17 -3
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +79 -18
- package/dist/components/verbs/bom-writer.d.ts +1 -1
- package/dist/components/verbs/bom-writer.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
- package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
- package/dist/components/wave-plan.d.ts +15 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +44 -5
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +32 -7
- package/dist/health.d.ts +110 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +105 -0
- package/dist/health.schema.json +191 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +29 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/merge-record.d.ts +104 -0
- package/dist/lifecycle/merge-record.d.ts.map +1 -0
- package/dist/lifecycle/merge-record.js +102 -0
- package/dist/lifecycle/publish-attachments.d.ts +8 -5
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
- package/dist/lifecycle/publish-attachments.js +27 -13
- package/dist/lifecycle/publish-npm.d.ts +3 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +4 -3
- package/dist/lifecycle/release-at.d.ts +40 -0
- package/dist/lifecycle/release-at.d.ts.map +1 -0
- package/dist/lifecycle/release-at.js +83 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +23 -12
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/release-publish.d.ts +28 -12
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +29 -17
- package/dist/lifecycle/release-store.d.ts +25 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +40 -13
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +4 -2
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +3 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/activities/shell.d.ts +38 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +89 -3
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +7 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +10 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +39 -0
- package/dist/pr-loop.d.ts +155 -7
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +137 -11
- package/dist/pr-report-url.d.ts +4 -4
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +5 -5
- package/dist/runner-start.d.ts +179 -0
- package/dist/runner-start.d.ts.map +1 -0
- package/dist/runner-start.js +277 -0
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +238 -231
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/check.schema.json +70 -2
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/checks/security.d.ts +35 -0
- package/dist/workspace/checks/security.d.ts.map +1 -0
- package/dist/workspace/checks/security.js +305 -0
- package/dist/workspace/checks.d.ts +2 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +5 -0
- package/dist/workspace/ci-builds.d.ts +163 -0
- package/dist/workspace/ci-builds.d.ts.map +1 -0
- package/dist/workspace/ci-builds.js +315 -0
- package/dist/workspace/ci-green-cli.d.ts +2 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +25 -5
- package/dist/workspace/ci-green-forge.d.ts +18 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +12 -0
- package/dist/workspace/ci-green-workflow.d.ts +55 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +128 -1
- package/dist/workspace/ci-green.d.ts +117 -16
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +358 -43
- package/dist/workspace/ci-last-green.schema.json +49 -0
- package/dist/workspace/ci-list.schema.json +47 -7
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +13 -1
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.js +6 -4
- package/dist/workspace/ci-run-index.schema.json +6 -4
- package/dist/workspace/ci-run.d.ts +85 -6
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +262 -64
- package/dist/workspace/ci-runner-key.d.ts +16 -3
- package/dist/workspace/ci-runner-key.d.ts.map +1 -1
- package/dist/workspace/ci-runner-key.js +30 -10
- package/dist/workspace/ci-runner-start.d.ts +77 -0
- package/dist/workspace/ci-runner-start.d.ts.map +1 -0
- package/dist/workspace/ci-runner-start.js +185 -0
- package/dist/workspace/ci-serve.d.ts +130 -11
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +323 -40
- package/dist/workspace/declaration.d.ts +25 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +14 -2
- package/dist/workspace/declaration.schema.json +30 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +8 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +2 -1
- package/dist/workspace/ls.schema.json +9 -2
- package/dist/workspace/member-pipeline.d.ts +1 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +2 -0
- package/dist/workspace/merge-record.schema.json +49 -0
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +75 -8
- package/dist/workspace/reason-codes.d.ts +18 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +20 -2
- package/dist/workspace/release-candidates.d.ts +129 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -0
- package/dist/workspace/release-candidates.js +233 -0
- package/dist/workspace/release-candidates.schema.json +353 -0
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/security.d.ts +40 -0
- package/dist/workspace/security.d.ts.map +1 -0
- package/dist/workspace/security.js +99 -0
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +8 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
- package/src/ci-jobs.ts +11 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +105 -0
- package/src/cli/handlers/build.ts +35 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lifecycle-health.test.ts +123 -0
- package/src/cli/handlers/lifecycle-health.ts +157 -0
- package/src/cli/handlers/lifecycle.ts +1 -1
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
- package/src/cli/handlers/pr-record.test.ts +201 -0
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +156 -0
- package/src/cli/handlers/pr.ts +648 -33
- package/src/cli/handlers/promote.test.ts +43 -1
- package/src/cli/handlers/promote.ts +19 -3
- package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
- package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
- package/src/cli/handlers/release.e2e.test.ts +19 -28
- package/src/cli/handlers/release.ts +81 -11
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +18 -0
- package/src/cli/main.ts +120 -33
- package/src/cli/parse-args.ts +28 -0
- package/src/cli/registry.ts +35 -8
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/deploy-inputs.test.ts +108 -0
- package/src/components/deploy-inputs.ts +225 -0
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +87 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/gate-plan.ts +9 -0
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +184 -28
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +355 -76
- package/src/components/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- package/src/components/release-promote.test.ts +55 -2
- package/src/components/release-promote.ts +93 -23
- package/src/components/verbs/bom-writer.ts +1 -1
- package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
- package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
- package/src/components/wave-plan.ts +48 -8
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/graph-detail.test.ts +57 -0
- package/src/graph-detail.ts +33 -7
- package/src/health.schema.json +191 -0
- package/src/health.test.ts +62 -0
- package/src/health.ts +178 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +43 -1
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/merge-record.test.ts +111 -0
- package/src/lifecycle/merge-record.ts +154 -0
- package/src/lifecycle/publish-attachments.ts +25 -13
- package/src/lifecycle/publish-npm.ts +4 -3
- package/src/lifecycle/publish-targets.test.ts +34 -4
- package/src/lifecycle/release-at.test.ts +48 -0
- package/src/lifecycle/release-at.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +41 -0
- package/src/lifecycle/release-cut.ts +22 -11
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/release-publish.test.ts +43 -13
- package/src/lifecycle/release-publish.ts +38 -18
- package/src/lifecycle/release-store.ts +57 -13
- package/src/lifecycle/run-ledger.test.ts +13 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
- package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/activity-contracts.ts +3 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/activities/shell-container.e2e.test.ts +67 -0
- package/src/op/activities/shell.test.ts +44 -1
- package/src/op/activities/shell.ts +110 -3
- package/src/op/builders.ts +7 -0
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/runtime.ts +2 -0
- package/src/op/step-console.test.ts +4 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +42 -0
- package/src/pr-forge.ts +40 -0
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +146 -1
- package/src/pr-loop.ts +278 -17
- package/src/pr-report-url.ts +7 -7
- package/src/runner-start-bundle.test.ts +29 -0
- package/src/runner-start.test.ts +179 -0
- package/src/runner-start.ts +329 -0
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/check-contract.test.ts +3 -2
- package/src/workspace/check.schema.json +70 -2
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +5 -0
- package/src/workspace/ci-builds.e2e.test.ts +208 -0
- package/src/workspace/ci-builds.test.ts +174 -0
- package/src/workspace/ci-builds.ts +385 -0
- package/src/workspace/ci-green-builds.test.ts +180 -0
- package/src/workspace/ci-green-cli.test.ts +54 -0
- package/src/workspace/ci-green-cli.ts +26 -5
- package/src/workspace/ci-green-forge.ts +30 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green-workflow.test.ts +84 -1
- package/src/workspace/ci-green-workflow.ts +139 -2
- package/src/workspace/ci-green.test.ts +6 -3
- package/src/workspace/ci-green.ts +408 -48
- package/src/workspace/ci-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +47 -7
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +6 -4
- package/src/workspace/ci-run-index.test.ts +42 -3
- package/src/workspace/ci-run-index.ts +19 -5
- package/src/workspace/ci-run.test.ts +203 -8
- package/src/workspace/ci-run.ts +295 -63
- package/src/workspace/ci-runner-key.test.ts +55 -17
- package/src/workspace/ci-runner-key.ts +36 -11
- package/src/workspace/ci-runner-start.test.ts +151 -0
- package/src/workspace/ci-runner-start.ts +193 -0
- package/src/workspace/ci-serve-builds.test.ts +86 -0
- package/src/workspace/ci-serve.test.ts +209 -6
- package/src/workspace/ci-serve.ts +341 -42
- package/src/workspace/declaration.schema.json +30 -2
- package/src/workspace/declaration.test.ts +40 -0
- package/src/workspace/declaration.ts +40 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +9 -2
- package/src/workspace/ls.ts +10 -4
- package/src/workspace/member-pipeline.ts +2 -1
- package/src/workspace/merge-record.schema.json +49 -0
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +75 -8
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +20 -2
- package/src/workspace/release-candidates.schema.json +353 -0
- package/src/workspace/release-candidates.test.ts +106 -0
- package/src/workspace/release-candidates.ts +315 -0
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* #4099: the runner key on a `chant ci serve` or `chant ci run` host. The
|
|
3
3
|
* key is made in the state directory with mode 0600, `--setup-key --trust`
|
|
4
4
|
* proposes its runner entry on a branch through `trust runner add` (#3510)
|
|
5
|
-
* and never writes the target branch
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* and never writes the target branch. Signing is opt-in (#4156): `ci serve`
|
|
6
|
+
* starts with no key and says nothing about it, and serves unsigned with one
|
|
7
|
+
* warning when `.chant/trust.json` at the target's head does not list its
|
|
8
|
+
* key. With `ci.green.signedSince` it refuses to start in either case.
|
|
8
9
|
*
|
|
9
10
|
* Every test works in its own temp repository with a bare origin and its own
|
|
10
11
|
* state directory. The jobs' `chant` is a stub script, and no job runs: the
|
|
@@ -126,9 +127,12 @@ describe("the host's runner key (#4099)", () => {
|
|
|
126
127
|
expect(trust.roles.runner).toEqual(["chant-runner:demo"]);
|
|
127
128
|
expect(git(origin, "rev-parse", `${branch}^`)).toBe(main);
|
|
128
129
|
|
|
129
|
-
// Until it merges, ci serve refuses the
|
|
130
|
+
// Until it merges, ci serve serves unsigned with a warning (refuses with the opt-in); once it has, the key signs as the principal.
|
|
130
131
|
const key = hostRunnerKey(state)!;
|
|
131
|
-
|
|
132
|
+
const pending = ciServeRunnerKey({ top: root, stateDir: state, branch: "main" });
|
|
133
|
+
expect(pending.pem).toBeUndefined();
|
|
134
|
+
expect(pending.warning).toMatch(/not trusted at main/);
|
|
135
|
+
expect(() => ciServeRunnerKey({ top: root, stateDir: state, branch: "main", required: main })).toThrow(/not trusted at main/);
|
|
132
136
|
git(root, "push", "-q", "origin", `refs/remotes/origin/${branch}:refs/heads/main`);
|
|
133
137
|
expect(ciServeRunnerKey({ top: root, stateDir: state, branch: "main" })).toMatchObject({ pem: key.pem, source: join(state, RUNNER_KEY_FILE) });
|
|
134
138
|
expect(ciTagSigner(root, git(root, "rev-parse", "refs/remotes/origin/main"), key.pem).principal).toBe("chant-runner:demo");
|
|
@@ -138,29 +142,63 @@ describe("the host's runner key (#4099)", () => {
|
|
|
138
142
|
expect(listed.err).toContain("lists it for chant-runner:demo, with the runner role");
|
|
139
143
|
});
|
|
140
144
|
|
|
141
|
-
test("ci serve
|
|
145
|
+
test("ci serve starts with no key and says nothing, and serves unsigned with one warning for a key trust.json does not list (#4156)", async () => {
|
|
142
146
|
const { root } = workspace();
|
|
143
147
|
const state = scratchDir("chant-runner-key-state-");
|
|
144
148
|
|
|
149
|
+
// No key: nothing about keys is printed. The serve goes on to the forge, which this test has no token for.
|
|
145
150
|
const none = await cli(root, SERVE(state));
|
|
146
|
-
expect(none.
|
|
147
|
-
expect(none.err).toContain("
|
|
148
|
-
|
|
151
|
+
expect(none.err.split("\n")).toHaveLength(2);
|
|
152
|
+
expect(none.err.split("\n")[0]).toContain("chant ci serve: --forge forgejo needs GITHUB_TOKEN");
|
|
153
|
+
const keyless = ciServeRunnerKey({ top: root, stateDir: state, branch: "main" });
|
|
154
|
+
expect(keyless.pem).toBeUndefined();
|
|
155
|
+
expect(keyless.warning).toBeUndefined();
|
|
156
|
+
expect(keyless.note).toContain("not signed");
|
|
157
|
+
// --verbose prints that one line.
|
|
158
|
+
const verbose = await cli(root, [...SERVE(state), "--verbose"]);
|
|
159
|
+
expect(verbose.err).toContain("ci serve: runs are not signed: there is no runner key");
|
|
160
|
+
|
|
161
|
+
// A key the head does not list: one warning at startup, and no key goes to the jobs.
|
|
149
162
|
ensureHostRunnerKey(state);
|
|
150
163
|
const unlisted = await cli(root, SERVE(state));
|
|
151
|
-
expect(unlisted.
|
|
152
|
-
expect(unlisted.err).
|
|
164
|
+
expect(unlisted.err.match(/not trusted at main [0-9a-f]{12}/g)).toHaveLength(1);
|
|
165
|
+
expect(unlisted.err).toContain("Runs are not signed until it is");
|
|
153
166
|
expect(unlisted.err).toContain("chant ci serve --setup-key --trust");
|
|
154
|
-
expect(unlisted.err).toContain("
|
|
167
|
+
expect(unlisted.err).not.toContain("signing runs with");
|
|
155
168
|
|
|
156
|
-
// A key listed only in the working tree, not at the head, is
|
|
169
|
+
// A key listed only in the working tree, not at the head, is not trusted either.
|
|
157
170
|
const other = generateKeyPairSync("ed25519").privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
158
171
|
writeFileSync(join(state, "other.pem"), other, { mode: 0o600 });
|
|
159
172
|
expect(trustWrite({ cwd: root, action: "runner add", name: "ci-bot", key: join(state, "other.pem") })).toMatchObject({ changed: true });
|
|
160
|
-
const flagged =
|
|
161
|
-
expect(flagged.
|
|
162
|
-
expect(flagged.
|
|
173
|
+
const flagged = ciServeRunnerKey({ top: root, stateDir: state, branch: "main", keyFile: join(state, "other.pem") });
|
|
174
|
+
expect(flagged.pem).toBeUndefined();
|
|
175
|
+
expect(flagged.warning).toContain("lists no runner with this key");
|
|
176
|
+
|
|
177
|
+
// A key file that is not a key is an error either way.
|
|
178
|
+
writeFileSync(join(state, "bad.pem"), "not a key", { mode: 0o600 });
|
|
179
|
+
expect(() => ciServeRunnerKey({ top: root, stateDir: state, branch: "main", keyFile: join(state, "bad.pem") })).toThrow(/not an Ed25519 private key in PEM/);
|
|
180
|
+
|
|
181
|
+
// --unsigned is accepted and serves unsigned.
|
|
182
|
+
expect(ciServeRunnerKey({ top: root, stateDir: state, branch: "main", unsigned: true })).toMatchObject({ fromFlag: false, source: "none (--unsigned)" });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("with ci.green.signedSince, ci serve refuses to start without a trusted key, --unsigned or not (#4156)", async () => {
|
|
186
|
+
const { root, main } = workspace();
|
|
187
|
+
const state = scratchDir("chant-runner-key-state-");
|
|
188
|
+
const green = { branch: "main", phases: { plan: ["pr-plan"] }, require: ["plan"], source: "runs", signedSince: main };
|
|
189
|
+
writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green } }) });
|
|
163
190
|
|
|
164
|
-
|
|
191
|
+
const none = await cli(root, SERVE(state));
|
|
192
|
+
expect(none.code).toBe(1);
|
|
193
|
+
expect(none.err).toContain(`ci.green.signedSince is ${main}, so every run must be signed, and there is no runner key`);
|
|
194
|
+
const unsigned = await cli(root, [...SERVE(state), "--unsigned"]);
|
|
195
|
+
expect(unsigned.code).toBe(1);
|
|
196
|
+
expect(unsigned.err).toContain("there is no runner key");
|
|
197
|
+
|
|
198
|
+
ensureHostRunnerKey(state);
|
|
199
|
+
const unlisted = await cli(root, SERVE(state));
|
|
200
|
+
expect(unlisted.code).toBe(1);
|
|
201
|
+
expect(unlisted.err).toMatch(/not trusted at main [0-9a-f]{12}/);
|
|
202
|
+
expect(unlisted.err).toContain("chant workspace trust runner add");
|
|
165
203
|
});
|
|
166
204
|
});
|
|
@@ -217,17 +217,38 @@ export function formatRunnerKeySetup(s: RunnerKeySetup, branch: string): string
|
|
|
217
217
|
/**
|
|
218
218
|
* The key `ci serve` signs with, checked before its first poll (#4099): the
|
|
219
219
|
* `--key` file, else `CHANT_RUNNER_KEY`, else the host's key in the state
|
|
220
|
-
* directory.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* directory. Signing is opt-in (#4156):
|
|
221
|
+
*
|
|
222
|
+
* - With no key, `ci serve` serves unsigned and says so only in `note`.
|
|
223
|
+
* - A key `.chant/trust.json` at the target's head does not list with the
|
|
224
|
+
* runner role, or a target branch it can't find to check against, leaves
|
|
225
|
+
* it serving unsigned, with one `warning`.
|
|
226
|
+
* - With `required` (the declaration's `ci.green.signedSince`) each of
|
|
227
|
+
* those refuses to start, as before #4156.
|
|
228
|
+
*
|
|
229
|
+
* A key that is not an Ed25519 private key in PEM, or a `--key` file that
|
|
230
|
+
* can't be read, refuses either way. `unsigned` (`--unsigned`) skips the
|
|
231
|
+
* key without `required` and is ignored with it.
|
|
223
232
|
*/
|
|
224
|
-
export function ciServeRunnerKey(req: {
|
|
225
|
-
|
|
233
|
+
export function ciServeRunnerKey(req: {
|
|
234
|
+
top: string;
|
|
235
|
+
stateDir: string;
|
|
236
|
+
branch: string;
|
|
237
|
+
keyFile?: string;
|
|
238
|
+
envKey?: string;
|
|
239
|
+
unsigned?: boolean;
|
|
240
|
+
/** The declaration's `ci.green.signedSince`, when it sets one: every run must then be signed. */
|
|
241
|
+
required?: string | null;
|
|
242
|
+
}): { pem?: string; fromFlag: boolean; source: string; warning?: string; note?: string } {
|
|
243
|
+
const required = req.required ?? null;
|
|
244
|
+
const optIn = `ci.green.signedSince is ${required}, so every run must be signed`;
|
|
245
|
+
if (req.unsigned && !required) return { fromFlag: false, source: "none (--unsigned)", note: "runs are not signed (--unsigned)" };
|
|
226
246
|
const host = req.keyFile === undefined && !req.envKey ? hostRunnerKey(req.stateDir) : undefined;
|
|
227
247
|
const pem = req.keyFile !== undefined ? readFileSync(req.keyFile, "utf-8") : req.envKey || host?.pem;
|
|
228
248
|
const source = req.keyFile !== undefined ? req.keyFile : req.envKey ? "CHANT_RUNNER_KEY" : (host?.path ?? join(req.stateDir, RUNNER_KEY_FILE));
|
|
229
249
|
if (pem === undefined) {
|
|
230
|
-
|
|
250
|
+
if (!required) return { fromFlag: false, source: "none", note: "runs are not signed: there is no runner key. To verify who ran a job, add one with chant ci serve --setup-key --trust" };
|
|
251
|
+
throw new CiRunError(`${optIn}, and there is no runner key: no --key, no CHANT_RUNNER_KEY and no ${source}. Make one with chant ci serve --setup-key and list it with --trust`);
|
|
231
252
|
}
|
|
232
253
|
let publicKey: string;
|
|
233
254
|
try {
|
|
@@ -236,13 +257,17 @@ export function ciServeRunnerKey(req: { top: string; stateDir: string; branch: s
|
|
|
236
257
|
throw new CiRunError(`the runner key from ${source} is not an Ed25519 private key in PEM: ${message(err)}`);
|
|
237
258
|
}
|
|
238
259
|
const head = targetHead(req.top, req.branch);
|
|
239
|
-
if (!head)
|
|
260
|
+
if (!head) {
|
|
261
|
+
const why = `there is no branch ${req.branch} here or on origin, so the runner key can't be checked against its .chant/trust.json`;
|
|
262
|
+
if (required) throw new CiRunError(`${optIn}, and ${why}; fetch it`);
|
|
263
|
+
return { fromFlag: false, source, warning: `${why}, so runs are not signed; fetch it to sign them` };
|
|
264
|
+
}
|
|
240
265
|
const listed = runnerKeyListed(req.top, head.commit, pem);
|
|
241
266
|
if (!listed.ok) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
const why = `the runner key from ${source} is not trusted at ${branchAt(req.branch, head.commit)}: ${listed.reason}`;
|
|
268
|
+
const fix = `List it on a branch for review with chant ci serve --setup-key --trust, or run ${trustCommand(defaultRunnerPrincipal(), publicKey)} in a checkout of ${req.branch} and merge the change.`;
|
|
269
|
+
if (required) throw new CiRunError(`${optIn}, and ${why}\n ${fix}`);
|
|
270
|
+
return { fromFlag: false, source, warning: `${why}. Runs are not signed until it is. ${fix}` };
|
|
246
271
|
}
|
|
247
272
|
return { pem, fromFlag: req.keyFile !== undefined, source };
|
|
248
273
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant ci runner-start` and `chant ci run --require-runner-start` (#4147):
|
|
3
|
+
* the statement checked against `.chant/trust.json` at base, and the
|
|
4
|
+
* readiness the isolated-runners security group reads (#4168).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterAll, describe, expect, test, vi } from "vitest";
|
|
11
|
+
import { parseArgs } from "../cli/parse-args";
|
|
12
|
+
import type { CommandContext } from "../cli/registry";
|
|
13
|
+
import { signRunnerStart, type CiJobIdentity } from "../runner-start";
|
|
14
|
+
import { cleanScratch, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
15
|
+
import { checkRunnerStart, parseJobSpec, requireRunnerStart, runCiRunnerStart, runnerStartReadiness, runnerStartReadinessAt } from "./ci-runner-start";
|
|
16
|
+
import { ciRun, CiRunError } from "./ci-run";
|
|
17
|
+
import { sshFromEd25519 } from "./trust/dsse";
|
|
18
|
+
import { emptyPolicy } from "./trust/policy";
|
|
19
|
+
|
|
20
|
+
afterAll(cleanScratch);
|
|
21
|
+
|
|
22
|
+
function key(principal: string): { principal: string; pem: string; ssh: string; signer: Parameters<typeof signRunnerStart>[1] } {
|
|
23
|
+
const { privateKey } = generateKeyPairSync("ed25519");
|
|
24
|
+
const jwk = createPublicKey(privateKey).export({ format: "jwk" }) as { x: string };
|
|
25
|
+
const ssh = sshFromEd25519(Buffer.from(jwk.x, "base64url"));
|
|
26
|
+
return { principal, pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(), ssh, signer: { principal, key: privateKey, publicKey: ssh } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PROVISIONER = key("provisioner");
|
|
30
|
+
const LATER = key("late-provisioner");
|
|
31
|
+
|
|
32
|
+
/** A repository whose first commit lists PROVISIONER as a runner, and whose second adds LATER. */
|
|
33
|
+
function repo(): { root: string; base: string; head: string } {
|
|
34
|
+
const root = scratchDir("chant-runner-start-");
|
|
35
|
+
git(root, "init", "-q", "-b", "main");
|
|
36
|
+
git(root, "config", "user.name", "t");
|
|
37
|
+
git(root, "config", "user.email", "t@example.com");
|
|
38
|
+
git(root, "config", "commit.gpgsign", "false");
|
|
39
|
+
const trust = (runners: Array<{ principal: string; ssh: string }>) =>
|
|
40
|
+
JSON.stringify({ schema: 1, runners: runners.map((r) => ({ principal: r.principal, class: "runner", key: r.ssh })), roles: { runner: runners.map((r) => r.principal) } });
|
|
41
|
+
writeFiles(root, { ".chant/trust.json": trust([PROVISIONER]) });
|
|
42
|
+
git(root, "add", "-A");
|
|
43
|
+
git(root, "commit", "-q", "-m", "trust");
|
|
44
|
+
const base = git(root, "rev-parse", "HEAD");
|
|
45
|
+
writeFiles(root, { ".chant/trust.json": trust([PROVISIONER, LATER]) });
|
|
46
|
+
git(root, "add", "-A");
|
|
47
|
+
git(root, "commit", "-q", "-m", "a pull request adds its own provisioner");
|
|
48
|
+
return { root, base, head: git(root, "rev-parse", "HEAD") };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const R = repo();
|
|
52
|
+
|
|
53
|
+
const job = (commit: string): CiJobIdentity => ({ system: "github", repo: "acme/infra", run: "4242", attempt: "1", job: "apply", commit, runner: "jit-1" });
|
|
54
|
+
const env = (commit: string, statement?: unknown): Record<string, string> => ({
|
|
55
|
+
GITHUB_ACTIONS: "true",
|
|
56
|
+
GITHUB_REPOSITORY: "acme/infra",
|
|
57
|
+
GITHUB_RUN_ID: "4242",
|
|
58
|
+
GITHUB_RUN_ATTEMPT: "1",
|
|
59
|
+
GITHUB_JOB: "apply",
|
|
60
|
+
GITHUB_SHA: commit,
|
|
61
|
+
RUNNER_NAME: "jit-1",
|
|
62
|
+
CHANT_RUNNER_START_FILE: "/nonexistent/runner-start.json",
|
|
63
|
+
...(statement ? { CHANT_RUNNER_START: JSON.stringify(statement) } : {}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("checkRunnerStart reads the runner keys at base", () => {
|
|
67
|
+
test("a statement by a provisioner listed at base verifies", () => {
|
|
68
|
+
const v = checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, signRunnerStart(job(R.head), PROVISIONER.signer)) });
|
|
69
|
+
expect(v).toMatchObject({ status: "verified", signer: "provisioner" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a provisioner the change itself lists does not count at the base before it", () => {
|
|
73
|
+
const statement = signRunnerStart(job(R.head), LATER.signer);
|
|
74
|
+
expect(checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, statement) }).status).toBe("untrusted");
|
|
75
|
+
expect(checkRunnerStart({ top: R.root, base: R.head, env: env(R.head, statement) }).status).toBe("verified");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("the job's commit stands in for the forge's variable, so a statement for another commit is refused", () => {
|
|
79
|
+
const statement = signRunnerStart(job(R.head), PROVISIONER.signer);
|
|
80
|
+
expect(checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, statement), commit: R.base }).status).toBe("mismatch");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("requireRunnerStart throws with the reason, and a missing statement is named", () => {
|
|
84
|
+
expect(() => requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: env(R.head) })).toThrow(/--require-runner-start: missing: .*has no runner-start statement/);
|
|
85
|
+
expect(() => requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: {} })).toThrow(/needs a forge job to check/);
|
|
86
|
+
expect(requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: env(R.head, signRunnerStart(job(R.head), PROVISIONER.signer)) }).signer).toBe("provisioner");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("runnerStartReadiness, for the isolated-runners group", () => {
|
|
91
|
+
test("a policy with a runner key whose principal holds the runner role is configured", () => {
|
|
92
|
+
expect(runnerStartReadinessAt(R.root, R.base)).toEqual({ configured: true, gaps: [] });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("no runner key, a key with no runner role, and an unreadable policy each name their gap", () => {
|
|
96
|
+
expect(runnerStartReadiness(emptyPolicy("x")).gaps.map((g) => g.gap)).toEqual(["no-runner-key"]);
|
|
97
|
+
const noRole = { ...emptyPolicy("x"), runners: [{ principal: "p", class: "runner" as const, key: PROVISIONER.ssh }] };
|
|
98
|
+
expect(runnerStartReadiness(noRole)).toMatchObject({ configured: false, gaps: [{ gap: "no-runner-role" }] });
|
|
99
|
+
expect(runnerStartReadiness(emptyPolicy("x", ["bad json"])).gaps.map((g) => g.gap)).toEqual(["trust-unreadable"]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("chant ci runner-start sign and verify", () => {
|
|
104
|
+
test("--job takes JSON or @file, and refuses fields chant does not know", () => {
|
|
105
|
+
const dir = scratchDir("chant-runner-start-job-");
|
|
106
|
+
writeFiles(dir, { "job.json": JSON.stringify(job(R.head)) });
|
|
107
|
+
expect(parseJobSpec(`@${join(dir, "job.json")}`)).toEqual(job(R.head));
|
|
108
|
+
expect(parseJobSpec('{"system":"gitlab","repo":"g/s/p","run":9,"job":77,"commit":"x"}')).toEqual({ system: "gitlab", repo: "g/s/p", run: "9", attempt: "1", job: "77", commit: "x" });
|
|
109
|
+
expect(() => parseJobSpec('{"system":"github","pr":7}')).toThrow(/fields chant does not know: pr/);
|
|
110
|
+
expect(parseArgs(["ci", "runner-start", "sign", "--key", "k.pem", "--principal", "provisioner", "--job", "@job.json"])).toMatchObject({ path: "runner-start", extraPositional: "sign", key: "k.pem", ciPrincipal: "provisioner", ciJob: "@job.json" });
|
|
111
|
+
expect(parseArgs(["ci", "run", "apply", "--env", "prod", "--require-runner-start"])).toMatchObject({ requireRunnerStart: true });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("sign writes an envelope the job then verifies, and verify exits 1 for another attempt", async () => {
|
|
115
|
+
const dir = scratchDir("chant-runner-start-cli-");
|
|
116
|
+
writeFiles(dir, { "key.pem": PROVISIONER.pem, "job.json": JSON.stringify(job(R.head)) });
|
|
117
|
+
const out = join(dir, "runner-start.json");
|
|
118
|
+
const ctx = (argv: string[]) => ({ args: parseArgs(argv) }) as unknown as CommandContext;
|
|
119
|
+
const quiet = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
120
|
+
try {
|
|
121
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "sign", "--key", join(dir, "key.pem"), "--principal", "provisioner", "--job", `@${join(dir, "job.json")}`, "-o", out]))).toBe(0);
|
|
122
|
+
const statement = JSON.parse(readFileSync(out, "utf8"));
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
process.chdir(R.root);
|
|
125
|
+
try {
|
|
126
|
+
vi.stubEnv("GITHUB_ACTIONS", "true");
|
|
127
|
+
for (const [k, v] of Object.entries(env(R.head))) vi.stubEnv(k, v);
|
|
128
|
+
vi.stubEnv("CHANT_RUNNER_START_FILE", out);
|
|
129
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "verify", "--base", R.base]))).toBe(0);
|
|
130
|
+
vi.stubEnv("GITHUB_RUN_ATTEMPT", "2");
|
|
131
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "verify", "--base", R.base]))).toBe(1);
|
|
132
|
+
} finally {
|
|
133
|
+
vi.unstubAllEnvs();
|
|
134
|
+
process.chdir(cwd);
|
|
135
|
+
}
|
|
136
|
+
expect(statement.payloadType).toBe("application/vnd.in-toto+json");
|
|
137
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "nope"]))).toBe(1);
|
|
138
|
+
} finally {
|
|
139
|
+
quiet.mockRestore();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("ci run --require-runner-start", () => {
|
|
145
|
+
test("a job with no statement is refused before anything runs, and --local is refused with it", async () => {
|
|
146
|
+
const run = (extra: Partial<Parameters<typeof ciRun>[0]>) => ciRun({ cwd: R.root, job: "apply", env: "prod", forge: "github", base: R.base, hostEnv: env(R.head), push: false, requireRunnerStart: true, ...extra });
|
|
147
|
+
await expect(run({})).rejects.toThrow(CiRunError);
|
|
148
|
+
await expect(run({})).rejects.toThrow(/has no runner-start statement/);
|
|
149
|
+
await expect(run({ local: true, hostEnv: {} })).rejects.toThrow(/--local runs the job on this machine/);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant ci runner-start sign|verify` and `chant ci run --require-runner-start`
|
|
3
|
+
* (#4147): the runner-start statement (../runner-start.ts) checked against
|
|
4
|
+
* `.chant/trust.json` at base.
|
|
5
|
+
*
|
|
6
|
+
* - `sign` is for the provisioner that starts a runner for one job: it signs
|
|
7
|
+
* the job's ids with a runner key the job never holds and prints the
|
|
8
|
+
* envelope, which the provisioner puts where the job looks for it.
|
|
9
|
+
* - `verify` is for the job: it names itself from the forge's variables,
|
|
10
|
+
* reads the statement, and fails unless a runner key `.chant/trust.json`
|
|
11
|
+
* at base lists, whose principal holds the `runner` role, signed it for
|
|
12
|
+
* this job.
|
|
13
|
+
* - `ci run --require-runner-start` makes the same check before the job's
|
|
14
|
+
* first step, so an apply refuses before it trades for any credentials.
|
|
15
|
+
*
|
|
16
|
+
* Requiring the statement is opt-in (#4156). With nothing asking for it,
|
|
17
|
+
* nothing reads it, and a runner with none never refuses, warns or hides
|
|
18
|
+
* anything.
|
|
19
|
+
*
|
|
20
|
+
* {@link runnerStartReadiness} says whether a repository could require it
|
|
21
|
+
* at base, and what is missing, for the isolated-runners security group
|
|
22
|
+
* (#4168, section 8), whose WSP165 check reports these gaps.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { execFileSync } from "node:child_process";
|
|
26
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { resolve } from "node:path";
|
|
28
|
+
import type { CommandContext } from "../cli/registry";
|
|
29
|
+
import { formatError } from "../cli/format";
|
|
30
|
+
import {
|
|
31
|
+
ciJobFromEnv,
|
|
32
|
+
RUNNER_START_SIGNER_ROLE,
|
|
33
|
+
signRunnerStart,
|
|
34
|
+
verifyRunnerStartForJob,
|
|
35
|
+
type CiJobIdentity,
|
|
36
|
+
type CiSystem,
|
|
37
|
+
type RunnerStartVerdict,
|
|
38
|
+
} from "../runner-start";
|
|
39
|
+
import { gitRevisionSource } from "./record-source";
|
|
40
|
+
import { gitTop } from "./tree";
|
|
41
|
+
import { loadRunnerKey } from "./trust/dsse";
|
|
42
|
+
import { emptyPolicy, readTrustPolicy, type TrustPolicy } from "./trust/policy";
|
|
43
|
+
|
|
44
|
+
/** The policy at `base` in the repository around `cwd`. */
|
|
45
|
+
function policyAt(top: string, base: string): TrustPolicy {
|
|
46
|
+
return readTrustPolicy(gitRevisionSource(top, base), base);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check this job's runner-start statement against `.chant/trust.json` at
|
|
51
|
+
* `base`. `commit` is the commit the job runs, in place of the forge's
|
|
52
|
+
* variable. Never throws.
|
|
53
|
+
*/
|
|
54
|
+
export function checkRunnerStart(input: { top: string; base: string; env?: Record<string, string | undefined>; commit?: string; readFile?: (path: string) => string }): RunnerStartVerdict {
|
|
55
|
+
const policy = policyAt(input.top, input.base);
|
|
56
|
+
if (policy.problems.length > 0) return { status: "untrusted", reason: `.chant/trust.json at ${input.base.slice(0, 12)} cannot be read: ${policy.problems.join("; ")}` };
|
|
57
|
+
return verifyRunnerStartForJob({
|
|
58
|
+
runners: policy.runners,
|
|
59
|
+
roles: policy.roles,
|
|
60
|
+
...(input.env ? { env: input.env } : {}),
|
|
61
|
+
...(input.commit ? { commit: input.commit } : {}),
|
|
62
|
+
...(input.readFile ? { readFile: input.readFile } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One thing a repository lacks before it can require runner-start statements. */
|
|
67
|
+
export interface RunnerStartGap {
|
|
68
|
+
/** Not a read-contract reason code: the isolated-runners group (#4168) maps each to its own security code. */
|
|
69
|
+
gap: "no-runner-key" | "no-runner-role" | "trust-unreadable";
|
|
70
|
+
reason: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether `.chant/trust.json` in `policy` lets a job require a runner-start
|
|
75
|
+
* statement: a key under `runners` whose principal holds the `runner` role.
|
|
76
|
+
* `configured` is true when there are no gaps. For the isolated-runners
|
|
77
|
+
* security group (#4168); nothing here refuses or warns.
|
|
78
|
+
*/
|
|
79
|
+
export function runnerStartReadiness(policy: TrustPolicy): { configured: boolean; gaps: RunnerStartGap[] } {
|
|
80
|
+
const gaps: RunnerStartGap[] = [];
|
|
81
|
+
if (policy.problems.length > 0) {
|
|
82
|
+
gaps.push({ gap: "trust-unreadable", reason: `.chant/trust.json cannot be read: ${policy.problems.join("; ")}` });
|
|
83
|
+
return { configured: false, gaps };
|
|
84
|
+
}
|
|
85
|
+
if (policy.runners.length === 0) {
|
|
86
|
+
gaps.push({ gap: "no-runner-key", reason: ".chant/trust.json lists no key under runners for a provisioner to sign with: chant workspace trust runner add <principal> --key <ssh-ed25519 line>" });
|
|
87
|
+
} else {
|
|
88
|
+
const holders = new Set((policy.roles[RUNNER_START_SIGNER_ROLE] ?? []).map((p) => p.normalize("NFKC").trim().toLowerCase()));
|
|
89
|
+
if (!policy.runners.some((r) => holders.has(r.principal.normalize("NFKC").trim().toLowerCase()))) {
|
|
90
|
+
gaps.push({ gap: "no-runner-role", reason: `no principal under runners holds the ${RUNNER_START_SIGNER_ROLE} role in .chant/trust.json: "roles": { "${RUNNER_START_SIGNER_ROLE}": ["<principal>"] }` });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { configured: gaps.length === 0, gaps };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** {@link runnerStartReadiness} for the repository around `cwd` at `base` (default `HEAD`). */
|
|
97
|
+
export function runnerStartReadinessAt(cwd: string, base = "HEAD"): { configured: boolean; gaps: RunnerStartGap[] } {
|
|
98
|
+
const top = gitTop(cwd);
|
|
99
|
+
if (!top) return runnerStartReadiness(emptyPolicy(null, [`${cwd} is not in a git repository`]));
|
|
100
|
+
const commit = revParse(top, base);
|
|
101
|
+
return runnerStartReadiness(commit ? policyAt(top, commit) : emptyPolicy(null, [`${base} is not a commit`]));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function revParse(top: string, rev: string): string | undefined {
|
|
105
|
+
try {
|
|
106
|
+
return execFileSync("git", ["rev-parse", "--verify", "-q", `${rev}^{commit}`], { cwd: top, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The base `verify` reads trust at: `--base`, else the remote's default branch, else `HEAD`. */
|
|
113
|
+
function verifyBase(top: string, base?: string): string {
|
|
114
|
+
const rev = base ?? (revParse(top, "refs/remotes/origin/HEAD") ? "refs/remotes/origin/HEAD" : "HEAD");
|
|
115
|
+
const commit = revParse(top, rev);
|
|
116
|
+
if (!commit) throw new Error(`--base ${rev} is not a commit in this repository`);
|
|
117
|
+
return commit;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The job named by `--job`: JSON, or `@<file>` holding it. */
|
|
121
|
+
export function parseJobSpec(spec: string): CiJobIdentity {
|
|
122
|
+
const text = spec.startsWith("@") ? readFileSync(resolve(spec.slice(1)), "utf8") : spec;
|
|
123
|
+
let raw: unknown;
|
|
124
|
+
try {
|
|
125
|
+
raw = JSON.parse(text);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error("--job takes the job as JSON, or @<file> holding it: {\"system\":\"github\",\"repo\":\"owner/name\",\"run\":\"<id>\",\"attempt\":\"1\",\"job\":\"apply\",\"commit\":\"<sha>\",\"runner\":\"<name>\"}");
|
|
128
|
+
}
|
|
129
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("--job takes a JSON object");
|
|
130
|
+
const o = raw as Record<string, unknown>;
|
|
131
|
+
const str = (k: string): string | undefined => (o[k] === undefined ? undefined : String(o[k]));
|
|
132
|
+
const known = ["system", "repo", "run", "attempt", "job", "commit", "runner"];
|
|
133
|
+
const extra = Object.keys(o).filter((k) => !known.includes(k));
|
|
134
|
+
if (extra.length > 0) throw new Error(`--job has fields chant does not know: ${extra.join(", ")}`);
|
|
135
|
+
return {
|
|
136
|
+
system: (str("system") ?? "") as CiSystem,
|
|
137
|
+
repo: str("repo") ?? "",
|
|
138
|
+
run: str("run") ?? "",
|
|
139
|
+
attempt: str("attempt") ?? "1",
|
|
140
|
+
job: str("job") ?? "",
|
|
141
|
+
commit: str("commit") ?? "",
|
|
142
|
+
...(str("runner") ? { runner: str("runner")! } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const USAGE = "chant ci runner-start sign --key <runner key.pem> --principal <name> --job <json|@file> [-o <file>]\n chant ci runner-start verify [--base <rev>] [--commit <sha>] [--json]";
|
|
147
|
+
|
|
148
|
+
/** `chant ci runner-start sign|verify`. */
|
|
149
|
+
export async function runCiRunnerStart(ctx: CommandContext): Promise<number> {
|
|
150
|
+
const { args } = ctx;
|
|
151
|
+
const action = args.extraPositional;
|
|
152
|
+
try {
|
|
153
|
+
if (action === "sign") {
|
|
154
|
+
if (!args.key) throw new Error("--key names the provisioner's runner key, an Ed25519 private key in PEM, which the job never holds");
|
|
155
|
+
if (!args.ciPrincipal) throw new Error("--principal names the principal .chant/trust.json lists the key for under runners");
|
|
156
|
+
if (!args.ciJob) throw new Error("--job names the job the runner was started for, as JSON or @<file>");
|
|
157
|
+
const job = parseJobSpec(args.ciJob);
|
|
158
|
+
const pem = readFileSync(resolve(args.key), "utf8");
|
|
159
|
+
const loaded = loadRunnerKey(pem);
|
|
160
|
+
const envelope = signRunnerStart(job, { principal: args.ciPrincipal, key: loaded.key, publicKey: loaded.publicKey });
|
|
161
|
+
const text = `${JSON.stringify(envelope)}\n`;
|
|
162
|
+
if (args.output) writeFileSync(resolve(args.output), text, { mode: 0o644 });
|
|
163
|
+
else process.stdout.write(text);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (action === "verify") {
|
|
167
|
+
const top = gitTop(process.cwd());
|
|
168
|
+
if (!top) throw new Error("verify reads .chant/trust.json at a commit, and this directory is not in a git repository");
|
|
169
|
+
const base = verifyBase(top, args.base);
|
|
170
|
+
const verdict = checkRunnerStart({ top, base, ...(args.commit ? { commit: args.commit } : {}) });
|
|
171
|
+
if (args.json) process.stdout.write(`${JSON.stringify({ ...verdict, base })}\n`);
|
|
172
|
+
else console.error(`ci runner-start: ${verdict.status}: ${verdict.reason}`);
|
|
173
|
+
return verdict.status === "verified" ? 0 : 1;
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`ci runner-start takes sign or verify${action ? `, not ${JSON.stringify(action)}` : ""}`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(formatError({ message: `chant ci runner-start: ${err instanceof Error ? err.message : String(err)}`, hint: USAGE }));
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The check `ci run --require-runner-start` makes before the job's first
|
|
184
|
+
* step: the statement for this job, verified against `.chant/trust.json` at
|
|
185
|
+
* `base`. Throws with the verdict's reason unless it verified.
|
|
186
|
+
*/
|
|
187
|
+
export function requireRunnerStart(input: { top: string; base: string; commit: string; env: Record<string, string | undefined> }): Extract<RunnerStartVerdict, { status: "verified" }> {
|
|
188
|
+
const named = ciJobFromEnv(input.env);
|
|
189
|
+
if (!named.ok) throw new Error(`--require-runner-start needs a forge job to check: ${named.reason}`);
|
|
190
|
+
const verdict = checkRunnerStart({ top: input.top, base: input.base, env: input.env, commit: input.commit });
|
|
191
|
+
if (verdict.status !== "verified") throw new Error(`--require-runner-start: ${verdict.status}: ${verdict.reason}`);
|
|
192
|
+
return verdict;
|
|
193
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4169 — `ci serve --build-command` serves each `ci/build/<sha>` tag once,
|
|
3
|
+
* within `maxJobs`, and records how each build ended.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
9
|
+
import { cleanScratch, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
10
|
+
import { buildRequestAnnotation, requestedBuild } from "./ci-builds";
|
|
11
|
+
import { ciServeBuilds, ciServePass, formatCiServeEvent, newCiServeRuntime, type CiServeEvent, type CiServeOptions } from "./ci-serve";
|
|
12
|
+
import type { CiGreen } from "./declaration";
|
|
13
|
+
|
|
14
|
+
afterAll(cleanScratch);
|
|
15
|
+
|
|
16
|
+
const GREEN = { branch: "main", phases: [], require: ["suites"] } as unknown as CiGreen;
|
|
17
|
+
|
|
18
|
+
function clone(): { root: string; shas: string[] } {
|
|
19
|
+
const origin = scratchDir("chant-serve-builds-origin-");
|
|
20
|
+
git(origin, "init", "-q", "--bare");
|
|
21
|
+
const root = scratchDir("chant-serve-builds-");
|
|
22
|
+
git(root, "init", "-q");
|
|
23
|
+
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
24
|
+
git(root, "config", "user.name", "t");
|
|
25
|
+
git(root, "config", "user.email", "t@example.com");
|
|
26
|
+
git(root, "remote", "add", "origin", origin);
|
|
27
|
+
const shas: string[] = [];
|
|
28
|
+
for (const n of ["a", "b"]) {
|
|
29
|
+
writeFiles(root, { [`${n}.txt`]: `${n}\n` });
|
|
30
|
+
git(root, "add", "-A");
|
|
31
|
+
git(root, "commit", "-q", "-m", n);
|
|
32
|
+
shas.push(git(root, "rev-parse", "HEAD"));
|
|
33
|
+
const r = requestedBuild(GREEN, shas.at(-1)!, ["suites"], new Date(`2026-10-20T1${shas.length}:00:00Z`));
|
|
34
|
+
git(root, "tag", "-a", "-m", buildRequestAnnotation(r), r.tag, r.commit);
|
|
35
|
+
}
|
|
36
|
+
git(root, "push", "-q", "origin", "main", "--tags");
|
|
37
|
+
// The serve's clone learns the tags from the forge.
|
|
38
|
+
for (const s of shas) git(root, "tag", "-d", `ci/build/${s}`);
|
|
39
|
+
return { root, shas };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const options = (root: string, built: string[], log: CiServeEvent[], extra: Partial<CiServeOptions> = {}): CiServeOptions => ({
|
|
43
|
+
cwd: root,
|
|
44
|
+
forge: "github",
|
|
45
|
+
repo: "acme/app",
|
|
46
|
+
env: "prod",
|
|
47
|
+
client: { openPullRequests: async () => [], branchHead: async () => undefined },
|
|
48
|
+
stateDir: join(root, ".serve"),
|
|
49
|
+
buildCommand: "true",
|
|
50
|
+
build: async ({ request }) => (built.push(request.commit), request.commit === built[0] ? 0 : 2),
|
|
51
|
+
log: (e) => log.push(e),
|
|
52
|
+
...extra,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("ci serve --build-command (#4169)", () => {
|
|
56
|
+
test("serves each request once, oldest first, within maxJobs, and records how it ended", async () => {
|
|
57
|
+
const { root, shas } = clone();
|
|
58
|
+
const built: string[] = [];
|
|
59
|
+
const log: CiServeEvent[] = [];
|
|
60
|
+
const rt = newCiServeRuntime();
|
|
61
|
+
const first = ciServeBuilds(options(root, built, log), rt);
|
|
62
|
+
expect(first.events.map((e) => e.type === "build" && e.status)).toEqual(["started", "held"]);
|
|
63
|
+
await Promise.all(first.started);
|
|
64
|
+
const second = ciServeBuilds(options(root, built, log), rt);
|
|
65
|
+
await Promise.all(second.started);
|
|
66
|
+
expect(built).toEqual(shas);
|
|
67
|
+
const state = JSON.parse(readFileSync(join(root, ".serve", "chant-apply-prod.json"), "utf-8"));
|
|
68
|
+
expect(Object.fromEntries(Object.entries(state.builds).map(([k, v]) => [k, (v as { status: string }).status]))).toEqual({ [`build:${shas[0]}`]: "ok", [`build:${shas[1]}`]: "fail" });
|
|
69
|
+
// Served once: another pass starts nothing.
|
|
70
|
+
expect(ciServeBuilds(options(root, built, log), rt).started).toEqual([]);
|
|
71
|
+
expect(log.map((e) => formatCiServeEvent(e))).toContain(`the build of suites at ${shas[1].slice(0, 12)} failed with exit code 2`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("without --build-command a pass leaves the tags alone; the default build runs the command in a worktree of the commit", async () => {
|
|
75
|
+
const { root, shas } = clone();
|
|
76
|
+
const log: CiServeEvent[] = [];
|
|
77
|
+
await ciServePass({ ...options(root, [], log), buildCommand: undefined });
|
|
78
|
+
expect(log.filter((e) => e.type === "build")).toEqual([]);
|
|
79
|
+
const out = join(root, ".serve-out");
|
|
80
|
+
await ciServePass({ ...options(root, [], log), build: undefined, buildCommand: `git rev-parse HEAD >> ${out}; echo "$CHANT_BUILD_PHASES $CHANT_BUILD_REASON" >> ${out}`, maxJobs: 2 });
|
|
81
|
+
expect(existsSync(out)).toBe(true);
|
|
82
|
+
const lines = readFileSync(out, "utf-8").trim().split("\n");
|
|
83
|
+
expect(lines.filter((l) => /^[0-9a-f]{40}$/.test(l)).sort()).toEqual([...shas].sort());
|
|
84
|
+
expect(lines.filter((l) => l === "suites requested")).toHaveLength(2);
|
|
85
|
+
});
|
|
86
|
+
});
|