@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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local files a component's steps deploy from, and their content digests
|
|
3
|
+
* (#4155).
|
|
4
|
+
*
|
|
5
|
+
* A component gate approves `componentPlanDigest` (./gate-plan.ts). A step
|
|
6
|
+
* whose capability has a `plan` is bound by what it plans. Every other step
|
|
7
|
+
* was bound only by its input as written, so a Helm chart, a kustomize
|
|
8
|
+
* directory or a static site's tree could change after review without moving
|
|
9
|
+
* the digest. This module names those files and hashes them, so the digest
|
|
10
|
+
* moves when they do.
|
|
11
|
+
*
|
|
12
|
+
* Which fields name a path is data, as the template fields are in
|
|
13
|
+
* ./release-compile.ts and the unit fields in ./deploy-units.ts: a kind's
|
|
14
|
+
* fields in {@link DEPLOY_INPUT_FIELDS}, plus `template`, `manifest` and
|
|
15
|
+
* `values` on any step ({@link TEMPLATE_FIELDS}). Only a literal path counts:
|
|
16
|
+
* a value wired from another step (`@Build.uri`) or an `archive:` path is a
|
|
17
|
+
* build output, and binding it would make an approval unusable by a build
|
|
18
|
+
* that is not reproducible. The build steps' own sources are bound instead.
|
|
19
|
+
*
|
|
20
|
+
* - A file is hashed by its bytes.
|
|
21
|
+
* - A directory is hashed over the files under it that git does not ignore
|
|
22
|
+
* (`git ls-files --cached --others --exclude-standard`), each by its path
|
|
23
|
+
* and bytes, so `node_modules` and a build's `dist` inside a Docker context
|
|
24
|
+
* are left out. Outside a git repository every file is hashed except under
|
|
25
|
+
* `.git` and `node_modules`.
|
|
26
|
+
* - A directory git ignores as a whole is a build output. It is not hashed;
|
|
27
|
+
* the step is listed as unbound, with that reason.
|
|
28
|
+
* - A path that does not exist is left out. One that appears later moves the
|
|
29
|
+
* digest.
|
|
30
|
+
*
|
|
31
|
+
* A step whose kind runs content chant can't name, such as `shell`, is listed
|
|
32
|
+
* as unbound ({@link UNBOUND_KINDS}), so the plan note can show a reviewer
|
|
33
|
+
* what the approval does not cover.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
38
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
39
|
+
import { sha256Hex } from "../content-digest";
|
|
40
|
+
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
41
|
+
import type { DriverGate, DriverPhase, DriverStep } from "./driver";
|
|
42
|
+
|
|
43
|
+
/** The fields that name every step's deployed file, whatever its kind (as ./release-compile.ts reads them). */
|
|
44
|
+
export const TEMPLATE_FIELDS: readonly string[] = ["template", "manifest", "values"];
|
|
45
|
+
|
|
46
|
+
/** Per kind, the fields beyond {@link TEMPLATE_FIELDS} that name a local file or directory the step deploys or builds from. */
|
|
47
|
+
export const DEPLOY_INPUT_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
48
|
+
"helm-upgrade": ["chart"],
|
|
49
|
+
"kustomize-apply": ["dir"],
|
|
50
|
+
"fly-release": ["plan"],
|
|
51
|
+
"fly-rollback": ["plan"],
|
|
52
|
+
"s3-sync": ["from"],
|
|
53
|
+
"r2-sync": ["from"],
|
|
54
|
+
"copy-to-host": ["from"],
|
|
55
|
+
"wrangler-deploy": ["config"],
|
|
56
|
+
"wrangler-versions-promote": ["config"],
|
|
57
|
+
"docker-build": ["context"],
|
|
58
|
+
"zip-package": ["source"],
|
|
59
|
+
"jvm-build": ["path"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Kinds that deploy content chant can't name from their input: a command, an agent's work. */
|
|
63
|
+
export const UNBOUND_KINDS: readonly string[] = ["shell", "run-agent"];
|
|
64
|
+
|
|
65
|
+
/** One path a step names. */
|
|
66
|
+
export interface DeployInputRef {
|
|
67
|
+
phase: string;
|
|
68
|
+
kind: string;
|
|
69
|
+
field: string;
|
|
70
|
+
/** Relative to the member's directory, as written. */
|
|
71
|
+
path: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A named path and the digest of what is there. */
|
|
75
|
+
export interface DeployInputDigest {
|
|
76
|
+
path: string;
|
|
77
|
+
/** `sha256:<hex>` of a file's bytes, or a plan digest over a directory's files. */
|
|
78
|
+
digest: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A step the digest does not cover, and why. */
|
|
82
|
+
export interface UnboundStep {
|
|
83
|
+
phase: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What a component's steps deploy from: the digests, and the steps they can't cover. */
|
|
89
|
+
export interface ComponentDeployInputs {
|
|
90
|
+
inputs: DeployInputDigest[];
|
|
91
|
+
unbound: UnboundStep[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
|
|
95
|
+
return (step as { kind?: unknown }).kind === "gate";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
|
|
99
|
+
return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isLiteralPath = (v: unknown): v is string =>
|
|
103
|
+
typeof v === "string" && v !== "" && !v.startsWith("@") && !v.startsWith("$") && !v.startsWith("archive:");
|
|
104
|
+
|
|
105
|
+
/** Every capability step of a composition with its phase, nested phases included, gates left out. */
|
|
106
|
+
export function stepsWithPhase(phases: readonly DriverPhase[]): Array<{ phase: string; step: DriverStep }> {
|
|
107
|
+
const out: Array<{ phase: string; step: DriverStep }> = [];
|
|
108
|
+
const walk = (phase: DriverPhase): void => {
|
|
109
|
+
for (const step of phase.steps) {
|
|
110
|
+
if (isPhase(step)) walk(step);
|
|
111
|
+
else if (!isGate(step)) out.push({ phase: phase.phase, step });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
phases.forEach(walk);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The literal paths one step names, in field order. A Docker build's Dockerfile is named under its context. */
|
|
119
|
+
export function stepInputRefs(phase: string, step: DriverStep): DeployInputRef[] {
|
|
120
|
+
const out: DeployInputRef[] = [];
|
|
121
|
+
for (const field of [...TEMPLATE_FIELDS, ...(DEPLOY_INPUT_FIELDS[step.kind] ?? [])]) {
|
|
122
|
+
const raw = step[field];
|
|
123
|
+
for (const value of Array.isArray(raw) ? raw : [raw]) {
|
|
124
|
+
if (isLiteralPath(value)) out.push({ phase, kind: step.kind, field, path: value });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (step.kind === "docker-build" && isLiteralPath(step.context) && isLiteralPath(step.dockerfile)) {
|
|
128
|
+
out.push({ phase, kind: step.kind, field: "dockerfile", path: join(step.context, step.dockerfile) });
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function git(root: string, args: string[]): { ok: boolean; out: string } {
|
|
134
|
+
const r = spawnSync("git", ["-C", root, ...args], { encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 });
|
|
135
|
+
return { ok: r.status === 0, out: r.stdout ?? "" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The files under `dir` (relative to `root`) a directory digest covers, relative to `root`, sorted. */
|
|
139
|
+
export function directoryFiles(root: string, dir: string): string[] {
|
|
140
|
+
const listed = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", dir]);
|
|
141
|
+
if (listed.ok) {
|
|
142
|
+
return [...new Set(listed.out.split("\0").filter((p) => p !== ""))]
|
|
143
|
+
.filter((p) => {
|
|
144
|
+
const full = resolve(root, p);
|
|
145
|
+
return existsSync(full) && statSync(full).isFile();
|
|
146
|
+
})
|
|
147
|
+
.sort();
|
|
148
|
+
}
|
|
149
|
+
return walkFiles(root, dir);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Every file under `dir`, relative to `root`, sorted, leaving out `.git` and `node_modules`. What a cut records of a directory git ignores. */
|
|
153
|
+
export function walkFiles(root: string, dir: string): string[] {
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
const walk = (abs: string): void => {
|
|
156
|
+
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
157
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
158
|
+
const full = join(abs, entry.name);
|
|
159
|
+
if (entry.isDirectory()) walk(full);
|
|
160
|
+
else if (entry.isFile()) out.push(relative(root, full).split(sep).join("/"));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
walk(resolve(root, dir));
|
|
164
|
+
return out.sort();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Whether git ignores `path` (relative to `root`). False outside a git repository. */
|
|
168
|
+
export function ignoredByGit(root: string, path: string): boolean {
|
|
169
|
+
return git(root, ["check-ignore", "-q", "--", path]).ok;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** `sha256:<hex>` of a file's bytes. */
|
|
173
|
+
export function fileDigest(file: string): string {
|
|
174
|
+
return `sha256:${sha256Hex(readFileSync(file))}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The digest of the files under `dir`: a plan digest over each file's path (from `dir`) and bytes. */
|
|
178
|
+
export function directoryDigest(root: string, dir: string): string {
|
|
179
|
+
const base = resolve(root, dir);
|
|
180
|
+
const files = directoryFiles(root, dir).map((p) => ({
|
|
181
|
+
path: relative(base, resolve(root, p)).split(sep).join("/"),
|
|
182
|
+
digest: fileDigest(resolve(root, p)),
|
|
183
|
+
}));
|
|
184
|
+
return computePlanDigest("directory", files);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* What a component's steps deploy from, read under `root` (the member's
|
|
189
|
+
* directory). `include` picks the steps to read, all of them by default; the
|
|
190
|
+
* wave planner passes the steps without a `plan`, which their plan does not
|
|
191
|
+
* already cover.
|
|
192
|
+
*/
|
|
193
|
+
export function componentDeployInputs(
|
|
194
|
+
phases: readonly DriverPhase[],
|
|
195
|
+
root: string,
|
|
196
|
+
include: (step: DriverStep) => boolean = () => true,
|
|
197
|
+
): ComponentDeployInputs {
|
|
198
|
+
const inputs = new Map<string, string>();
|
|
199
|
+
const unbound: UnboundStep[] = [];
|
|
200
|
+
for (const { phase, step } of stepsWithPhase(phases)) {
|
|
201
|
+
if (!include(step)) continue;
|
|
202
|
+
if (UNBOUND_KINDS.includes(step.kind)) {
|
|
203
|
+
unbound.push({ phase, kind: step.kind, reason: `a ${step.kind} step runs content chant can't name from its input` });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
for (const ref of stepInputRefs(phase, step)) {
|
|
207
|
+
if (inputs.has(ref.path)) continue;
|
|
208
|
+
const full = resolve(root, ref.path);
|
|
209
|
+
if (!existsSync(full)) continue;
|
|
210
|
+
if (statSync(full).isDirectory()) {
|
|
211
|
+
if (ignoredByGit(root, ref.path)) {
|
|
212
|
+
unbound.push({ phase, kind: step.kind, reason: `its ${ref.field} "${ref.path}" is a directory git ignores, so a build output; bind its sources through the step that builds it` });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
inputs.set(ref.path, directoryDigest(root, ref.path));
|
|
216
|
+
} else {
|
|
217
|
+
inputs.set(ref.path, fileDigest(full));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
inputs: [...inputs].map(([path, digest]) => ({ path, digest })).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
|
223
|
+
unbound,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, expect, it } from "vitest";
|
|
14
|
-
import { CapabilityRegistry, type DeployContext } from "./capability";
|
|
14
|
+
import { CapabilityRegistry, isPlanMovedError, type DeployContext } from "./capability";
|
|
15
15
|
import { stubCapability } from "./verbs/stub";
|
|
16
16
|
import { describeGateMismatch, memoryGateLedgerPort } from "../op/gate";
|
|
17
17
|
import { componentPlanDigest } from "./gate-plan";
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
UnknownDependencyError,
|
|
23
23
|
accumulateComponentOutputs,
|
|
24
24
|
collectComponentOutputs,
|
|
25
|
+
failedRunExitCode,
|
|
25
26
|
resolveComponentGraph,
|
|
26
27
|
resolveWiring,
|
|
27
28
|
runComponentDeploy,
|
|
@@ -1173,3 +1174,37 @@ describe("unresolvedWiringSources (#4084)", () => {
|
|
|
1173
1174
|
).toEqual([]);
|
|
1174
1175
|
});
|
|
1175
1176
|
});
|
|
1177
|
+
|
|
1178
|
+
describe("a step that refuses a moved plan (#3997)", () => {
|
|
1179
|
+
it.each([
|
|
1180
|
+
["planMoved: true", { planMoved: true }, true],
|
|
1181
|
+
["exitCode 3", { exitCode: 3 }, true],
|
|
1182
|
+
["exitCode 7", { exitCode: 7 }, false],
|
|
1183
|
+
['exitCode "3"', { exitCode: "3" }, false],
|
|
1184
|
+
["planMoved: \"yes\"", { planMoved: "yes" }, false],
|
|
1185
|
+
["no mark", {}, false],
|
|
1186
|
+
])("isPlanMovedError reads an error with %s", (_label, mark, moved) => {
|
|
1187
|
+
expect(isPlanMovedError(Object.assign(new Error("refused"), mark))).toBe(moved);
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it("is not fooled by a non-object", () => {
|
|
1191
|
+
expect(isPlanMovedError(null)).toBe(false);
|
|
1192
|
+
expect(isPlanMovedError("planMoved")).toBe(false);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it("marks the failed step's record, and failedRunExitCode answers 3 for it and 1 for any other failure", async () => {
|
|
1196
|
+
const run = async (mark: object) => {
|
|
1197
|
+
const registry = new CapabilityRegistry();
|
|
1198
|
+
registry.register(fakeCapability("apply", { run: () => { throw Object.assign(new Error("refused"), mark); } }).capability);
|
|
1199
|
+
const component: DriverComponent = { name: "c", deploy: [{ phase: "Apply", steps: [{ kind: "apply" }] }] };
|
|
1200
|
+
return runComponentDeploy(component, { env: "dev", component: "c" }, registry, {});
|
|
1201
|
+
};
|
|
1202
|
+
const moved = await run({ exitCode: 3 });
|
|
1203
|
+
expect(moved.records[0]).toMatchObject({ status: "fail", planMoved: true });
|
|
1204
|
+
expect(failedRunExitCode([moved])).toBe(3);
|
|
1205
|
+
const other = await run({ exitCode: 7 });
|
|
1206
|
+
expect(other.records[0]).not.toHaveProperty("planMoved");
|
|
1207
|
+
expect(failedRunExitCode([other])).toBe(1);
|
|
1208
|
+
expect(failedRunExitCode([other, moved])).toBe(3);
|
|
1209
|
+
});
|
|
1210
|
+
});
|
package/src/components/driver.ts
CHANGED
|
@@ -52,8 +52,10 @@ import { evaluateGate, gitGateLedgerPort, type GateDigestMismatch, type GateLedg
|
|
|
52
52
|
import { gateName } from "../op/gate-name";
|
|
53
53
|
import { GateNotRecordableError } from "../lifecycle/no-record";
|
|
54
54
|
import { componentPlanDigest } from "./gate-plan";
|
|
55
|
+
import { componentDeployInputs } from "./deploy-inputs";
|
|
55
56
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
56
57
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
58
|
+
import { isPlanMovedError, PLAN_MOVED_EXIT_CODE } from "./capability";
|
|
57
59
|
import { isPromotedArtifact } from "./auto-release";
|
|
58
60
|
import type { ReleaseIdentity } from "../telemetry-attribution";
|
|
59
61
|
import type { RunProgressEvent } from "./run-progress";
|
|
@@ -154,6 +156,8 @@ export interface DriverStepRecord {
|
|
|
154
156
|
durationMs: number;
|
|
155
157
|
output?: unknown;
|
|
156
158
|
error?: string;
|
|
159
|
+
/** Set on a failed step whose capability refused a plan that moved since it was approved (`isPlanMovedError`, #3997). */
|
|
160
|
+
planMoved?: true;
|
|
157
161
|
/** Set on a `gate` step that passed (#2119): who resolved it, when, and at what address. */
|
|
158
162
|
approval?: {
|
|
159
163
|
gate: string;
|
|
@@ -356,12 +360,51 @@ export function unresolvedWiringSources(
|
|
|
356
360
|
input: Record<string, unknown>,
|
|
357
361
|
componentOutputs: Record<string, Record<string, unknown>>,
|
|
358
362
|
): string[] {
|
|
359
|
-
|
|
363
|
+
return [...new Set(unresolvedWiringReferences(input, componentOutputs).map((r) => r.source))].sort();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** One cross-component reference a step input makes: whose output, and which. */
|
|
367
|
+
export interface WiringReference {
|
|
368
|
+
/** The component or stack whose output it reads. */
|
|
369
|
+
source: string;
|
|
370
|
+
/** `<stack>.<name>` for `stackOutput()`, `<component>.publish.<field>` for an artifact. */
|
|
371
|
+
ref: string;
|
|
372
|
+
/**
|
|
373
|
+
* Whether it reads a published artifact's identity (#3999): an
|
|
374
|
+
* `@<component>.publish.*` reference, or a `stackOutput()` whose name is
|
|
375
|
+
* under `publish`. The driver namespaces every publish-family output there
|
|
376
|
+
* (`collectComponentOutputs`), whatever capability wrote it.
|
|
377
|
+
*/
|
|
378
|
+
publish: boolean;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Every cross-component reference in `input` that has no value in
|
|
383
|
+
* `componentOutputs`, sorted by `ref` and each once (#3999). A pull
|
|
384
|
+
* request's plan names these when the output is known only after its
|
|
385
|
+
* upstream applies.
|
|
386
|
+
*/
|
|
387
|
+
export function unresolvedWiringReferences(
|
|
388
|
+
input: Record<string, unknown>,
|
|
389
|
+
componentOutputs: Record<string, Record<string, unknown>>,
|
|
390
|
+
): WiringReference[] {
|
|
391
|
+
return wiringReferences(input).filter((r) => r.value(componentOutputs) === undefined).map(({ source, ref, publish }) => ({ source, ref, publish }));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Every cross-component reference in `input`, each once, sorted by `ref`,
|
|
396
|
+
* with the value it resolves to against a given outputs map.
|
|
397
|
+
*/
|
|
398
|
+
function wiringReferences(input: Record<string, unknown>): Array<WiringReference & { value: (outputs: Record<string, Record<string, unknown>>) => unknown }> {
|
|
399
|
+
const found = new Map<string, WiringReference & { value: (outputs: Record<string, Record<string, unknown>>) => unknown }>();
|
|
360
400
|
const walk = (value: unknown): void => {
|
|
361
401
|
if (typeof value === "string") {
|
|
362
402
|
if (PRIOR_STEP_REF.test(value)) return;
|
|
363
403
|
const ref = value.match(COMPONENT_ARTIFACT_REF);
|
|
364
|
-
if (ref
|
|
404
|
+
if (ref) {
|
|
405
|
+
const [, source, field] = ref;
|
|
406
|
+
found.set(`${source}.publish.${field}`, { source, ref: `${source}.publish.${field}`, publish: true, value: (o) => resolvePath(o[source]?.publish, field) });
|
|
407
|
+
}
|
|
365
408
|
return;
|
|
366
409
|
}
|
|
367
410
|
if (Array.isArray(value)) {
|
|
@@ -371,14 +414,29 @@ export function unresolvedWiringSources(
|
|
|
371
414
|
if (value && typeof value === "object") {
|
|
372
415
|
if ("stackOutput" in value) {
|
|
373
416
|
const { stack, name } = (value as { stackOutput: { stack: string; name: string } }).stackOutput;
|
|
374
|
-
|
|
417
|
+
found.set(`${stack}.${name}`, { source: stack, ref: `${stack}.${name}`, publish: name === "publish" || name.startsWith("publish."), value: (o) => resolvePath(o[stack], name) });
|
|
375
418
|
return;
|
|
376
419
|
}
|
|
377
420
|
Object.values(value as Record<string, unknown>).forEach(walk);
|
|
378
421
|
}
|
|
379
422
|
};
|
|
380
423
|
walk(input);
|
|
381
|
-
return [...found].sort();
|
|
424
|
+
return [...found.values()].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The cross-component references in `input` that resolve to another value
|
|
429
|
+
* against `now` than against `read`, sorted (#3999). A pull request's apply
|
|
430
|
+
* holds back a member whose plan read `read` when its upstream applied and
|
|
431
|
+
* left `now`.
|
|
432
|
+
*/
|
|
433
|
+
export function movedWiringReferences(
|
|
434
|
+
input: Record<string, unknown>,
|
|
435
|
+
read: Record<string, Record<string, unknown>>,
|
|
436
|
+
now: Record<string, Record<string, unknown>>,
|
|
437
|
+
): WiringReference[] {
|
|
438
|
+
const same = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
439
|
+
return wiringReferences(input).filter((r) => !same(r.value(read), r.value(now))).map(({ source, ref, publish }) => ({ source, ref, publish }));
|
|
382
440
|
}
|
|
383
441
|
|
|
384
442
|
// ── Step + phase execution ───────────────────────────────────────────────
|
|
@@ -501,6 +559,7 @@ async function runCapabilityStep(
|
|
|
501
559
|
status: "fail",
|
|
502
560
|
durationMs: Date.now() - start,
|
|
503
561
|
error: errMessage(err),
|
|
562
|
+
...(isPlanMovedError(err) ? { planMoved: true as const } : {}),
|
|
504
563
|
},
|
|
505
564
|
resolvedInput,
|
|
506
565
|
};
|
|
@@ -795,6 +854,13 @@ async function rollbackExecuted(
|
|
|
795
854
|
|
|
796
855
|
// ── Per-component deploy ─────────────────────────────────────────────────
|
|
797
856
|
|
|
857
|
+
/** Whether a component's composition has a gate step anywhere, nested phases and `rollback` included. */
|
|
858
|
+
function hasGateStep(component: DriverComponent): boolean {
|
|
859
|
+
const walk = (steps: DriverPhase["steps"]): boolean =>
|
|
860
|
+
steps.some((step) => isGateStep(step) || (Array.isArray((step as DriverPhase).steps) && walk((step as DriverPhase).steps)));
|
|
861
|
+
return [...component.deploy, ...(component.rollback ?? [])].some((p) => walk(p.steps));
|
|
862
|
+
}
|
|
863
|
+
|
|
798
864
|
/**
|
|
799
865
|
* Run one component's `deploy` composition. On terminal failure: unwind every
|
|
800
866
|
* executed step in reverse via its capability's `rollback` (saga
|
|
@@ -842,6 +908,9 @@ export async function runComponentDeploy(
|
|
|
842
908
|
component,
|
|
843
909
|
...(ctx.vars ? { vars: ctx.vars } : {}),
|
|
844
910
|
...(release !== undefined ? { release } : {}),
|
|
911
|
+
// #4155: the files the steps deploy from, read where the steps read
|
|
912
|
+
// them. Only a gate reads the digest, so a run with none skips the hashing.
|
|
913
|
+
...(hasGateStep(component) ? { inputs: componentDeployInputs(component.deploy, process.cwd()).inputs } : {}),
|
|
845
914
|
}),
|
|
846
915
|
};
|
|
847
916
|
const records: DriverStepRecord[] = [];
|
|
@@ -995,6 +1064,20 @@ function findStackOutputs(
|
|
|
995
1064
|
return found;
|
|
996
1065
|
}
|
|
997
1066
|
|
|
1067
|
+
/** Whether a step in `results` refused a plan that moved since it was approved (#3997). */
|
|
1068
|
+
export function planMovedIn(results: readonly DriverComponentResult[]): boolean {
|
|
1069
|
+
return results.some((r) => r.records.some((s) => s.planMoved === true));
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* The exit code of a run that failed: {@link PLAN_MOVED_EXIT_CODE} when a
|
|
1074
|
+
* step refused a plan that moved since it was approved, 1 otherwise. Every
|
|
1075
|
+
* command that runs capability steps ends a failed run with it.
|
|
1076
|
+
*/
|
|
1077
|
+
export function failedRunExitCode(results: readonly DriverComponentResult[]): number {
|
|
1078
|
+
return planMovedIn(results) ? PLAN_MOVED_EXIT_CODE : 1;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
998
1081
|
function errMessage(err: unknown): string {
|
|
999
1082
|
return err instanceof Error ? err.message : String(err);
|
|
1000
1083
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
import { existsSync, readFileSync } from "node:fs";
|
|
39
39
|
import { readWaveRecords, writeWaveRecordFile, type WaveRecord } from "../wave-engine";
|
|
40
40
|
import type { ChangeSetDocument } from "../change-set";
|
|
41
|
-
import type { PrMember } from "../pr-loop";
|
|
41
|
+
import type { PrMember, PrWave } from "../pr-loop";
|
|
42
42
|
|
|
43
43
|
/** What a pull request's apply approved, kept so a re-run can finish it (#3464). */
|
|
44
44
|
export interface PrApplyRecord {
|
|
@@ -59,6 +59,11 @@ export interface PrApplyRecord {
|
|
|
59
59
|
* against them again, so an unchanged member plans to the same digest.
|
|
60
60
|
*/
|
|
61
61
|
planOutputs: Record<string, Record<string, unknown>>;
|
|
62
|
+
/**
|
|
63
|
+
* With `--wave-gate` (#4003): each wave reached so far and the approval it
|
|
64
|
+
* applied or waits under, so the next wave's job reports every wave.
|
|
65
|
+
*/
|
|
66
|
+
waves?: PrWave[];
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export interface FanOutAttempt {
|
|
@@ -100,6 +105,7 @@ function readPrApplyRecord(value: unknown): PrApplyRecord | undefined {
|
|
|
100
105
|
if (isRecord(planOutputs)) {
|
|
101
106
|
for (const [name, v] of Object.entries(planOutputs)) if (isRecord(v)) outputs[name] = v;
|
|
102
107
|
}
|
|
108
|
+
const waves = Array.isArray(value.waves) ? value.waves.map(readPrWave).filter((w): w is PrWave => w !== undefined) : [];
|
|
103
109
|
return {
|
|
104
110
|
op,
|
|
105
111
|
gate,
|
|
@@ -109,6 +115,28 @@ function readPrApplyRecord(value: unknown): PrApplyRecord | undefined {
|
|
|
109
115
|
members: members as unknown as PrMember[],
|
|
110
116
|
changeSet: changeSet as unknown as ChangeSetDocument,
|
|
111
117
|
planOutputs: outputs,
|
|
118
|
+
...(waves.length > 0 ? { waves } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const WAVE_STATUSES: ReadonlySet<string> = new Set(["applied", "failed", "plan-failed", "waiting"]);
|
|
123
|
+
|
|
124
|
+
/** One `prApply.waves` entry, or `undefined` when it is malformed. */
|
|
125
|
+
function readPrWave(value: unknown): PrWave | undefined {
|
|
126
|
+
if (!isRecord(value)) return undefined;
|
|
127
|
+
const { wave, components, status, approval, gate, digest } = value;
|
|
128
|
+
if (typeof wave !== "number" || !isStringArray(components) || typeof status !== "string" || !WAVE_STATUSES.has(status)) return undefined;
|
|
129
|
+
if ((approval !== "pull-request" && approval !== "wave") || typeof gate !== "string" || typeof digest !== "string") return undefined;
|
|
130
|
+
return {
|
|
131
|
+
wave,
|
|
132
|
+
components,
|
|
133
|
+
status: status as PrWave["status"],
|
|
134
|
+
approval,
|
|
135
|
+
gate,
|
|
136
|
+
digest,
|
|
137
|
+
...(isStringArray(value.approvedBy) ? { approvedBy: value.approvedBy } : {}),
|
|
138
|
+
...(typeof value.approved === "string" ? { approved: value.approved } : {}),
|
|
139
|
+
...(isStringArray(value.moved) ? { moved: value.moved } : {}),
|
|
112
140
|
};
|
|
113
141
|
}
|
|
114
142
|
|
|
@@ -209,3 +209,38 @@ describe("one approval over the whole set", () => {
|
|
|
209
209
|
expect(ran.sort()).toEqual(["app-one", "app-three", "app-two", "cluster-b"]);
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
|
+
|
|
213
|
+
describe("holding a component back (#3999)", () => {
|
|
214
|
+
test("a held component and what is downstream of it do not run and settle waiting; independent branches run", async () => {
|
|
215
|
+
const plan = planFanOut({ components: ESTATE, changed: ["net"] });
|
|
216
|
+
const { registry, ran } = registryWith();
|
|
217
|
+
const events: Array<{ type: string; component?: string; status?: string }> = [];
|
|
218
|
+
const asked: Array<[string, string[]]> = [];
|
|
219
|
+
const result = await runFanOut(plan, ESTATE, registry, {
|
|
220
|
+
...opts(),
|
|
221
|
+
onRunEvent: (e) => events.push(e as never),
|
|
222
|
+
hold: (name, outputs) => {
|
|
223
|
+
asked.push([name, Object.keys(outputs).sort()]);
|
|
224
|
+
return name === "cluster-a" ? "its plan read net.id before that output applied as another value" : undefined;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
expect(result.status).toBe("ok");
|
|
228
|
+
expect(ran).not.toContain("cluster-a");
|
|
229
|
+
expect(ran).not.toContain("app-one");
|
|
230
|
+
expect(ran).toContain("app-three");
|
|
231
|
+
expect(result.held).toEqual([
|
|
232
|
+
{ component: "cluster-a", reason: "its plan read net.id before that output applied as another value" },
|
|
233
|
+
{ component: "app-one", reason: "its plan read net.id before that output applied as another value", heldBy: "cluster-a" },
|
|
234
|
+
{ component: "app-two", reason: "its plan read net.id before that output applied as another value", heldBy: "cluster-a" },
|
|
235
|
+
].sort((a, b) => plan.order.indexOf(a.component) - plan.order.indexOf(b.component)));
|
|
236
|
+
// Asked once per component as its wave starts, never for one already held.
|
|
237
|
+
expect(asked.map(([name]) => name).sort()).toEqual(["app-three", "cluster-a", "cluster-b", "net"]);
|
|
238
|
+
expect(events.filter((e) => e.type === "component-settle" && e.status === "waiting").map((e) => e.component).sort()).toEqual(["app-one", "app-two", "cluster-a"]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("with no hook, or a hook that holds nothing, the result has no held list", async () => {
|
|
242
|
+
const plan = planFanOut({ components: ESTATE, changed: ["net"] });
|
|
243
|
+
expect((await runFanOut(plan, ESTATE, registryWith().registry, opts())).held).toBeUndefined();
|
|
244
|
+
expect((await runFanOut(plan, ESTATE, registryWith().registry, { ...opts(), hold: () => undefined })).held).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -205,6 +205,25 @@ export interface FanOutRunOptions {
|
|
|
205
205
|
* the returned result cannot do.
|
|
206
206
|
*/
|
|
207
207
|
onComponentSettled?: (result: DriverComponentResult, outputs: Record<string, unknown> | undefined) => void;
|
|
208
|
+
/**
|
|
209
|
+
* Asked as each component's wave starts, with the outputs standing then
|
|
210
|
+
* (#3999). A reason holds the component back: it does not run, and nothing
|
|
211
|
+
* downstream of it runs either; each is listed in `held` and settles
|
|
212
|
+
* `waiting`. A pull request's apply holds a member whose plan read an
|
|
213
|
+
* upstream output that applied as another value, or that was known only
|
|
214
|
+
* after apply. Not with `waveGate`, which plans each wave after the one
|
|
215
|
+
* before it applied.
|
|
216
|
+
*/
|
|
217
|
+
hold?: (component: string, componentOutputs: Record<string, Record<string, unknown>>) => string | undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A component {@link FanOutRunOptions.hold} held back, or one downstream of it. */
|
|
221
|
+
export interface FanOutHeld {
|
|
222
|
+
component: string;
|
|
223
|
+
/** Why the hook held it, or, downstream, why it held the component named in `heldBy`. */
|
|
224
|
+
reason: string;
|
|
225
|
+
/** Downstream of a held component: that component. */
|
|
226
|
+
heldBy?: string;
|
|
208
227
|
}
|
|
209
228
|
|
|
210
229
|
export interface FanOutRunResult {
|
|
@@ -223,6 +242,8 @@ export interface FanOutRunResult {
|
|
|
223
242
|
gate?: PendingGateRecord;
|
|
224
243
|
/** With `waveGate`: each wave this attempt planned, in order, the one it stopped at included. */
|
|
225
244
|
waves?: WaveRecord[];
|
|
245
|
+
/** What {@link FanOutRunOptions.hold} held back, in plan order. Absent when nothing was. */
|
|
246
|
+
held?: FanOutHeld[];
|
|
226
247
|
}
|
|
227
248
|
|
|
228
249
|
/**
|
|
@@ -296,9 +317,21 @@ export async function runFanOut(
|
|
|
296
317
|
const completed: string[] = [];
|
|
297
318
|
const failed: string[] = [];
|
|
298
319
|
const blockedBy = new Map<string, string>();
|
|
320
|
+
const held = new Map<string, FanOutHeld>();
|
|
299
321
|
let gatedFact: PendingGateRecord | undefined;
|
|
300
322
|
|
|
301
323
|
const inPlan = new Set(active.order);
|
|
324
|
+
const waveOf = waveNumbers(active.waves);
|
|
325
|
+
/** Hold `root` and everything downstream of it that has not settled otherwise. */
|
|
326
|
+
const markHeld = (root: string, reason: string): void => {
|
|
327
|
+
held.set(root, { component: root, reason });
|
|
328
|
+
emit?.({ type: "component-settle", wave: waveOf.get(root)!, component: root, status: "waiting" });
|
|
329
|
+
for (const [component] of downstreamWithin(components, inPlan, [root])) {
|
|
330
|
+
if (held.has(component) || blockedBy.has(component) || failed.includes(component)) continue;
|
|
331
|
+
held.set(component, { component, reason, heldBy: root });
|
|
332
|
+
emit?.({ type: "component-settle", wave: waveOf.get(component)!, component, status: "waiting" });
|
|
333
|
+
}
|
|
334
|
+
};
|
|
302
335
|
/** Skip everything downstream of `root`, blaming `root` rather than the nearest edge. */
|
|
303
336
|
const markBlocked = (root: string): void => {
|
|
304
337
|
for (const [component, by] of downstreamWithin(components, inPlan, [root])) {
|
|
@@ -310,7 +343,12 @@ export async function runFanOut(
|
|
|
310
343
|
for (const [waveIndex, wave] of active.waves.entries()) {
|
|
311
344
|
const waveNum = waveIndex + 1;
|
|
312
345
|
// A component whose upstream failed in an earlier wave is not dispatched.
|
|
313
|
-
const
|
|
346
|
+
const candidates = wave.filter((name) => inPlan.has(name) && !blockedBy.has(name) && !held.has(name));
|
|
347
|
+
for (const name of candidates) {
|
|
348
|
+
const reason = options.hold?.(name, componentOutputs);
|
|
349
|
+
if (reason !== undefined) markHeld(name, reason);
|
|
350
|
+
}
|
|
351
|
+
const runnable = candidates.filter((name) => !held.has(name));
|
|
314
352
|
if (runnable.length === 0) continue;
|
|
315
353
|
|
|
316
354
|
emit?.({ type: "wave-start", wave: waveNum, components: runnable });
|
|
@@ -384,6 +422,7 @@ export async function runFanOut(
|
|
|
384
422
|
blocked,
|
|
385
423
|
componentOutputs,
|
|
386
424
|
...(gatedFact ? { gate: gatedFact } : {}),
|
|
425
|
+
...(held.size > 0 ? { held: active.order.filter((name) => held.has(name)).map((name) => held.get(name)!) } : {}),
|
|
387
426
|
};
|
|
388
427
|
}
|
|
389
428
|
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* its publish output against a recorded digest that is not in the
|
|
18
18
|
* composition, so it passes that digest here.
|
|
19
19
|
* - `vars`, the environment config the caller resolved ahead of the run.
|
|
20
|
+
* - The content digests of the local files the steps deploy from (#4155,
|
|
21
|
+
* ./deploy-inputs.ts): a chart, a manifest, a site's tree, a build step's
|
|
22
|
+
* sources. A file changed after review moves the digest. The key is left
|
|
23
|
+
* out when no step names an existing file, so a composition that deploys
|
|
24
|
+
* none keeps the digest it had before #4155.
|
|
20
25
|
*
|
|
21
26
|
* ## What it leaves out
|
|
22
27
|
*
|
|
@@ -28,6 +33,7 @@
|
|
|
28
33
|
*/
|
|
29
34
|
|
|
30
35
|
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
36
|
+
import type { DeployInputDigest } from "./deploy-inputs";
|
|
31
37
|
import type { DriverComponent } from "./driver";
|
|
32
38
|
|
|
33
39
|
/** What a component gate's plan digest is taken over. */
|
|
@@ -37,6 +43,8 @@ export interface ComponentGatePlan {
|
|
|
37
43
|
vars?: Record<string, unknown>;
|
|
38
44
|
/** The recorded digest a promote or rollback deploys, when the caller has one. */
|
|
39
45
|
release?: string;
|
|
46
|
+
/** What the steps deploy from (`componentDeployInputs`), when the caller read it. */
|
|
47
|
+
inputs?: readonly DeployInputDigest[];
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/** The plan digest a component gate records and approves. */
|
|
@@ -49,5 +57,6 @@ export function componentPlanDigest(plan: ComponentGatePlan): string {
|
|
|
49
57
|
rollback: component.rollback ?? null,
|
|
50
58
|
vars: plan.vars ?? null,
|
|
51
59
|
release: plan.release ?? null,
|
|
60
|
+
...(plan.inputs && plan.inputs.length > 0 ? { inputs: plan.inputs } : {}),
|
|
52
61
|
});
|
|
53
62
|
}
|