@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
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* tag the commits that turned green, revoke the ones that turned red
|
|
9
9
|
* ci workflow [--workflow <name>]... [--output <file>] [--chant <command>] [--install <command>]
|
|
10
10
|
* [--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>] [--runner-key-secret <NAME>]
|
|
11
|
-
* write the root CI file that runs the tick
|
|
11
|
+
* write the root CI file that runs the tick, and the triggers and
|
|
12
|
+
* concurrency of the workflows holding a phase that sets build (#4169)
|
|
12
13
|
*
|
|
13
14
|
* The tick signs its tags with a runner key (#4031) from `--key <file>` or
|
|
14
15
|
* the PEM in `CHANT_RUNNER_KEY`, which the generated workflow sets from the
|
|
@@ -30,7 +31,7 @@ import type { CommandContext } from "../cli/registry";
|
|
|
30
31
|
import { findWorkspaceRoot } from "../project-root";
|
|
31
32
|
import { ciList, ciTick, CiGreenError, formatList, formatTick, lastGreen, RUNNER_KEY_ENV } from "./ci-green";
|
|
32
33
|
import { ciForgeFromEnv } from "./ci-green-forge";
|
|
33
|
-
import { ACTIONS_NAME, CI_GREEN_WORKFLOW_FILE, ciGreenHeader, type PushToken, readWorkflowSources, renderCiGreenWorkflow, workflowsHolding, writeWorkflowFile } from "./ci-green-workflow";
|
|
34
|
+
import { ACTIONS_NAME, buildTriggerEdits, CI_GREEN_WORKFLOW_FILE, ciGreenHeader, type PushToken, readWorkflowSources, renderCiGreenWorkflow, workflowsHolding, writeWorkflowFile } from "./ci-green-workflow";
|
|
34
35
|
import { readDeclaration, readerVersion, WorkspaceReadError, type CiSource } from "./declaration";
|
|
35
36
|
import { recordGeneratedFiles, resolveMemberContext, shellArg } from "./member-pipeline";
|
|
36
37
|
import { gitTop, workingTree } from "./tree";
|
|
@@ -105,6 +106,13 @@ export async function runCiTick(ctx: CommandContext): Promise<number> {
|
|
|
105
106
|
// The forge is made only when the source is check runs, so a tick from chant runs needs no forge token.
|
|
106
107
|
const forge = () => ciForgeFromEnv(ctx.args.forge ?? "github");
|
|
107
108
|
const runnerKey = ctx.args.key !== undefined ? readFileSync(resolve(ctx.args.key), "utf-8") : process.env[RUNNER_KEY_ENV] || undefined;
|
|
109
|
+
// #4168: under who-ran-it every tag the tick makes is signed, so a tick without a key makes none.
|
|
110
|
+
if (runnerKey === undefined && ctx.args.dryRun !== true) {
|
|
111
|
+
const { securityGroupsAt } = await import("./security");
|
|
112
|
+
if (securityGroupsAt(process.cwd()).has("who-ran-it")) {
|
|
113
|
+
throw new Error(`the workspace opts into who-ran-it, so the tick signs every tag it makes and needs a runner key; pass --key <runner key.pem> or set ${RUNNER_KEY_ENV}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
108
116
|
const result = await ciTick({ cwd: process.cwd(), forge, dryRun: ctx.args.dryRun === true, ...(source ? { source } : {}), ...(runnerKey !== undefined ? { runnerKey } : {}) });
|
|
109
117
|
console.log(formatTick(result, ctx.args.dryRun === true));
|
|
110
118
|
return 0;
|
|
@@ -143,7 +151,10 @@ export async function runCiWorkflow(ctx: CommandContext): Promise<number> {
|
|
|
143
151
|
const target = ctx.args.output ? resolve(realpathSync(cwd), ctx.args.output) : join(repoRoot, CI_GREEN_WORKFLOW_FILE);
|
|
144
152
|
const fromRepo = relative(repoRoot, target).split(sep).join("/");
|
|
145
153
|
|
|
146
|
-
const
|
|
154
|
+
const sources = readWorkflowSources(repoRoot, fromRepo);
|
|
155
|
+
const scan = workflowsHolding(green, sources);
|
|
156
|
+
// #4169: the triggers and concurrency of the workflows holding a phase that sets build. Refused before anything is written.
|
|
157
|
+
const edits = buildTriggerEdits(green, sources);
|
|
147
158
|
const extra = ctx.args.ciWorkflows ?? [];
|
|
148
159
|
const workflows = [...new Set([...scan.workflows, ...extra])].sort();
|
|
149
160
|
for (const f of scan.unreadable) console.error(`warning: ${f} is not valid YAML, so its jobs were not read`);
|
|
@@ -161,8 +172,9 @@ export async function runCiWorkflow(ctx: CommandContext): Promise<number> {
|
|
|
161
172
|
if (push && "appIdVar" in push) parts.push("--app-id-var", push.appIdVar, "--app-key-secret", push.appKeySecret);
|
|
162
173
|
const runnerKeySecret = ctx.args.ciRunnerKeySecret;
|
|
163
174
|
if (runnerKeySecret) parts.push("--runner-key-secret", runnerKeySecret);
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
const { securityGroupsAt } = await import("./security");
|
|
176
|
+
if ((green.signedSince || securityGroupsAt(root).has("who-ran-it")) && !runnerKeySecret) {
|
|
177
|
+
console.error(`warning: ci.green.signedSince is set or the workspace opts into who-ran-it, so the tick needs a runner key; pass --runner-key-secret <NAME> naming the repository secret that holds it, or every tick will fail`);
|
|
166
178
|
}
|
|
167
179
|
const command = parts.map(shellArg).join(" ");
|
|
168
180
|
const workspaceRoot = relative(repoRoot, root).split(sep).join("/") || ".";
|
|
@@ -172,6 +184,15 @@ export async function runCiWorkflow(ctx: CommandContext): Promise<number> {
|
|
|
172
184
|
const written = writeWorkflowFile(target, ciGreenHeader(command, member ? "member" : "root") + yaml);
|
|
173
185
|
if (member) recordGeneratedFiles(member.memberRoot, [{ path: fromRepo, command }]);
|
|
174
186
|
console.log(`Wrote ${written}: ticks when ${workflows.join(", ")} complete${workflows.length === 1 ? "s" : ""} on ${green.branch}, and every 15 minutes.`);
|
|
187
|
+
for (const e of edits) {
|
|
188
|
+
if (!e.changed) continue;
|
|
189
|
+
const path = writeWorkflowFile(join(repoRoot, e.path), e.text);
|
|
190
|
+
console.log(
|
|
191
|
+
e.build === "newest"
|
|
192
|
+
? `Wrote ${path}'s triggers and concurrency: ${e.phases.join(", ")} build${e.phases.length === 1 ? "s" : ""} the newest commit on ${green.branch} when a run ends, and a ci/build/<sha> tag by hand.`
|
|
193
|
+
: `Wrote ${path}'s concurrency: ${e.phases.join(", ")} build${e.phases.length === 1 ? "s" : ""} every commit on ${green.branch}, none cancelled.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
175
196
|
return 0;
|
|
176
197
|
} catch (err) {
|
|
177
198
|
console.error(formatError({ message: `chant ci workflow: ${describe(err)}`, hint: USAGE }));
|
|
@@ -32,8 +32,8 @@ export interface CheckRun {
|
|
|
32
32
|
url: string | null;
|
|
33
33
|
/**
|
|
34
34
|
* Set when the "check run" is a chant Op run (#4044, `ci.green.source:
|
|
35
|
-
* "runs"`): the run's id and env, the runner whose statement it
|
|
36
|
-
*
|
|
35
|
+
* "runs"`): the run's id and env, and the runner whose statement it
|
|
36
|
+
* carries with that statement, or a null signer for an unsigned run. `name` is then the Op's name and `id` the run's end
|
|
37
37
|
* time in milliseconds, which orders a re-run after the run it replaced.
|
|
38
38
|
*/
|
|
39
39
|
opRun?: OpRunRef;
|
|
@@ -45,14 +45,28 @@ export interface CheckRun {
|
|
|
45
45
|
export interface OpRunRef {
|
|
46
46
|
id: string;
|
|
47
47
|
env: string;
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/** The runner whose statement the run carries, or null for an unsigned run, which counts unless `ci.green.signedSince` asks for one (#4156). */
|
|
49
|
+
signer: string | null;
|
|
50
|
+
/** The run's statement, when it has one. */
|
|
51
|
+
statement?: NonNullable<OpRunRecord["statement"]>;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface CiForge {
|
|
53
55
|
kind: ForgeKind;
|
|
54
56
|
/** Every check run on `sha`, every attempt. */
|
|
55
57
|
checkRuns(sha: string): Promise<CheckRun[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Run a failed check run's job again (#4169), so a flake costs one more
|
|
60
|
+
* run before the commit counts red. Absent on a forge chant can't ask.
|
|
61
|
+
*/
|
|
62
|
+
rerun?(run: CheckRun): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Start a workflow at a ref (#4169), for a build the tick asks for: a
|
|
65
|
+
* bisect step, or `chant ci run --at`. `workflow` is the file's name in
|
|
66
|
+
* `.github/workflows/`, and `ref` a branch or tag. Absent on a forge chant
|
|
67
|
+
* can't ask.
|
|
68
|
+
*/
|
|
69
|
+
dispatch?(workflow: string, ref: string): Promise<void>;
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
export interface GithubCiForgeOptions {
|
|
@@ -101,7 +115,19 @@ export function githubCiForge(opts: GithubCiForgeOptions): CiForge {
|
|
|
101
115
|
}
|
|
102
116
|
return out;
|
|
103
117
|
},
|
|
118
|
+
// An Actions job's check run has the job's id, so the job is re-run by it. Both need `actions: write`.
|
|
119
|
+
async rerun(run) {
|
|
120
|
+
await post(`${root}/repos/${opts.repo}/actions/jobs/${run.id}/rerun`, {});
|
|
121
|
+
},
|
|
122
|
+
async dispatch(workflow, ref) {
|
|
123
|
+
await post(`${root}/repos/${opts.repo}/actions/workflows/${encodeURIComponent(workflow)}/dispatches`, { ref });
|
|
124
|
+
},
|
|
104
125
|
};
|
|
126
|
+
|
|
127
|
+
async function post(url: string, body: unknown): Promise<void> {
|
|
128
|
+
const res = await fetchImpl(url, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
129
|
+
if (!res.ok) throw new ForgeApiError("POST", url, res.status, await res.text());
|
|
130
|
+
}
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
/** Thrown for a forge `chant ci tick` has no check-run client for yet. */
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #4044: chant run results as a status source for `ci tick`. With
|
|
3
3
|
* `ci.green.source: "runs"` (or `--source runs`) the tick reads Op runs from
|
|
4
|
-
* the run ledger on `chant/lifecycle` instead of a forge's check runs.
|
|
5
|
-
*
|
|
6
|
-
* `.chant/trust.json` lists for a principal
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* the run ledger on `chant/lifecycle` instead of a forge's check runs.
|
|
5
|
+
* Signing is opt-in (#4156): an unsigned run counts with no signer, and a run
|
|
6
|
+
* whose statement a runner key `.chant/trust.json` lists for a principal
|
|
7
|
+
* holding the `runner` role signed counts with that signer. A statement that
|
|
8
|
+
* does not verify is ignored and reported. With `ci.green.signedSince` an
|
|
9
|
+
* unsigned run on that commit or after it is ignored too. A tick given a
|
|
10
|
+
* runner key signs its tags, and every tag names the runs behind it.
|
|
9
11
|
*
|
|
10
12
|
* Each test works in its own temp repository with a bare origin, and every
|
|
11
13
|
* key is a throwaway one. Nothing is written in process.cwd().
|
|
@@ -149,19 +151,61 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
149
151
|
expect((await ciTick({ cwd: root, now: NOW, runnerKey: BOT.pem })).made).toEqual([]);
|
|
150
152
|
});
|
|
151
153
|
|
|
152
|
-
test("an unsigned run
|
|
154
|
+
test("an unsigned run counts with no signer: with no key anywhere the tick tags the commit and ci list shows it (#4156)", async () => {
|
|
153
155
|
const { root } = workspace();
|
|
154
156
|
const a = commitAt(root, 3, "app/a.txt");
|
|
157
|
+
const lint = await runOp(root, "lint");
|
|
158
|
+
await runOp(root, "test");
|
|
159
|
+
expect(lint.statement).toBeUndefined();
|
|
160
|
+
push(root);
|
|
161
|
+
|
|
162
|
+
const list = (await ciList({ cwd: root, now: NOW })) as Extract<Awaited<ReturnType<typeof ciList>>, { commits: unknown }>;
|
|
163
|
+
readList.expectValid(list);
|
|
164
|
+
const listed = list.commits.find((c) => c.sha === a)!;
|
|
165
|
+
expect(listed.phases.map((p) => [p.phase, p.result])).toEqual([
|
|
166
|
+
["lint", "pass"],
|
|
167
|
+
["test", "pass"],
|
|
168
|
+
]);
|
|
169
|
+
expect(listed.phases[0]!.runs[0]).toMatchObject({ name: "lint", id: null, opRun: { id: lint.id, env: "ci", signer: null } });
|
|
170
|
+
expect(listed.ignoredRuns).toEqual([]);
|
|
171
|
+
|
|
172
|
+
// No runner key: the tick still tags, unsigned, and the tag names the unsigned runs.
|
|
173
|
+
const tick = await ciTick({ cwd: root, now: NOW });
|
|
174
|
+
expect(tick.made).toEqual([`ci/green/${a}`]);
|
|
175
|
+
expect(tick.signer).toBeNull();
|
|
176
|
+
const record = recordOf(root, `ci/green/${a}`) as { statement?: unknown; phases: Record<string, { opRun: Record<string, unknown> }[]> };
|
|
177
|
+
expect(record.statement).toBeUndefined();
|
|
178
|
+
expect(record.phases.lint![0]!.opRun).toEqual({ id: lint.id, env: "ci", signer: null });
|
|
179
|
+
|
|
180
|
+
// A signed run beside them shows its signer.
|
|
181
|
+
const signed = await runOp(root, "lint", { pem: BOT.pem });
|
|
182
|
+
const source = await readOpRunSource(root, root, git(root, "rev-parse", "HEAD"));
|
|
183
|
+
expect(source.checkRuns(a).map((r) => [r.opRun!.id, r.opRun!.signer])).toEqual([
|
|
184
|
+
[lint.id, null],
|
|
185
|
+
[signed.id, "ci-bot"],
|
|
186
|
+
[expect.any(String), null],
|
|
187
|
+
]);
|
|
188
|
+
expect(source.ignored.size).toBe(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("with ci.green.signedSince an unsigned run neither greens nor revokes a commit from that one on, and is reported", async () => {
|
|
192
|
+
const { root } = workspace();
|
|
193
|
+
const a = commitAt(root, 3, "app/a.txt");
|
|
194
|
+
git(root, "push", "-q", "origin", "main");
|
|
195
|
+
// The opt-in: every run on a, and after it, must be signed.
|
|
196
|
+
writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: { ...GREEN, signedSince: a } } }) });
|
|
155
197
|
await runOp(root, "lint", { pem: BOT.pem });
|
|
156
198
|
const unsigned = await runOp(root, "test");
|
|
157
199
|
expect(unsigned.statement).toBeUndefined();
|
|
158
200
|
push(root);
|
|
159
201
|
|
|
202
|
+
// The opt-in also asks for signed tags, so the tick needs the key.
|
|
203
|
+
await expect(ciTick({ cwd: root, now: NOW })).rejects.toThrow(/needs a statement signed by a runner key, and none was given/);
|
|
160
204
|
const tick = await ciTick({ cwd: root, now: NOW, runnerKey: BOT.pem });
|
|
161
205
|
expect(tick.made).toEqual([]);
|
|
162
206
|
const at = tick.commits.find((c) => c.sha === a)!;
|
|
163
207
|
expect(at.verdict?.verdict).toBe("pending");
|
|
164
|
-
expect(at.ignoredRuns).toEqual([{ run: unsigned.id, op: "test", env: "ci", status: "unsigned", reason: expect.stringContaining("
|
|
208
|
+
expect(at.ignoredRuns).toEqual([{ run: unsigned.id, op: "test", env: "ci", status: "unsigned", reason: expect.stringContaining("ci.green.signedSince is") }]);
|
|
165
209
|
expect(formatTick(tick)).toContain(`ignored run ${unsigned.id} of test in ci (unsigned)`);
|
|
166
210
|
expect(git(root, "tag", "-l")).toBe("");
|
|
167
211
|
|
|
@@ -173,7 +217,6 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
173
217
|
["test", "pending"],
|
|
174
218
|
]);
|
|
175
219
|
expect(listed.ignoredRuns?.map((r) => r.status)).toEqual(["unsigned"]);
|
|
176
|
-
expect(formatList(list)).toContain(`ignored run ${unsigned.id} of test in ci (unsigned)`);
|
|
177
220
|
|
|
178
221
|
// Signed now, the commit goes green; a later unsigned failure does not revoke it.
|
|
179
222
|
await runOp(root, "test", { pem: BOT.pem });
|
|
@@ -186,7 +229,7 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
186
229
|
expect(after.commits.find((c) => c.sha === a)!.ignoredRuns?.map((r) => r.status)).toEqual(["unsigned", "unsigned"]);
|
|
187
230
|
});
|
|
188
231
|
|
|
189
|
-
test("a run signed by an unlisted key, or whose record changed after signing, does not count", async () => {
|
|
232
|
+
test("a run signed by an unlisted key, or whose record changed after signing, does not count, with or without the opt-in", async () => {
|
|
190
233
|
const { root } = workspace();
|
|
191
234
|
const a = commitAt(root, 3, "app/a.txt");
|
|
192
235
|
// A key trust.json does not list can't sign a run here at all.
|
|
@@ -219,13 +262,12 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
219
262
|
expect(record).toMatchObject({ source: "runs", phase: "test", run: { name: "test", id: null, conclusion: "failure", opRun: { id: red.id, signer: "ci-bot" } } });
|
|
220
263
|
});
|
|
221
264
|
|
|
222
|
-
test("a tick from runs
|
|
265
|
+
test("a tick from runs without a runner key makes unsigned tags, --source overrides the declaration, and ci list refuses --forge", async () => {
|
|
223
266
|
const { root } = workspace({ ...GREEN, source: "checks" });
|
|
224
267
|
const a = commitAt(root, 3, "app/a.txt");
|
|
225
268
|
await runOp(root, "lint", { pem: BOT.pem });
|
|
226
269
|
await runOp(root, "test", { pem: BOT.pem });
|
|
227
270
|
push(root);
|
|
228
|
-
await expect(ciTick({ cwd: root, now: NOW, source: "runs" })).rejects.toThrow(/every tag it makes from them is signed by a runner key, and none was given/);
|
|
229
271
|
expect((await ciTick({ cwd: root, now: NOW, source: "runs", dryRun: true })).made).toEqual([`ci/green/${a}`]);
|
|
230
272
|
expect(git(root, "tag", "-l")).toBe("");
|
|
231
273
|
// The declaration says checks, so without --source the tick wants a forge.
|
|
@@ -11,7 +11,7 @@ import { GENERATED_MARKER } from "../discovery/files";
|
|
|
11
11
|
import type { CommandContext } from "../cli/registry";
|
|
12
12
|
import { parseYAMLDocument } from "../yaml";
|
|
13
13
|
import { runCiWorkflow } from "./ci-green-cli";
|
|
14
|
-
import { CI_GREEN_WORKFLOW_FILE, jobCheckRuns, renderCiGreenWorkflow, workflowsHolding } from "./ci-green-workflow";
|
|
14
|
+
import { BUILD_BLOCK_MARKER, buildTriggerEdits, CI_GREEN_WORKFLOW_FILE, dispatchableWorkflows, jobCheckRuns, renderCiGreenWorkflow, workflowsHolding, writeTopLevel } from "./ci-green-workflow";
|
|
15
15
|
import { parseDeclaration } from "./declaration";
|
|
16
16
|
import { cleanScratch, declaration, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
17
17
|
|
|
@@ -302,3 +302,86 @@ describe("chant ci workflow (#3573)", () => {
|
|
|
302
302
|
}
|
|
303
303
|
});
|
|
304
304
|
});
|
|
305
|
+
|
|
306
|
+
// ── A phase's triggers (#4169) ───────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
const BUILD_GREEN = { branch: "main", phases: { lint: { runs: ["lint"], build: "every" }, suites: { runs: ["suites"], build: "newest" } }, require: ["lint", "suites"] };
|
|
309
|
+
const LINT_WF = `name: ci
|
|
310
|
+
on:
|
|
311
|
+
push:
|
|
312
|
+
branches: [main]
|
|
313
|
+
pull_request:
|
|
314
|
+
|
|
315
|
+
# Comments above a block stay.
|
|
316
|
+
concurrency:
|
|
317
|
+
group: ci-\${{ github.ref }}
|
|
318
|
+
cancel-in-progress: true
|
|
319
|
+
|
|
320
|
+
jobs:
|
|
321
|
+
lint:
|
|
322
|
+
runs-on: ubuntu-latest
|
|
323
|
+
`;
|
|
324
|
+
const SUITES_WF = `name: suites
|
|
325
|
+
on:
|
|
326
|
+
schedule:
|
|
327
|
+
- cron: "0 */3 * * *"
|
|
328
|
+
workflow_dispatch: {}
|
|
329
|
+
permissions:
|
|
330
|
+
contents: read
|
|
331
|
+
jobs:
|
|
332
|
+
suites:
|
|
333
|
+
runs-on: ubuntu-latest
|
|
334
|
+
`;
|
|
335
|
+
|
|
336
|
+
describe("the triggers of a phase that sets build (#4169)", () => {
|
|
337
|
+
const green = () => parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [], ci: { green: BUILD_GREEN } }), "chant.workspace.json").ci!.green!;
|
|
338
|
+
|
|
339
|
+
test("newest: pushes to the branch and by hand, no schedule, one group per ref that never cancels; every: the commit's own group on the branch", () => {
|
|
340
|
+
const edits = buildTriggerEdits(green(), [
|
|
341
|
+
{ path: ".github/workflows/ci.yml", text: LINT_WF },
|
|
342
|
+
{ path: ".github/workflows/suites.yml", text: SUITES_WF },
|
|
343
|
+
]);
|
|
344
|
+
expect(edits.map((e) => [e.path, e.build, e.phases, e.changed])).toEqual([
|
|
345
|
+
[".github/workflows/ci.yml", "every", ["lint"], true],
|
|
346
|
+
[".github/workflows/suites.yml", "newest", ["suites"], true],
|
|
347
|
+
]);
|
|
348
|
+
const ci = parseYAMLDocument(edits[0].text) as Yaml;
|
|
349
|
+
// The triggers of an every phase stay, pull requests included.
|
|
350
|
+
expect(ci.on).toEqual({ push: { branches: ["main"] }, pull_request: null });
|
|
351
|
+
expect(ci.concurrency).toEqual({ group: "chant-build-lint-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}", "cancel-in-progress": "${{ github.ref != 'refs/heads/main' }}" });
|
|
352
|
+
expect(edits[0].text).toContain(`# Comments above a block stay.\n${BUILD_BLOCK_MARKER}\nconcurrency:`);
|
|
353
|
+
const suites = parseYAMLDocument(edits[1].text) as Yaml;
|
|
354
|
+
expect(suites.on).toEqual({ push: { branches: ["main"] }, workflow_dispatch: {} });
|
|
355
|
+
expect(suites.concurrency).toEqual({ group: "chant-build-suites-${{ github.ref }}", "cancel-in-progress": false });
|
|
356
|
+
// A file without the block gets it before permissions:, and writing again changes nothing.
|
|
357
|
+
expect(edits[1].text.indexOf("concurrency:")).toBeLessThan(edits[1].text.indexOf("permissions:"));
|
|
358
|
+
const again = buildTriggerEdits(green(), edits.map((e) => ({ path: e.path, text: e.text })));
|
|
359
|
+
expect(again.map((e) => e.changed)).toEqual([false, false]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a phase without build leaves its workflow alone, and one workflow holding both builds is refused", () => {
|
|
363
|
+
const plain = parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [], ci: { green: { branch: "main", phases: { lint: ["lint"] }, require: ["lint"] } } }), "chant.workspace.json").ci!.green!;
|
|
364
|
+
expect(buildTriggerEdits(plain, [{ path: ".github/workflows/ci.yml", text: LINT_WF }])).toEqual([]);
|
|
365
|
+
const both = LINT_WF.replace("jobs:\n", "jobs:\n suites:\n runs-on: ubuntu-latest\n");
|
|
366
|
+
expect(() => buildTriggerEdits(green(), [{ path: ".github/workflows/ci.yml", text: both }])).toThrow(/holds runs of lint \(build: every\) and suites \(build: newest\)/);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("writeTopLevel replaces a block up to the next top-level line, or adds one before jobs", () => {
|
|
370
|
+
expect(writeTopLevel("a: 1\nconcurrency:\n group: x\n\njobs: {}\n", "concurrency", { group: "y" })).toBe(`a: 1\n${BUILD_BLOCK_MARKER}\nconcurrency:\n group: y\n\njobs: {}\n`);
|
|
371
|
+
expect(writeTopLevel("a: 1\njobs: {}\n", "concurrency", { group: "y" })).toBe(`a: 1\n${BUILD_BLOCK_MARKER}\nconcurrency:\n group: y\n\njobs: {}\n`);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("the tick may re-run jobs and start workflows, and ticks on a build tag's runs, only when a phase sets build", () => {
|
|
375
|
+
const tick = parseYAMLDocument(renderCiGreenWorkflow({ green: green(), workflows: ["ci"], chantVersion: "0.124.33", workspaceRoot: "." })) as Yaml;
|
|
376
|
+
expect(tick.permissions).toEqual({ contents: "write", checks: "read", actions: "write" });
|
|
377
|
+
expect(tick.on.workflow_run.branches).toEqual(["main", "ci/build/**"]);
|
|
378
|
+
const plain = parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [], ci: { green: { branch: "main", phases: { lint: ["lint"] }, require: ["lint"] } } }), "chant.workspace.json").ci!.green!;
|
|
379
|
+
const before = parseYAMLDocument(renderCiGreenWorkflow({ green: plain, workflows: ["ci"], chantVersion: "0.124.33", workspaceRoot: "." })) as Yaml;
|
|
380
|
+
expect(before.permissions).toEqual({ contents: "write", checks: "read" });
|
|
381
|
+
expect(before.on.workflow_run.branches).toEqual(["main"]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("the workflows a build request can start are the ones holding its phases with workflow_dispatch", () => {
|
|
385
|
+
expect(dispatchableWorkflows(green(), ["lint", "suites"], [{ path: ".github/workflows/ci.yml", text: LINT_WF }, { path: ".github/workflows/suites.yml", text: SUITES_WF }])).toEqual({ dispatchable: ["suites.yml"], without: ["ci.yml"] });
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
49
49
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
50
50
|
import { GENERATED_MARKER } from "../discovery/files";
|
|
51
|
-
import { parseYAMLDocument } from "../yaml";
|
|
52
|
-
import type { CiGreen } from "./declaration";
|
|
51
|
+
import { emitYAMLEntry, parseYAMLDocument } from "../yaml";
|
|
52
|
+
import type { CiBuild, CiGreen } from "./declaration";
|
|
53
53
|
import { matchesCheckRun } from "./ci-green";
|
|
54
54
|
import { githubSecretOrJobToken } from "../ci-jobs";
|
|
55
55
|
|
|
@@ -129,6 +129,31 @@ export function workflowsHolding(green: CiGreen, sources: readonly WorkflowSourc
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The workflow files that hold a check run of one of `phases` and can be
|
|
134
|
+
* started by hand (`workflow_dispatch`), by file name, for a build the tick
|
|
135
|
+
* asks for (#4169). Files that can't be parsed are left out.
|
|
136
|
+
*/
|
|
137
|
+
export function dispatchableWorkflows(green: CiGreen, phases: readonly string[], sources: readonly WorkflowSource[]): { dispatchable: string[]; without: string[] } {
|
|
138
|
+
const patterns = phases.flatMap((name) => green.phases.find((p) => p.name === name)?.runs ?? []);
|
|
139
|
+
const dispatchable: string[] = [];
|
|
140
|
+
const without: string[] = [];
|
|
141
|
+
for (const s of sources) {
|
|
142
|
+
let doc: unknown;
|
|
143
|
+
try {
|
|
144
|
+
doc = parseYAMLDocument(s.text);
|
|
145
|
+
} catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const runs = jobCheckRuns(doc);
|
|
149
|
+
if (!patterns.some((p) => runs.some((r) => meet(p, r)))) continue;
|
|
150
|
+
const on = isObject(doc) ? doc.on : undefined;
|
|
151
|
+
const file = s.path.slice(s.path.lastIndexOf("/") + 1);
|
|
152
|
+
(isObject(on) && "workflow_dispatch" in on ? dispatchable : without).push(file);
|
|
153
|
+
}
|
|
154
|
+
return { dispatchable: dispatchable.sort(), without: without.sort() };
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
/** The repository's GitHub workflow files, other than `skip`. */
|
|
133
158
|
export function readWorkflowSources(repoRoot: string, skip: string): WorkflowSource[] {
|
|
134
159
|
const dir = join(repoRoot, ".github", "workflows");
|
|
@@ -213,6 +238,8 @@ export function renderCiGreenWorkflow(input: WorkflowInput): string {
|
|
|
213
238
|
" - completed",
|
|
214
239
|
" branches:",
|
|
215
240
|
` - ${q(green.branch)}`,
|
|
241
|
+
// #4169: a build the tick asked for runs at a ci/build/<sha> tag, and its completion ticks too.
|
|
242
|
+
...(newestPhases(green) ? [` - ${q("ci/build/**")}`] : []),
|
|
216
243
|
" schedule:",
|
|
217
244
|
" - cron: \"*/15 * * * *\"",
|
|
218
245
|
" workflow_dispatch: {}",
|
|
@@ -225,6 +252,8 @@ export function renderCiGreenWorkflow(input: WorkflowInput): string {
|
|
|
225
252
|
"permissions:",
|
|
226
253
|
" contents: write",
|
|
227
254
|
" checks: read",
|
|
255
|
+
// #4169: re-running a failed job and starting a workflow at a ci/build/<sha> tag.
|
|
256
|
+
...(declaresBuild(green) ? [" actions: write"] : []),
|
|
228
257
|
"",
|
|
229
258
|
"jobs:",
|
|
230
259
|
" tick:",
|
|
@@ -254,6 +283,114 @@ export function renderCiGreenWorkflow(input: WorkflowInput): string {
|
|
|
254
283
|
return lines.join("\n") + "\n";
|
|
255
284
|
}
|
|
256
285
|
|
|
286
|
+
/** Whether any phase sets `build` (#4169): then the tick may re-run jobs and start workflows. */
|
|
287
|
+
export const declaresBuild = (green: CiGreen) => green.phases.some((p) => p.build !== undefined);
|
|
288
|
+
/** Whether a required phase builds only the newest commit (#4169). */
|
|
289
|
+
export const newestPhases = (green: CiGreen) => green.phases.some((p) => p.build === "newest" && green.require.includes(p.name));
|
|
290
|
+
|
|
291
|
+
// ── A phase's triggers (#4169) ───────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
/** The comment line above each top-level block `ci workflow` writes into a phase's workflow. */
|
|
294
|
+
export const BUILD_BLOCK_MARKER = "# Written by chant ci workflow from ci.green's build (#4169); edit ci.green and run it again.";
|
|
295
|
+
|
|
296
|
+
/** What `ci workflow` writes into one workflow holding a phase that sets `build`. */
|
|
297
|
+
export interface BuildTriggerEdit {
|
|
298
|
+
/** Relative to the repository root. */
|
|
299
|
+
path: string;
|
|
300
|
+
build: CiBuild;
|
|
301
|
+
phases: string[];
|
|
302
|
+
/** The file with its triggers and concurrency written. */
|
|
303
|
+
text: string;
|
|
304
|
+
changed: boolean;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The workflows holding the check runs of phases that set `build`, each with
|
|
309
|
+
* its triggers and concurrency written for that build (#4169):
|
|
310
|
+
*
|
|
311
|
+
* - `newest`: on pushes to the branch, and by hand (`workflow_dispatch`),
|
|
312
|
+
* which is how the tick starts a bisect step or a requested build at a
|
|
313
|
+
* `ci/build/<sha>` tag. A `schedule` goes: a runner freeing up, not a
|
|
314
|
+
* clock, picks the commit. Other triggers stay. One concurrency group per
|
|
315
|
+
* ref that never cancels a run in progress, so GitHub keeps the newest
|
|
316
|
+
* push waiting and drops the ones between, and each tag's build runs in a
|
|
317
|
+
* group of its own.
|
|
318
|
+
* - `every`: the triggers stay, and the group is the commit itself on the
|
|
319
|
+
* branch, so no push cancels or replaces another's run; elsewhere, such as
|
|
320
|
+
* a pull request, a new push cancels the run it supersedes.
|
|
321
|
+
*
|
|
322
|
+
* Only the top-level `on:` (for `newest`) and `concurrency:` blocks change,
|
|
323
|
+
* each under {@link BUILD_BLOCK_MARKER}; the rest of the file is kept as it
|
|
324
|
+
* is. Throws when one workflow holds a phase of each build, which no one
|
|
325
|
+
* concurrency group can serve.
|
|
326
|
+
*/
|
|
327
|
+
export function buildTriggerEdits(green: CiGreen, sources: readonly WorkflowSource[]): BuildTriggerEdit[] {
|
|
328
|
+
const out: BuildTriggerEdit[] = [];
|
|
329
|
+
for (const s of sources) {
|
|
330
|
+
let doc: unknown;
|
|
331
|
+
try {
|
|
332
|
+
doc = parseYAMLDocument(s.text);
|
|
333
|
+
} catch {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const runs = jobCheckRuns(doc);
|
|
337
|
+
const held = green.phases.filter((p) => p.build !== undefined && p.runs.some((pattern) => runs.some((r) => meet(pattern, r))));
|
|
338
|
+
if (held.length === 0) continue;
|
|
339
|
+
const builds = [...new Set(held.map((p) => p.build!))];
|
|
340
|
+
if (builds.length > 1) {
|
|
341
|
+
throw new Error(`${s.path} holds runs of ${held.map((p) => `${p.name} (build: ${p.build})`).join(" and ")}, and one workflow can't run on every commit and only the newest; move the jobs into a workflow per build`);
|
|
342
|
+
}
|
|
343
|
+
const build = builds[0];
|
|
344
|
+
const phases = held.map((p) => p.name);
|
|
345
|
+
const tag = phases.map((p) => p.replace(/[^A-Za-z0-9_-]/g, "-")).join("-");
|
|
346
|
+
let text = s.text;
|
|
347
|
+
if (build === "newest") {
|
|
348
|
+
const on = isObject(doc) && isObject(doc.on) ? { ...doc.on } : {};
|
|
349
|
+
delete on.schedule;
|
|
350
|
+
const push = isObject(on.push) ? { ...on.push } : {};
|
|
351
|
+
const branches = Array.isArray(push.branches) ? push.branches.filter((b): b is string => typeof b === "string") : [];
|
|
352
|
+
push.branches = branches.includes(green.branch) ? branches : [...branches, green.branch];
|
|
353
|
+
const { push: _push, workflow_dispatch: dispatch, ...rest } = on;
|
|
354
|
+
text = writeTopLevel(text, "on", { push, workflow_dispatch: dispatch ?? {}, ...rest });
|
|
355
|
+
text = writeTopLevel(text, "concurrency", { group: `chant-build-${tag}-\${{ github.ref }}`, "cancel-in-progress": false });
|
|
356
|
+
} else {
|
|
357
|
+
const main = `refs/heads/${green.branch}`;
|
|
358
|
+
// Written as lines: the emitter would quote the expression, and GitHub reads it the same either way.
|
|
359
|
+
text = writeTopLevel(text, "concurrency", [
|
|
360
|
+
"concurrency:",
|
|
361
|
+
` group: chant-build-${tag}-\${{ github.ref == '${main}' && github.sha || github.ref }}`,
|
|
362
|
+
` cancel-in-progress: \${{ github.ref != '${main}' }}`,
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
out.push({ path: s.path, build, phases, text, changed: text !== s.text });
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Replace the top-level `key:` block of a YAML file with `value` (or with
|
|
372
|
+
* `value`'s lines, the key's own line first), under
|
|
373
|
+
* {@link BUILD_BLOCK_MARKER}, or add it before `permissions:` or `jobs:`
|
|
374
|
+
* when the file has none. The block runs from its key's line to the next
|
|
375
|
+
* line at column 0 that is not blank; comments above the key stay.
|
|
376
|
+
*/
|
|
377
|
+
export function writeTopLevel(text: string, key: string, value: Record<string, unknown> | string[]): string {
|
|
378
|
+
const lines = text.split("\n");
|
|
379
|
+
const block = [BUILD_BLOCK_MARKER, ...(Array.isArray(value) ? value : emitYAMLEntry(key, value).split("\n"))];
|
|
380
|
+
const at = lines.findIndex((l) => l === `${key}:` || l.startsWith(`${key}: `) || l.startsWith(`${key}:#`));
|
|
381
|
+
if (at < 0) {
|
|
382
|
+
const before = lines.findIndex((l) => /^(permissions|jobs):/.test(l));
|
|
383
|
+
const where = before < 0 ? lines.length : before;
|
|
384
|
+
return [...lines.slice(0, where), ...block, "", ...lines.slice(where)].join("\n");
|
|
385
|
+
}
|
|
386
|
+
let end = at + 1;
|
|
387
|
+
while (end < lines.length && (lines[end] === "" || /^\s/.test(lines[end]))) end++;
|
|
388
|
+
// A blank line ending the block belongs to what follows it.
|
|
389
|
+
while (end > at + 1 && lines[end - 1] === "") end--;
|
|
390
|
+
const start = at > 0 && lines[at - 1] === BUILD_BLOCK_MARKER ? at - 1 : at;
|
|
391
|
+
return [...lines.slice(0, start), ...block, ...lines.slice(end)].join("\n");
|
|
392
|
+
}
|
|
393
|
+
|
|
257
394
|
/** The header line naming the command that regenerates the file, and where to run it. */
|
|
258
395
|
export function ciGreenHeader(command: string, where: "member" | "root"): string {
|
|
259
396
|
return `# ${GENERATED_MARKER}. Regenerate with: ${command} (in the ${where === "member" ? "member's directory" : "workspace root"})\n`;
|
|
@@ -51,7 +51,7 @@ describe("the ci.green block (#3573)", () => {
|
|
|
51
51
|
});
|
|
52
52
|
expect(parseDeclaration(decl({ green: { ...GREEN, signedSince: "a".repeat(40) } }), "chant.workspace.json").ci!.green!.signedSince).toBe("a".repeat(40));
|
|
53
53
|
expect(parseDeclaration(decl({ green: { ...GREEN, window: "90m" } }), "chant.workspace.json").ci!.green!.window).toBe("90m");
|
|
54
|
-
expect(parseDeclaration(decl({}), "chant.workspace.json").ci).toEqual({ green: null });
|
|
54
|
+
expect(parseDeclaration(decl({}), "chant.workspace.json").ci).toEqual({ green: null, reportUrl: null });
|
|
55
55
|
expect(parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [] }), "chant.workspace.json").ci).toBeNull();
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -117,10 +117,13 @@ describe("judging a phase from check runs (#3573)", () => {
|
|
|
117
117
|
const runs = [run(1, "macos (test)", "skipped")];
|
|
118
118
|
expect(evaluatePhase(phase(["macos (test)"]), runs).verdict).toBe("fail");
|
|
119
119
|
expect(evaluatePhase(phase(["macos (test)"], "pass"), runs).verdict).toBe("pass");
|
|
120
|
-
// Neither counts any other conclusion as a pass.
|
|
121
|
-
for (const c of ["neutral", "
|
|
120
|
+
// Neither counts any other conclusion as a pass, and a stopped run is no failure either (#4169).
|
|
121
|
+
for (const c of ["neutral", "timed_out", "action_required"]) {
|
|
122
122
|
expect(evaluatePhase(phase(["x"], "pass"), [run(1, "x", c)]).verdict, c).toBe("fail");
|
|
123
123
|
}
|
|
124
|
+
for (const c of ["cancelled", "stale"]) {
|
|
125
|
+
expect(evaluatePhase(phase(["x"], "pass"), [run(1, "x", c)]), c).toMatchObject({ verdict: "unbuilt", reason: "cancelled" });
|
|
126
|
+
}
|
|
124
127
|
});
|
|
125
128
|
|
|
126
129
|
test("a run in progress, or a pattern nothing matches yet, leaves the phase undecided; a failure decides it", () => {
|