@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
package/src/cli/handlers/pr.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* chant components pr-plan --base <ref> --pr <n> --env <env> [--forge github|gitlab|forgejo]
|
|
7
7
|
* chant approve pr-<n> pr-apply --plan <digest> --approver github:<login> --sign
|
|
8
8
|
* chant components pr-apply --base <ref> --env <env> [--pr <n>] [--forge <kind>] [--require-review] [--resume <file>]
|
|
9
|
+
* [--wave-gate <gate> [--wave <k>]]
|
|
9
10
|
* ```
|
|
10
11
|
*
|
|
11
12
|
* Both measure the change from the merge base of `--base` and `HEAD`, select
|
|
@@ -27,6 +28,13 @@
|
|
|
27
28
|
* approval still stands, applies the rest without a fresh approval. When
|
|
28
29
|
* they are not covered it says why and runs as it would without a record.
|
|
29
30
|
*
|
|
31
|
+
* `--wave-gate <gate>` (#4003) applies the approved set a wave at a time,
|
|
32
|
+
* each wave planned after the one before it applied (`prApplyWaves`). A
|
|
33
|
+
* wave whose plan the approved change set covers applies under the pull
|
|
34
|
+
* request's approval; one whose plan moved waits at `<gate>-wave-<k>` under
|
|
35
|
+
* the pull request's op (exit 3). `--wave <k>` runs one wave, reading the
|
|
36
|
+
* approved set and the waves that applied from the `--resume` record.
|
|
37
|
+
*
|
|
30
38
|
* Every run writes `pr-plan.json` or `pr-apply.json` (`pr-report.schema.json`),
|
|
31
39
|
* `change-set.json` and `pr-note.md` under `--output` (default `.chant/pr`). *
|
|
32
40
|
* `--events-json` (#4041, #4007) streams the stage's run events to stdout,
|
|
@@ -48,6 +56,12 @@
|
|
|
48
56
|
* recorded under `pr-<n>-<member>`, and the note and the statuses carry the
|
|
49
57
|
* member's name, so each member's loop on one pull request stands apart.
|
|
50
58
|
* The name must be the member that owns the current directory.
|
|
59
|
+
*
|
|
60
|
+
* `--no-status` (#4161) posts neither the stage's status nor its note: the
|
|
61
|
+
* report and the note are written under `--output` only. `chant ci run
|
|
62
|
+
* --local` passes it. pr-plan then builds no forge client, since posting is
|
|
63
|
+
* all it uses one for; pr-apply still reads the pull request and its reviews
|
|
64
|
+
* through `--forge`.
|
|
51
65
|
*/
|
|
52
66
|
|
|
53
67
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -58,14 +72,19 @@ import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
|
|
|
58
72
|
import {
|
|
59
73
|
applyPrSet,
|
|
60
74
|
approvalOf,
|
|
75
|
+
approversWhoAuthored,
|
|
61
76
|
approversWithoutReview,
|
|
62
77
|
decidePrGate,
|
|
63
78
|
planPrSet,
|
|
64
79
|
prReport,
|
|
65
80
|
readOnlyLedger,
|
|
81
|
+
prWavePlan,
|
|
66
82
|
resumePrSet,
|
|
83
|
+
waveNotCovered,
|
|
67
84
|
type PrPlanSet,
|
|
68
85
|
} from "../../components/pr-run";
|
|
86
|
+
import { composeChangeSet, verifyChangeSetDigest } from "../../change-set";
|
|
87
|
+
import { waveGateName } from "../../gated-waves";
|
|
69
88
|
import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt, type PrApplyRecord } from "../../components/fan-out-record";
|
|
70
89
|
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
|
|
71
90
|
import { samePlanDigest } from "../../lifecycle/plan-digest";
|
|
@@ -85,18 +104,22 @@ import {
|
|
|
85
104
|
prStatusState,
|
|
86
105
|
renderPrNote,
|
|
87
106
|
type PrApproval,
|
|
107
|
+
type PrMember,
|
|
88
108
|
type PrReport,
|
|
109
|
+
type PrWave,
|
|
110
|
+
type PrRound,
|
|
89
111
|
} from "../../pr-loop";
|
|
90
112
|
import { FORGE_KINDS, forgeFromEnv, forgePrincipalOf, type ForgeKind, type PrForge } from "../../pr-forge";
|
|
91
113
|
import { GITHUB_COMMENT_LIMIT, GITLAB_NOTE_LIMIT } from "../../plan-summary";
|
|
92
114
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
93
115
|
import { formatError, formatInfo, formatSuccess, formatWarning } from "../format";
|
|
116
|
+
import { reportUninstalledDependencies } from "../dependencies";
|
|
94
117
|
import { findWorkspaceRoot } from "../../project-root";
|
|
95
118
|
import { resolveChangedUnits } from "./fan-out";
|
|
96
119
|
import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run";
|
|
97
120
|
import type { CommandContext } from "../registry";
|
|
98
121
|
import type { ComponentChangeSignal, FanOutPlan } from "../../components/fan-out";
|
|
99
|
-
import type
|
|
122
|
+
import { failedRunExitCode, type DriverComponent } from "../../components/driver";
|
|
100
123
|
import type { CapabilityRegistry } from "../../components/capability";
|
|
101
124
|
|
|
102
125
|
|
|
@@ -130,7 +153,7 @@ const fail = (message: string, hint?: string): number => {
|
|
|
130
153
|
};
|
|
131
154
|
|
|
132
155
|
/** Everything both stages share: the change and the derivation. Each stage plans the members itself. */
|
|
133
|
-
async function prepare(ctx: CommandContext, stage: "plan" | "apply"): Promise<Prepared | number> {
|
|
156
|
+
async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "record"): Promise<Prepared | number> {
|
|
134
157
|
const { args } = ctx;
|
|
135
158
|
const projectPath = resolve(".");
|
|
136
159
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
@@ -161,7 +184,7 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply"): Promise<Pr
|
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
let forge: PrForge | undefined;
|
|
164
|
-
if (args.forge) {
|
|
187
|
+
if (args.forge && !(stage === "plan" && args.noStatus)) {
|
|
165
188
|
try {
|
|
166
189
|
forge = forgeFromEnv(args.forge as ForgeKind);
|
|
167
190
|
} catch (err) {
|
|
@@ -191,9 +214,15 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply"): Promise<Pr
|
|
|
191
214
|
config,
|
|
192
215
|
});
|
|
193
216
|
} catch (err) {
|
|
194
|
-
|
|
217
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
218
|
+
reportUninstalledDependencies([message], projectPath); // #4116
|
|
219
|
+
return fail(message);
|
|
220
|
+
}
|
|
221
|
+
if (!derived.success) {
|
|
222
|
+
// #4116: component files that import packages the project has not installed.
|
|
223
|
+
if (derived.error) reportUninstalledDependencies([derived.error], projectPath);
|
|
224
|
+
return fail(derived.error ?? "Could not discover components");
|
|
195
225
|
}
|
|
196
|
-
if (!derived.success) return fail(derived.error ?? "Could not discover components");
|
|
197
226
|
|
|
198
227
|
const seededOutputs: Record<string, Record<string, unknown>> = {};
|
|
199
228
|
for (const file of args.seedOutputs ?? []) {
|
|
@@ -268,9 +297,11 @@ function settleOf(status: PrReport["status"]): RunEventSettleStatus {
|
|
|
268
297
|
switch (status) {
|
|
269
298
|
case "nothing":
|
|
270
299
|
case "planned":
|
|
300
|
+
case "recorded":
|
|
271
301
|
case "applied":
|
|
272
302
|
return "ok";
|
|
273
303
|
case "refused":
|
|
304
|
+
case "waiting":
|
|
274
305
|
return "gated";
|
|
275
306
|
default:
|
|
276
307
|
return "failed";
|
|
@@ -334,7 +365,7 @@ function printMembers(report: PrReport): void {
|
|
|
334
365
|
async function finish(ctx: CommandContext, p: Prepared, report: PrReport, upload?: PrReportUpload): Promise<void> {
|
|
335
366
|
const note = noteFor(report, p.forge);
|
|
336
367
|
writeOutputs(ctx, report, note, upload);
|
|
337
|
-
await publish(p.forge, report, note);
|
|
368
|
+
if (!ctx.args.noStatus) await publish(p.forge, report, note);
|
|
338
369
|
if (ctx.args.json) console.log(JSON.stringify(report, null, 2));
|
|
339
370
|
}
|
|
340
371
|
|
|
@@ -375,7 +406,7 @@ async function withStageEvents(stage: string, ctx: CommandContext, run: (events:
|
|
|
375
406
|
* Open the stage's `--report-url` upload under its pull request and commit,
|
|
376
407
|
* when the flag was given. Its events are the lines `openCliRunEvents` writes.
|
|
377
408
|
*/
|
|
378
|
-
function openUpload(events: StageEvents, stage: "plan" | "apply", pr: number | null, p: Prepared): PrReportUpload | undefined {
|
|
409
|
+
function openUpload(events: StageEvents, stage: "plan" | "apply" | "record", pr: number | null, p: Prepared): PrReportUpload | undefined {
|
|
379
410
|
if (!events.target) return undefined;
|
|
380
411
|
events.upload = openPrReportUpload({
|
|
381
412
|
target: events.target,
|
|
@@ -432,6 +463,7 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
432
463
|
set,
|
|
433
464
|
approval,
|
|
434
465
|
status,
|
|
466
|
+
...(ctx.args.waveGate ? { waveGate: ctx.args.waveGate } : {}),
|
|
435
467
|
});
|
|
436
468
|
await finish(ctx, p, report, upload);
|
|
437
469
|
// The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
|
|
@@ -453,8 +485,15 @@ export function runComponentsPrApply(ctx: CommandContext): Promise<number> {
|
|
|
453
485
|
}
|
|
454
486
|
|
|
455
487
|
async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
488
|
+
// #4168: the review-rules security group turns --require-review on, read from the declaration at base.
|
|
489
|
+
const { securityGroupsAt } = await import("../../workspace/security");
|
|
490
|
+
const reviewRules = !ctx.args.requireReview && securityGroupsAt(process.cwd(), ctx.args.base).has("review-rules");
|
|
491
|
+
if (reviewRules && !ctx.args.forge) return fail("the workspace opts into review-rules, which reads the pull request's reviews, so pr-apply needs --forge <kind>");
|
|
492
|
+
if (reviewRules) ctx = { ...ctx, args: { ...ctx.args, requireReview: true } };
|
|
456
493
|
const { args } = ctx;
|
|
457
494
|
if (args.requireReview && !args.forge) return fail("--require-review reads the pull request's reviews, so it needs --forge <kind>");
|
|
495
|
+
if (args.wave !== undefined && !args.waveGate) return fail("--wave runs one wave of a --wave-gate apply, so it needs --wave-gate <name>");
|
|
496
|
+
if (args.wave !== undefined && args.wave < 1) return fail(`--wave counts from 1, not ${args.wave}`);
|
|
458
497
|
const p = await prepare(ctx, "apply");
|
|
459
498
|
if (typeof p === "number") return p;
|
|
460
499
|
|
|
@@ -474,6 +513,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
474
513
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
|
|
475
514
|
stage.stream = events;
|
|
476
515
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
516
|
+
if (args.waveGate) return prApplyWaves(ctx, p, { pr, op, release, upload, events });
|
|
477
517
|
|
|
478
518
|
// `--resume` (#3464): an earlier attempt at this change on this commit
|
|
479
519
|
// applied part of an approved set. When the members left are covered by
|
|
@@ -506,14 +546,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
506
546
|
const set = resumed?.set ?? (await planAll(p, events?.emit));
|
|
507
547
|
const base = { stage: "apply" as const, pr, env: p.env, ...(p.member ? { member: p.member } : {}), base: p.base, head: p.head, op, gate: p.gate, plan: p.plan, signal: p.signal, set };
|
|
508
548
|
|
|
509
|
-
const stop =
|
|
510
|
-
events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-apply ${report.status}` : undefined);
|
|
511
|
-
await finish(ctx, p, report, upload);
|
|
512
|
-
console.error((code === 0 ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-apply: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
|
|
513
|
-
if (report.message) console.error(report.message);
|
|
514
|
-
printMembers(report);
|
|
515
|
-
return code;
|
|
516
|
-
};
|
|
549
|
+
const stop = stopper(ctx, p, events, upload);
|
|
517
550
|
|
|
518
551
|
if (p.plan.order.length === 0) {
|
|
519
552
|
return stop(prReport({ ...base, approval: { status: "pending" }, status: "nothing" }), 0);
|
|
@@ -582,22 +615,12 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
582
615
|
}
|
|
583
616
|
}
|
|
584
617
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
const strangers = approversWithoutReview(approval.approvedBy ?? [], reviewers);
|
|
593
|
-
if (strangers.length > 0) {
|
|
594
|
-
const message =
|
|
595
|
-
`The plan is approved by ${strangers.join(", ")}, who ${strangers.length === 1 ? "has" : "have"} no approving review on #${pr}` +
|
|
596
|
-
` (approving reviews: ${reviewers.join(", ") || "none"}). Nothing was applied. ` +
|
|
597
|
-
`An approval counts when it names the reviewer the way the forge does: ${p.forge.principalOf("<login>")}.`;
|
|
598
|
-
emitReviewStop(events, message);
|
|
599
|
-
return stop(prReport({ ...base, approval, status: "refused", refusal: "not-a-reviewer", message }), GATED_EXIT_CODE);
|
|
600
|
-
}
|
|
618
|
+
const reviewOf = reviewCheck(args, p, pr);
|
|
619
|
+
const review = await reviewOf(approval.approvedBy ?? [], "The plan");
|
|
620
|
+
if (review && "error" in review) return fail(review.error);
|
|
621
|
+
if (review) {
|
|
622
|
+
emitReviewStop(events, review.message);
|
|
623
|
+
return stop(prReport({ ...base, approval, status: "refused", refusal: review.refusal, message: review.message }), GATED_EXIT_CODE);
|
|
601
624
|
}
|
|
602
625
|
|
|
603
626
|
// The record is written before the first component runs and again as each
|
|
@@ -632,12 +655,38 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
632
655
|
};
|
|
633
656
|
recordAttempt();
|
|
634
657
|
|
|
658
|
+
// #3999: a round planned after an upstream applied waits on a gate of its
|
|
659
|
+
// own, under the same identity.gates rule and the same review checks.
|
|
660
|
+
const roundLedger = gitGateLedgerPort();
|
|
661
|
+
const decideRound = async ({ round, gate, set: planned }: { round: number; gate: string; set: PrPlanSet }) => {
|
|
662
|
+
console.error(formatInfo(`round ${round}: ${planned.members.map((m) => m.component).filter((c, i, all) => all.indexOf(c) === i).join(", ")} planned again after an upstream applied, ${planned.doc.digest}`));
|
|
663
|
+
await storeChangeSetForGate(ctx, p, planned.doc);
|
|
664
|
+
const port = {
|
|
665
|
+
...roundLedger,
|
|
666
|
+
...(roundLedger.approvalRule ? { approvalRule: (g: string, at?: { base?: string }) => roundLedger.approvalRule!(g === gate ? p.gate : g, at) } : {}),
|
|
667
|
+
};
|
|
668
|
+
const check = await emitGateStep(events?.emit, { op, gate }, () =>
|
|
669
|
+
decidePrGate(port, { op, gate, digest: planned.doc.digest, description: `pull request #${pr}, round ${round}: planned again after an upstream applied` }),
|
|
670
|
+
);
|
|
671
|
+
const roundApproval = approvalOf(check);
|
|
672
|
+
if (!check.satisfied) return { satisfied: false, approval: roundApproval };
|
|
673
|
+
const refused = await reviewOf(roundApproval.approvedBy ?? [], `Round ${round}'s plan`);
|
|
674
|
+
if (refused) {
|
|
675
|
+
const message = "error" in refused ? refused.error : refused.message;
|
|
676
|
+
emitReviewStop(events, message);
|
|
677
|
+
return { satisfied: false, approval: roundApproval, message };
|
|
678
|
+
}
|
|
679
|
+
return { satisfied: true, approval: roundApproval };
|
|
680
|
+
};
|
|
681
|
+
|
|
635
682
|
const applied = await applyPrSet({
|
|
636
683
|
components: p.components,
|
|
637
684
|
plan: resumed?.plan ?? p.plan,
|
|
638
685
|
registry: p.registry,
|
|
639
686
|
env: p.env,
|
|
640
687
|
set,
|
|
688
|
+
gate: p.gate,
|
|
689
|
+
decideRound,
|
|
641
690
|
...(resumed ? { resumed: prior } : {}),
|
|
642
691
|
// The apply is this run's waves, so the fan-out's own run framing is
|
|
643
692
|
// dropped. The log's lines go to stderr with or without --events-json.
|
|
@@ -668,13 +717,209 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
668
717
|
status: applied.status,
|
|
669
718
|
members: applied.members,
|
|
670
719
|
...(resumed ? { resumed: resumed.completed } : {}),
|
|
720
|
+
...(applied.rounds ? { rounds: applied.rounds } : {}),
|
|
721
|
+
...(applied.status === "waiting" ? { message: waitingMessage(op, applied.rounds) } : {}),
|
|
671
722
|
});
|
|
672
|
-
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
673
|
-
|
|
723
|
+
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
724
|
+
// a person. A step that refused its plan because the target moved since
|
|
725
|
+
// the approval exits 3, as a moved digest does before anything applies (#3997).
|
|
726
|
+
const exit = applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
727
|
+
const code = await stop(report, exit, applied.run.status === "gated" ? "gated" : undefined);
|
|
674
728
|
if (applied.status === "applied") console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
675
729
|
return code;
|
|
676
730
|
}
|
|
677
731
|
|
|
732
|
+
/** chant components pr-record --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
733
|
+
export function runComponentsPrRecord(ctx: CommandContext): Promise<number> {
|
|
734
|
+
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
735
|
+
return withStageEvents("pr-record", ctx, (events) => prRecord(ctx, events));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* The record stage (#4168 step 1): what a merge does unless apply on merge
|
|
740
|
+
* is opted in. It plans the members the change reaches on the merged
|
|
741
|
+
* commit, stores the change set, decides the pull request's gate against
|
|
742
|
+
* it without writing to the ledger, and writes the merge record
|
|
743
|
+
* (`../../lifecycle/merge-record.ts`). It applies nothing and refuses
|
|
744
|
+
* nothing. A plan that differs from the approved one is recorded, and the
|
|
745
|
+
* `chant/record` status fails (decision 1). It exits 1 only when a member
|
|
746
|
+
* failed to plan.
|
|
747
|
+
*/
|
|
748
|
+
async function prRecord(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
749
|
+
const { args } = ctx;
|
|
750
|
+
const p = await prepare(ctx, "record");
|
|
751
|
+
if (typeof p === "number") return p;
|
|
752
|
+
|
|
753
|
+
let pr: number | null = args.pr ?? null;
|
|
754
|
+
if (pr === null && p.forge) {
|
|
755
|
+
try {
|
|
756
|
+
pr = await p.forge.pullRequestFor(p.head);
|
|
757
|
+
} catch (err) {
|
|
758
|
+
console.error(formatWarning({ message: `could not ask ${p.forge.kind} which pull request merged ${p.head}, so the record names none: ${err instanceof Error ? err.message : String(err)}` }));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const op = pr === null ? "pr-unknown" : prOp(pr, p.member);
|
|
762
|
+
const upload = openUpload(stage, "record", pr, p);
|
|
763
|
+
const events = await openCliRunEvents(args, { kind: "component", name: `pr-record ${op}`, cwd: resolve(".") }, upload?.line);
|
|
764
|
+
stage.stream = events;
|
|
765
|
+
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
766
|
+
const set = await planAll(p, events?.emit);
|
|
767
|
+
const nothing = p.plan.order.length === 0;
|
|
768
|
+
|
|
769
|
+
let approval: PrApproval = { status: "pending" };
|
|
770
|
+
if (!nothing && !set.failed) {
|
|
771
|
+
await storeChangeSetForGate(ctx, p, set.doc);
|
|
772
|
+
if (pr !== null) {
|
|
773
|
+
try {
|
|
774
|
+
const check = await emitGateStep(events?.emit, { op, gate: p.gate }, () =>
|
|
775
|
+
decidePrGate(readOnlyLedger(gitGateLedgerPort()), { op, gate: p.gate, digest: set.doc.digest }),
|
|
776
|
+
);
|
|
777
|
+
approval = approvalOf(check);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
console.error(formatWarning({ message: `could not read the gate ledger, so the record says no approval: ${err instanceof Error ? err.message : String(err)}` }));
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// #4000: the record keeps an approval by someone who wrote the change, which review rules would not count.
|
|
785
|
+
let authorApproved: string[] | undefined;
|
|
786
|
+
if (pr !== null && p.forge && approval.status === "approved") {
|
|
787
|
+
try {
|
|
788
|
+
const authors = (await p.forge.authors(pr)).map((login) => p.forge!.principalOf(login));
|
|
789
|
+
authorApproved = approversWhoAuthored(approval.approvedBy ?? [], authors);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
console.error(formatWarning({ message: `could not read the authors of #${pr}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const status = nothing ? "nothing" : set.failed ? "plan-failed" : "recorded";
|
|
796
|
+
const report = prReport({
|
|
797
|
+
stage: "record",
|
|
798
|
+
pr,
|
|
799
|
+
env: p.env,
|
|
800
|
+
...(p.member ? { member: p.member } : {}),
|
|
801
|
+
base: p.base,
|
|
802
|
+
head: p.head,
|
|
803
|
+
op,
|
|
804
|
+
gate: p.gate,
|
|
805
|
+
plan: p.plan,
|
|
806
|
+
signal: p.signal,
|
|
807
|
+
set,
|
|
808
|
+
approval,
|
|
809
|
+
status,
|
|
810
|
+
...(status === "plan-failed" ? { message: "A member failed to plan on the merged commit, so the record has no plan to compare with the approved one. Nothing was applied." } : {}),
|
|
811
|
+
...(status === "recorded" && approval.status === "changed"
|
|
812
|
+
? { message: `${describePlanChanged({ op, gate: p.gate, digest: set.doc.digest, approval })} The merge stands and nothing was applied; the release gate decides before this deploys.` }
|
|
813
|
+
: {}),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const { mergeSourceOf, writeMergeRecord, MERGE_RECORD_SCHEMA_ID, MERGE_RECORD_CONTRACT } = await import("../../lifecycle/merge-record");
|
|
817
|
+
const { pushLifecycleReport } = await import("../../lifecycle/git");
|
|
818
|
+
try {
|
|
819
|
+
const written = await writeMergeRecord({
|
|
820
|
+
$schema: MERGE_RECORD_SCHEMA_ID,
|
|
821
|
+
contract: MERGE_RECORD_CONTRACT,
|
|
822
|
+
commit: p.head,
|
|
823
|
+
base: p.base,
|
|
824
|
+
env: p.env,
|
|
825
|
+
...(p.member ? { member: p.member } : {}),
|
|
826
|
+
source: mergeSourceOf(resolve("."), p.head, pr),
|
|
827
|
+
op: pr === null ? null : op,
|
|
828
|
+
gate: p.gate,
|
|
829
|
+
digest: nothing ? null : set.doc.digest,
|
|
830
|
+
plan: nothing ? "nothing" : set.failed ? "plan-failed" : approval.status === "approved" ? "approved" : approval.status === "changed" ? "changed" : "unapproved",
|
|
831
|
+
...(approval.status === "changed" && approval.approved ? { approvedDigest: approval.approved } : {}),
|
|
832
|
+
approvedBy: approval.status === "approved" ? (approval.approvedBy ?? []) : [],
|
|
833
|
+
...(authorApproved && authorApproved.length > 0 ? { authorApproved } : {}),
|
|
834
|
+
components: p.plan.order,
|
|
835
|
+
recordedAt: new Date().toISOString(),
|
|
836
|
+
});
|
|
837
|
+
if (written.written) {
|
|
838
|
+
const pushed = await pushLifecycleReport();
|
|
839
|
+
if (pushed.pushWarning) console.error(formatWarning({ message: pushed.pushWarning }));
|
|
840
|
+
}
|
|
841
|
+
} catch (err) {
|
|
842
|
+
console.error(formatWarning({ message: `could not write the merge record for ${p.head}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const stop = stopper(ctx, p, events, upload);
|
|
846
|
+
return stop(report, status === "plan-failed" ? 1 : 0);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** How a stage stops: settle its events, write and publish the report, say how it ended, and return `code`. */
|
|
850
|
+
function stopper(ctx: CommandContext, p: Prepared, events: RunEventStream | undefined, upload: PrReportUpload | undefined) {
|
|
851
|
+
return async (report: PrReport, code: number, settle?: RunEventSettleStatus): Promise<number> => {
|
|
852
|
+
events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-${report.stage} ${report.status}` : undefined);
|
|
853
|
+
await finish(ctx, p, report, upload);
|
|
854
|
+
console.error((code === 0 ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-${report.stage}: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
|
|
855
|
+
if (report.message) console.error(report.message);
|
|
856
|
+
printMembers(report);
|
|
857
|
+
return code;
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Why `--require-review` does not count an approval, or why the reviews could not be read. */
|
|
862
|
+
type ReviewStop = { refusal: "author-approved" | "not-a-reviewer"; message: string } | { error: string } | undefined;
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* The `--require-review` check (#3183, #4000) of an approval's approvers,
|
|
866
|
+
* `what` naming the plan they approved. The pull request's reviews and
|
|
867
|
+
* authors are read once, at the first call. Without `--require-review` every
|
|
868
|
+
* approval counts.
|
|
869
|
+
*/
|
|
870
|
+
function reviewCheck(args: CommandContext["args"], p: Prepared, pr: number): (approvedBy: readonly string[], what: string) => Promise<ReviewStop> {
|
|
871
|
+
let read: Promise<{ reviewers: string[]; authors: string[] }> | undefined;
|
|
872
|
+
return async (approvedBy, what) => {
|
|
873
|
+
const forge = p.forge;
|
|
874
|
+
if (!args.requireReview || !forge) return undefined;
|
|
875
|
+
read ??= (async () => ({
|
|
876
|
+
reviewers: (await forge.approvers(pr)).map((login) => forge.principalOf(login)),
|
|
877
|
+
authors: (await forge.authors(pr)).map((login) => forge.principalOf(login)),
|
|
878
|
+
}))();
|
|
879
|
+
let reviewers: string[];
|
|
880
|
+
let authors: string[];
|
|
881
|
+
try {
|
|
882
|
+
({ reviewers, authors } = await read);
|
|
883
|
+
} catch (err) {
|
|
884
|
+
return { error: `could not read the reviews and authors of #${pr}: ${err instanceof Error ? err.message : String(err)}` };
|
|
885
|
+
}
|
|
886
|
+
// #4000: whoever wrote the change never counts toward its gate, through a forge review or a ledger line.
|
|
887
|
+
const writers = approversWhoAuthored(approvedBy, authors);
|
|
888
|
+
if (writers.length > 0) {
|
|
889
|
+
return {
|
|
890
|
+
refusal: "author-approved",
|
|
891
|
+
message:
|
|
892
|
+
`${what} is approved by ${writers.join(", ")}, who wrote #${pr} ` +
|
|
893
|
+
`(its author and the authors and committers of its commits: ${authors.join(", ")}). ` +
|
|
894
|
+
`An approval from someone who wrote the change never counts, so nothing was applied. ` +
|
|
895
|
+
`Have someone else approve the pull request and the plan, then run the apply again.`,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const strangers = approversWithoutReview(approvedBy, reviewers);
|
|
899
|
+
if (strangers.length > 0) {
|
|
900
|
+
return {
|
|
901
|
+
refusal: "not-a-reviewer",
|
|
902
|
+
message:
|
|
903
|
+
`${what} is approved by ${strangers.join(", ")}, who ${strangers.length === 1 ? "has" : "have"} no approving review on #${pr}` +
|
|
904
|
+
` (approving reviews: ${reviewers.join(", ") || "none"}). Nothing was applied. ` +
|
|
905
|
+
`An approval counts when it names the reviewer the way the forge does: ${forge.principalOf("<login>")}.`,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
return undefined;
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** What a `waiting` apply prints (#3999): the round's gate and digest to approve, and how to carry on. */
|
|
913
|
+
function waitingMessage(op: string, rounds: PrRound[] | undefined): string {
|
|
914
|
+
const last = rounds?.at(-1);
|
|
915
|
+
if (!last) return "Some members were held back after an upstream applied.";
|
|
916
|
+
return (
|
|
917
|
+
`${last.components.join(", ")} ${last.components.length === 1 ? "was" : "were"} planned again after an upstream applied, ` +
|
|
918
|
+
`and no approval stands for that plan (${last.digest}), so ${last.components.length === 1 ? "it" : "they"} did not apply. ` +
|
|
919
|
+
`Approve it: ${prApproveCommand({ op, gate: last.gate, digest: last.digest })}; then run the apply again with the same --resume record.`
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
678
923
|
/** What a component's settle says in the job log. */
|
|
679
924
|
function settleWords(body: RunEventComponentSettle): string {
|
|
680
925
|
switch (body.status) {
|
|
@@ -809,3 +1054,373 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
|
|
|
809
1054
|
console.error(formatInfo(`resume : ${completed.join(", ")} applied in an earlier attempt; applying ${resumed.plan.order.join(", ") || "nothing more"} under the approval of ${record.digest}`));
|
|
810
1055
|
return { plan: resumed.plan, set: resumed.set, check, approval: approvalOf(check), completed, outputs };
|
|
811
1056
|
}
|
|
1057
|
+
|
|
1058
|
+
// ── A gate per wave on merge (#4003) ─────────────────────────────────────────
|
|
1059
|
+
|
|
1060
|
+
/** What an earlier wave's job left for this one (#4003): the approved set, the pull request's gate decided against it, and the progress that carries. */
|
|
1061
|
+
interface WaveCarry {
|
|
1062
|
+
record: PrApplyRecord;
|
|
1063
|
+
check: GateCheck;
|
|
1064
|
+
approval: PrApproval;
|
|
1065
|
+
completed: string[];
|
|
1066
|
+
outputs: Record<string, Record<string, unknown>>;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Whether `attempt` carries into this wave's job. It does when it was made
|
|
1071
|
+
* by an apply of this pull request's gate, on this commit, for this
|
|
1072
|
+
* derivation; when its change set is the one its digest names; and when the
|
|
1073
|
+
* pull request's approval still stands for that digest. Of the components it
|
|
1074
|
+
* applied, those whose input is the same now carry (#4081). Otherwise it says
|
|
1075
|
+
* why on stderr, and the job runs as it would with no record.
|
|
1076
|
+
*/
|
|
1077
|
+
async function carryWaves(p: Prepared, attempt: FanOutAttempt, file: string, op: string, inputsNow: Record<string, string>): Promise<WaveCarry | undefined> {
|
|
1078
|
+
const skip = (why: string): undefined => {
|
|
1079
|
+
console.error(formatWarning({
|
|
1080
|
+
message: `--resume: "${file}" does not carry: ${why}`,
|
|
1081
|
+
hint: "Wave 1 plans every member again and needs the pull request's approval of that plan; a later wave refuses until wave 1 applied.",
|
|
1082
|
+
}));
|
|
1083
|
+
return undefined;
|
|
1084
|
+
};
|
|
1085
|
+
const record = attempt.prApply;
|
|
1086
|
+
if (!record) return skip("it was not written by pr-apply");
|
|
1087
|
+
if (record.op !== op || record.gate !== p.gate) return skip(`it records ${record.op} ${record.gate}, not ${op} ${p.gate}`);
|
|
1088
|
+
if (record.head !== p.head) return skip(`it records an apply of ${record.head.slice(0, 12)}, not ${p.head.slice(0, 12)}`);
|
|
1089
|
+
if (!samePlanDigest(attempt.digest, p.plan.digest)) return skip("the change selects other components than it did");
|
|
1090
|
+
if (record.changeSet.digest !== record.digest || !verifyChangeSetDigest(record.changeSet)) return skip(`its change set is not the one its digest ${record.digest} names`);
|
|
1091
|
+
const split = progressThatCarries(attempt, inputsNow);
|
|
1092
|
+
if (split.unrecorded && attempt.completed.length > 0) return skip("it does not record what each component applied, since it was written before chant recorded component inputs");
|
|
1093
|
+
for (const name of split.changed) console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
|
|
1094
|
+
let check: GateCheck;
|
|
1095
|
+
try {
|
|
1096
|
+
check = await decidePrGate(readOnlyLedger(gitGateLedgerPort()), { op, gate: p.gate, digest: record.digest });
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
return skip(`could not read the gate ledger: ${err instanceof Error ? err.message : String(err)}`);
|
|
1099
|
+
}
|
|
1100
|
+
if (!check.satisfied) return skip(`no approval stands for the plan it applied (${record.digest}) any more`);
|
|
1101
|
+
const completed = split.carried;
|
|
1102
|
+
const outputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) => completed.includes(name)));
|
|
1103
|
+
return { record, check, approval: approvalOf(check), completed, outputs };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** A run event with its wave renumbered to `wave`: the fan-out that applies one wave numbers it 1. */
|
|
1107
|
+
function inWave(body: RunEventBody, wave: number): RunEventBody {
|
|
1108
|
+
return "wave" in body && typeof (body as { wave?: unknown }).wave === "number" ? ({ ...body, wave } as RunEventBody) : body;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* `pr-apply --wave-gate <gate>` (#4003): apply the pull request's approved
|
|
1113
|
+
* set a wave at a time, each wave planned after the one before it applied.
|
|
1114
|
+
*
|
|
1115
|
+
* The first job plans every member, as pr-apply does, and decides the pull
|
|
1116
|
+
* request's gate against that digest. Approved, wave 1 applies with those
|
|
1117
|
+
* plans. Each later wave is planned again against the outputs the waves
|
|
1118
|
+
* before it left. When the approved change set covers its plan
|
|
1119
|
+
* (`planCoveredBy`, as on a resume) it applies under the pull request's
|
|
1120
|
+
* approval; when it does not, it waits at `<gate>-wave-<k>` under the pull
|
|
1121
|
+
* request's op for an approval of its new plan, and exits 3.
|
|
1122
|
+
*
|
|
1123
|
+
* With `--wave <k>` the job runs wave k and no other, reading the approved
|
|
1124
|
+
* set and the progress from the `--resume` record the job before it left.
|
|
1125
|
+
* Without it the waves run in order until one stops.
|
|
1126
|
+
*/
|
|
1127
|
+
async function prApplyWaves(
|
|
1128
|
+
ctx: CommandContext,
|
|
1129
|
+
p: Prepared,
|
|
1130
|
+
at: { pr: number | null; op: string; release: ReturnType<typeof autoReleaseRun>; upload?: PrReportUpload; events?: RunEventStream },
|
|
1131
|
+
): Promise<number> {
|
|
1132
|
+
const { args } = ctx;
|
|
1133
|
+
const { pr, op, events, upload } = at;
|
|
1134
|
+
const waveGate = args.waveGate!;
|
|
1135
|
+
const only = args.wave;
|
|
1136
|
+
const total = p.plan.waves.length;
|
|
1137
|
+
const stop = stopper(ctx, p, events, upload);
|
|
1138
|
+
const log = applyLog();
|
|
1139
|
+
log({ type: "run-start", waves: p.plan.waves });
|
|
1140
|
+
const reportBase = { stage: "apply" as const, pr, env: p.env, ...(p.member ? { member: p.member } : {}), base: p.base, head: p.head, op, gate: p.gate, plan: p.plan, signal: p.signal, waveGate };
|
|
1141
|
+
|
|
1142
|
+
if (p.plan.order.length === 0 || pr === null) {
|
|
1143
|
+
const set = await planAll(p, events?.emit);
|
|
1144
|
+
if (p.plan.order.length === 0) return stop(prReport({ ...reportBase, set, approval: { status: "pending" }, status: "nothing" }), 0);
|
|
1145
|
+
return stop(
|
|
1146
|
+
prReport({
|
|
1147
|
+
...reportBase,
|
|
1148
|
+
set,
|
|
1149
|
+
approval: { status: "pending" },
|
|
1150
|
+
status: "refused",
|
|
1151
|
+
refusal: "no-pull-request",
|
|
1152
|
+
message: `No pull request merged ${p.head}, so no approval answers for this change. Nothing was applied. Pass --pr <n>, or land the change through a pull request.`,
|
|
1153
|
+
}),
|
|
1154
|
+
GATED_EXIT_CODE,
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const resumePath = args.resume ? resolve(args.resume) : undefined;
|
|
1159
|
+
let attempt: FanOutAttempt | undefined;
|
|
1160
|
+
if (resumePath) {
|
|
1161
|
+
try {
|
|
1162
|
+
attempt = readFanOutAttempt(resumePath);
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
return fail(`--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const inputsNow = resumePath ? componentInputDigests(p.components, { projectRoot: resolve("."), env: p.env }) : {};
|
|
1168
|
+
const carry = attempt ? await carryWaves(p, attempt, args.resume!, op, inputsNow) : undefined;
|
|
1169
|
+
|
|
1170
|
+
const completed = new Set(carry?.completed ?? []);
|
|
1171
|
+
const outputsSoFar: Record<string, Record<string, unknown>> = { ...(carry?.outputs ?? {}) };
|
|
1172
|
+
const inputsSoFar: Record<string, string> = Object.fromEntries([...completed].filter((n) => inputsNow[n] !== undefined).map((n) => [n, inputsNow[n]!]));
|
|
1173
|
+
const failedSoFar = new Set<string>();
|
|
1174
|
+
let waves: PrWave[] = carry?.record.waves ?? [];
|
|
1175
|
+
const after = new Map<string, PrMember>();
|
|
1176
|
+
const leftIn = (k: number): string[] => (p.plan.waves[k - 1] ?? []).filter((name) => !completed.has(name));
|
|
1177
|
+
const firstLeft = (): number => p.plan.waves.findIndex((_, i) => leftIn(i + 1).length > 0) + 1;
|
|
1178
|
+
|
|
1179
|
+
// The approved set: carried from the record, or planned here and decided against the pull request's gate.
|
|
1180
|
+
let approved: PrPlanSet | undefined = carry
|
|
1181
|
+
? { doc: carry.record.changeSet, members: carry.record.members, plans: {}, outputs: carry.record.planOutputs, failed: false }
|
|
1182
|
+
: undefined;
|
|
1183
|
+
let approval: PrApproval = carry?.approval ?? { status: "pending" };
|
|
1184
|
+
const report = (input: { status: PrReport["status"]; refusal?: PrReport["refusal"]; message?: string }) => {
|
|
1185
|
+
const set = approved ?? { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
1186
|
+
const members = set.members.map((m): PrMember => {
|
|
1187
|
+
const now = after.get(m.member);
|
|
1188
|
+
if (now) return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}) };
|
|
1189
|
+
return completed.has(m.component) ? { ...m, status: "applied" } : m;
|
|
1190
|
+
});
|
|
1191
|
+
return prReport({ ...reportBase, set, approval, members, waves, ...input });
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// `--wave <k>` reached before an earlier wave applied: nothing runs, as `fan-out --wave` refuses.
|
|
1195
|
+
const left = firstLeft();
|
|
1196
|
+
if (only !== undefined && left > 0 && left < only) {
|
|
1197
|
+
return stop(
|
|
1198
|
+
report({
|
|
1199
|
+
status: "refused",
|
|
1200
|
+
refusal: "earlier-wave",
|
|
1201
|
+
message:
|
|
1202
|
+
`Wave ${only} cannot run yet: wave ${left} has not applied (${leftIn(left).join(", ")})${carry ? "" : ", and no --resume record of this apply says otherwise"}. ` +
|
|
1203
|
+
`Nothing was applied. Run wave ${left}'s job first, with the same --resume record.`,
|
|
1204
|
+
}),
|
|
1205
|
+
1,
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
let planned: PrPlanSet | undefined;
|
|
1210
|
+
if (carry) {
|
|
1211
|
+
// What the earlier waves applied settles first (#4007), each in its own wave, as a resumed apply's does.
|
|
1212
|
+
p.plan.waves.forEach((names, i) => {
|
|
1213
|
+
for (const component of names.filter((name) => completed.has(name))) {
|
|
1214
|
+
const body: RunEventComponentSettle = { type: "component-settle", wave: i + 1, component, status: "already-applied" };
|
|
1215
|
+
events?.emit(body);
|
|
1216
|
+
log(body);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
emitResumedGate(events, op, p.gate, carry.check);
|
|
1220
|
+
} else {
|
|
1221
|
+
const set = await planAll(p, events?.emit);
|
|
1222
|
+
approved = set;
|
|
1223
|
+
if (set.failed) {
|
|
1224
|
+
return stop(report({ status: "plan-failed", message: "A member failed to plan, so there is no plan to compare with the approved one. Nothing was applied." }), 1);
|
|
1225
|
+
}
|
|
1226
|
+
await storeChangeSetForGate(ctx, p, set.doc);
|
|
1227
|
+
const check = await emitGateStep(
|
|
1228
|
+
events?.emit,
|
|
1229
|
+
{ op, gate: p.gate },
|
|
1230
|
+
() => decidePrGate(gitGateLedgerPort(), { op, gate: p.gate, digest: set.doc.digest, description: `pull request #${pr}: ${p.plan.order.join(", ")}` }),
|
|
1231
|
+
(unapproved) => {
|
|
1232
|
+
const a = approvalOf(unapproved);
|
|
1233
|
+
return a.status === "changed"
|
|
1234
|
+
? describePlanChanged({ op, gate: p.gate, digest: set.doc.digest, approval: a })
|
|
1235
|
+
: `No approval stands for this plan. Approve it: ${prApproveCommand({ op, gate: p.gate, digest: set.doc.digest })}`;
|
|
1236
|
+
},
|
|
1237
|
+
);
|
|
1238
|
+
approval = approvalOf(check);
|
|
1239
|
+
if (!check.satisfied) {
|
|
1240
|
+
return stop(
|
|
1241
|
+
approval.status === "changed"
|
|
1242
|
+
? report({ status: "refused", refusal: "plan-changed", message: describePlanChanged({ op, gate: p.gate, digest: set.doc.digest, approval }) })
|
|
1243
|
+
: report({
|
|
1244
|
+
status: "refused",
|
|
1245
|
+
refusal: "not-approved",
|
|
1246
|
+
message: `No approval stands for this plan, so nothing was applied. Approve it: ${prApproveCommand({ op, gate: p.gate, digest: set.doc.digest })}; then run the apply again.`,
|
|
1247
|
+
}),
|
|
1248
|
+
GATED_EXIT_CODE,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
planned = set;
|
|
1252
|
+
}
|
|
1253
|
+
const approvedSet = approved!;
|
|
1254
|
+
|
|
1255
|
+
const review = reviewCheck(args, p, pr);
|
|
1256
|
+
const prReview = await review(approval.approvedBy ?? [], "The plan");
|
|
1257
|
+
if (prReview && "error" in prReview) return fail(prReview.error);
|
|
1258
|
+
if (prReview) {
|
|
1259
|
+
emitReviewStop(events, prReview.message);
|
|
1260
|
+
return stop(report({ status: "refused", refusal: prReview.refusal, message: prReview.message }), GATED_EXIT_CODE);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const recordAttempt = (): void => {
|
|
1264
|
+
if (!resumePath) return;
|
|
1265
|
+
writeFanOutAttempt(resumePath, {
|
|
1266
|
+
digest: p.plan.digest,
|
|
1267
|
+
completed: [...completed].sort(),
|
|
1268
|
+
failed: [...failedSoFar].sort(),
|
|
1269
|
+
outputs: outputsSoFar,
|
|
1270
|
+
inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
|
|
1271
|
+
prApply: {
|
|
1272
|
+
op,
|
|
1273
|
+
gate: p.gate,
|
|
1274
|
+
head: p.head,
|
|
1275
|
+
digest: approvedSet.doc.digest,
|
|
1276
|
+
approvedBy: approval.approvedBy ?? [],
|
|
1277
|
+
members: approvedSet.members,
|
|
1278
|
+
changeSet: approvedSet.doc,
|
|
1279
|
+
planOutputs: approvedSet.outputs,
|
|
1280
|
+
...(waves.length > 0 ? { waves } : {}),
|
|
1281
|
+
},
|
|
1282
|
+
});
|
|
1283
|
+
};
|
|
1284
|
+
const setWave = (wave: PrWave): void => {
|
|
1285
|
+
waves = [...waves.filter((w) => w.wave !== wave.wave), wave].sort((a, b) => a.wave - b.wave);
|
|
1286
|
+
recordAttempt();
|
|
1287
|
+
};
|
|
1288
|
+
recordAttempt();
|
|
1289
|
+
|
|
1290
|
+
const from = only ?? (left || total + 1);
|
|
1291
|
+
const to = only ?? total;
|
|
1292
|
+
for (let k = from; k <= to; k++) {
|
|
1293
|
+
const wavePlan = prWavePlan(p.plan, p.components, k, [...completed]);
|
|
1294
|
+
if (wavePlan.order.length === 0) continue;
|
|
1295
|
+
const inThisWave = new Set(wavePlan.order);
|
|
1296
|
+
|
|
1297
|
+
// Wave 1 of the apply that just planned every member applies those plans:
|
|
1298
|
+
// they are the approved set's. Every other wave is planned again, against
|
|
1299
|
+
// the outputs the waves before it left.
|
|
1300
|
+
let set: PrPlanSet;
|
|
1301
|
+
let entry: PrWave;
|
|
1302
|
+
if (planned && k === 1) {
|
|
1303
|
+
set = {
|
|
1304
|
+
...planned,
|
|
1305
|
+
members: planned.members.filter((m) => inThisWave.has(m.component)),
|
|
1306
|
+
plans: Object.fromEntries(Object.entries(planned.plans).filter(([name]) => inThisWave.has(name))),
|
|
1307
|
+
};
|
|
1308
|
+
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
|
|
1309
|
+
} else {
|
|
1310
|
+
const notApplied = Object.fromEntries(Object.entries(approvedSet.outputs).filter(([name]) => !completed.has(name)));
|
|
1311
|
+
const fresh = await planPrSet({
|
|
1312
|
+
components: p.components,
|
|
1313
|
+
plan: wavePlan,
|
|
1314
|
+
registry: p.registry,
|
|
1315
|
+
env: p.env,
|
|
1316
|
+
seededOutputs: { ...p.seededOutputs, ...notApplied, ...outputsSoFar },
|
|
1317
|
+
...(events ? { onRunEvent: (body: RunEventBody) => events.emit(body) } : {}),
|
|
1318
|
+
});
|
|
1319
|
+
if (fresh.failed) {
|
|
1320
|
+
for (const m of fresh.members) after.set(m.member, m);
|
|
1321
|
+
setWave({ wave: k, components: wavePlan.order, status: "plan-failed", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest });
|
|
1322
|
+
return stop(report({ status: "plan-failed", message: `A member of wave ${k} failed to plan, so nothing in wave ${k} was applied.` }), 1);
|
|
1323
|
+
}
|
|
1324
|
+
const moved = waveNotCovered(approvedSet.doc, fresh.doc, approvedSet.members);
|
|
1325
|
+
set = fresh;
|
|
1326
|
+
if (moved.length === 0) {
|
|
1327
|
+
console.error(formatInfo(`wave ${k}: the approved plan covers what it plans now, so it applies under the pull request's approval`));
|
|
1328
|
+
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
|
|
1329
|
+
} else {
|
|
1330
|
+
const gate = waveGateName(waveGate, k);
|
|
1331
|
+
const digest = fresh.doc.digest;
|
|
1332
|
+
console.error(formatInfo(`wave ${k}: its plan moved since the pull request's (${moved.join("; ")}), so it needs an approval of ${gate}`));
|
|
1333
|
+
await storeChangeSetForGate(ctx, p, fresh.doc);
|
|
1334
|
+
writeWaveChangeSet(ctx, k, fresh.doc, upload);
|
|
1335
|
+
const approveLine = prApproveCommand({ op, gate, digest });
|
|
1336
|
+
const check = await emitGateStep(
|
|
1337
|
+
events?.emit,
|
|
1338
|
+
{ op, gate },
|
|
1339
|
+
() => decidePrGate(gitGateLedgerPort(), { op, gate, digest, description: `pull request #${pr}, wave ${k} of ${total}: ${wavePlan.order.join(", ")}` }),
|
|
1340
|
+
(unapproved) =>
|
|
1341
|
+
unapproved.mismatch
|
|
1342
|
+
? `wave ${k}'s plan changed after its approval. Approve the plan now: ${approveLine}`
|
|
1343
|
+
: `wave ${k}'s plan is not the one the pull request approved. Approve it: ${approveLine}`,
|
|
1344
|
+
);
|
|
1345
|
+
const waveApproval = approvalOf(check);
|
|
1346
|
+
const waiting: PrWave = {
|
|
1347
|
+
wave: k,
|
|
1348
|
+
components: wavePlan.order,
|
|
1349
|
+
status: "waiting",
|
|
1350
|
+
approval: "wave",
|
|
1351
|
+
gate,
|
|
1352
|
+
digest,
|
|
1353
|
+
moved,
|
|
1354
|
+
...(waveApproval.approved ? { approved: waveApproval.approved } : {}),
|
|
1355
|
+
};
|
|
1356
|
+
if (!check.satisfied) {
|
|
1357
|
+
setWave(waiting);
|
|
1358
|
+
const message =
|
|
1359
|
+
waveApproval.status === "changed"
|
|
1360
|
+
? `Wave ${k}'s plan changed after its approval, so nothing in it was applied. approved: ${waveApproval.approved ?? "(none)"}; planned now: ${digest}. ` +
|
|
1361
|
+
`Read the new plan, and if it is right, approve it: ${approveLine}; then run the wave's apply again.`
|
|
1362
|
+
: `Wave ${k} was planned after the waves before it applied, and the pull request's approved plan does not cover it: ${moved.join("; ")}. ` +
|
|
1363
|
+
`Nothing in wave ${k} was applied. Read its plan, and if it is right, approve it: ${approveLine}; then run the wave's apply again.`;
|
|
1364
|
+
return stop(report({ status: "refused", refusal: waveApproval.status === "changed" ? "wave-plan-changed" : "wave-not-approved", message }), GATED_EXIT_CODE);
|
|
1365
|
+
}
|
|
1366
|
+
const waveReview = await review(waveApproval.approvedBy ?? [], `Wave ${k}'s plan`);
|
|
1367
|
+
if (waveReview && "error" in waveReview) return fail(waveReview.error);
|
|
1368
|
+
if (waveReview) {
|
|
1369
|
+
setWave(waiting);
|
|
1370
|
+
emitReviewStop(events, waveReview.message);
|
|
1371
|
+
return stop(report({ status: "refused", refusal: waveReview.refusal, message: waveReview.message }), GATED_EXIT_CODE);
|
|
1372
|
+
}
|
|
1373
|
+
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "wave", gate, digest, moved, ...(waveApproval.approvedBy ? { approvedBy: waveApproval.approvedBy } : {}) };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const applied = await applyPrSet({
|
|
1378
|
+
components: p.components,
|
|
1379
|
+
plan: wavePlan,
|
|
1380
|
+
registry: p.registry,
|
|
1381
|
+
env: p.env,
|
|
1382
|
+
set,
|
|
1383
|
+
// One wave's fan-out numbers its wave 1; the events and the log carry the derivation's number.
|
|
1384
|
+
onRunEvent: (body: RunEventBody) => {
|
|
1385
|
+
const numbered = inWave(body, k);
|
|
1386
|
+
events?.nested(numbered);
|
|
1387
|
+
if (numbered.type !== "run-start") log(numbered);
|
|
1388
|
+
},
|
|
1389
|
+
onComponentSettled: (settled, outputs) => {
|
|
1390
|
+
if (settled.status === "ok") {
|
|
1391
|
+
completed.add(settled.component);
|
|
1392
|
+
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
1393
|
+
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
1394
|
+
} else if (settled.status === "fail") {
|
|
1395
|
+
failedSoFar.add(settled.component);
|
|
1396
|
+
}
|
|
1397
|
+
recordAttempt();
|
|
1398
|
+
},
|
|
1399
|
+
});
|
|
1400
|
+
for (const m of applied.members) after.set(m.member, m);
|
|
1401
|
+
for (const name of applied.run.completed) {
|
|
1402
|
+
completed.add(name);
|
|
1403
|
+
const outputs = applied.run.componentOutputs[name];
|
|
1404
|
+
if (outputs) outputsSoFar[name] = outputs;
|
|
1405
|
+
}
|
|
1406
|
+
setWave({ ...entry, status: applied.status === "applied" ? "applied" : "failed" });
|
|
1407
|
+
await recordAutoReleasesForRun(applied.run.results, p.env, at.release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), at.release.runOrigin);
|
|
1408
|
+
if (applied.status !== "applied") {
|
|
1409
|
+
return stop(report({ status: "failed" }), failedRunExitCode(applied.run.results), applied.run.status === "gated" ? "gated" : undefined);
|
|
1410
|
+
}
|
|
1411
|
+
console.error(formatSuccess(`applied wave ${k} of ${total} under ${entry.approval === "pull-request" ? `the pull request's approval of ${approvedSet.doc.digest}` : `${entry.gate}, approved for ${entry.digest}`} by ${(entry.approvedBy ?? []).join(", ")}`));
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const done = p.plan.order.every((name) => completed.has(name));
|
|
1415
|
+
return stop(report({ status: done ? "applied" : "wave-applied" }), 0);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** A wave's new plan (#4003), beside the report as `change-set-wave-<k>.json`, for whoever approves its gate. */
|
|
1419
|
+
function writeWaveChangeSet(ctx: CommandContext, wave: number, doc: PrPlanSet["doc"], upload?: PrReportUpload): void {
|
|
1420
|
+
const dir = resolve(ctx.args.output ?? PR_REPORT_DIR);
|
|
1421
|
+
mkdirSync(dir, { recursive: true });
|
|
1422
|
+
const name = `change-set-wave-${wave}.json`;
|
|
1423
|
+
const body = JSON.stringify(doc, null, 2) + "\n";
|
|
1424
|
+
writeFileSync(join(dir, name), body);
|
|
1425
|
+
upload?.file(name, body);
|
|
1426
|
+
}
|