@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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { describe, test, expect } from "vitest";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
-
import { shellCmd } from "./shell";
|
|
13
|
+
import { containerCommand, imageProblem, shellCmd } from "./shell";
|
|
14
14
|
import { shellCmdContract } from "./activity-contracts";
|
|
15
15
|
import { shell } from "../builders";
|
|
16
16
|
import { activity, phase } from "../builders";
|
|
@@ -244,3 +244,46 @@ describe("a step's own timeout (#2787)", () => {
|
|
|
244
244
|
}
|
|
245
245
|
});
|
|
246
246
|
});
|
|
247
|
+
|
|
248
|
+
describe("shellCmd in a container (#4015)", () => {
|
|
249
|
+
const PINNED = `alpine:3.21@sha256:${"e".repeat(64)}`;
|
|
250
|
+
|
|
251
|
+
test("builds a docker run --rm line: workspace read-only, env by name, the command under sh -c", () => {
|
|
252
|
+
const line = containerCommand({ cmd: "make test", image: PINNED, env: { TOKEN: "x", MODE: "ci" } }, { cwd: "/src/app" });
|
|
253
|
+
expect(line).toBe(`docker run --rm -v '/src/app:/workspace:ro' -w /workspace -e 'TOKEN' -e 'MODE' '${PINNED}' sh -c 'make test'`);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("mounts each output directory writable over a read-only workspace, and none over a writable one", () => {
|
|
257
|
+
const ro = containerCommand({ cmd: "npm run build", image: PINNED, outputs: ["dist", "./reports/"] }, { cwd: "/src/app", cli: "podman", user: "501:20" });
|
|
258
|
+
expect(ro).toBe(
|
|
259
|
+
`podman run --rm --user 501:20 -v '/src/app:/workspace:ro' -v '/src/app/dist:/workspace/dist:rw' -v '/src/app/reports:/workspace/reports:rw' -w /workspace '${PINNED}' sh -c 'npm run build'`,
|
|
260
|
+
);
|
|
261
|
+
const rw = containerCommand({ cmd: "make", image: PINNED, mounts: { workspace: "rw" }, outputs: ["dist"] }, { cwd: "/src/app" });
|
|
262
|
+
expect(rw).toBe(`docker run --rm -v '/src/app:/workspace:rw' -w /workspace '${PINNED}' sh -c 'make'`);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("an image is pinned by digest, as a literal string", () => {
|
|
266
|
+
expect(imageProblem(PINNED)).toBeUndefined();
|
|
267
|
+
expect(imageProblem("alpine:3.21")).toMatch(/not pinned by digest/);
|
|
268
|
+
expect(imageProblem(`alpine@sha256:${"E".repeat(64)}`)).toMatch(/not pinned by digest/);
|
|
269
|
+
expect(imageProblem(42)).toMatch(/literal string/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("refuses before running anything: a tag, an output outside the working directory, outputs without an image", async () => {
|
|
273
|
+
await expect(shellCmd({ cmd: "true", image: "alpine:3.21" })).rejects.toThrow(/not pinned by digest/);
|
|
274
|
+
await expect(shellCmd({ cmd: "true", image: PINNED, outputs: ["../escape"] })).rejects.toThrow(/inside the working directory/);
|
|
275
|
+
await expect(shellCmd({ cmd: "true", image: PINNED, outputs: ["/etc"] })).rejects.toThrow(/inside the working directory/);
|
|
276
|
+
await expect(shellCmd({ cmd: "true", outputs: ["dist"] })).rejects.toThrow(/give the step an image/);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("the contract accepts image, mounts and outputs", () => {
|
|
280
|
+
expect(shellCmdContract.args.safeParse({ cmd: "x", image: PINNED, mounts: { workspace: "rw" }, outputs: ["dist"] }).success).toBe(true);
|
|
281
|
+
expect(shellCmdContract.args.safeParse({ cmd: "x", mounts: { workspace: "rwx" } }).success).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("the shell() builder passes them through to the step", () => {
|
|
285
|
+
const step = shell("npm test", { image: PINNED, outputs: ["coverage"] });
|
|
286
|
+
expect(step.args).toEqual({ cmd: "npm test", image: PINNED, outputs: ["coverage"] });
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, normalize, resolve } from "node:path";
|
|
2
4
|
import { promisify } from "node:util";
|
|
3
5
|
import { GateWait, asPendingGate } from "../gate-wait";
|
|
4
6
|
import type { PendingGateRecord } from "../../lifecycle/gate-ledger";
|
|
@@ -56,6 +58,84 @@ export interface ShellCmdArgs {
|
|
|
56
58
|
* something else. Empty stdout is `null`.
|
|
57
59
|
*/
|
|
58
60
|
json?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Run the command in a container (#4015) instead of on the host: an image
|
|
63
|
+
* pinned by digest, `<ref>@sha256:<64 hex>`, so the step names the
|
|
64
|
+
* toolchain it ran with. The step's working directory is mounted at
|
|
65
|
+
* `/workspace`, which is the command's working directory; the container
|
|
66
|
+
* sees `env` by name and nothing of this process's environment, whatever
|
|
67
|
+
* `inheritEnv` says. Run with `docker run --rm`, or the command
|
|
68
|
+
* `CHANT_CONTAINER_CLI` names (`podman`).
|
|
69
|
+
*/
|
|
70
|
+
image?: string;
|
|
71
|
+
/** How the container mounts the working directory (#4015). Default `{ workspace: "ro" }`. */
|
|
72
|
+
mounts?: { workspace?: "ro" | "rw" };
|
|
73
|
+
/**
|
|
74
|
+
* Directories under the working directory the containerized command writes
|
|
75
|
+
* (#4015). Each is created on the host if missing and mounted writable at
|
|
76
|
+
* the same path, so what the command leaves there stays on the host. A
|
|
77
|
+
* directory still empty when the command ends fails a step that otherwise
|
|
78
|
+
* passed. Only with `image`.
|
|
79
|
+
*/
|
|
80
|
+
outputs?: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** An image reference pinned by digest (#4015): anything, then `@sha256:` and 64 lowercase hex digits. */
|
|
84
|
+
export const PINNED_IMAGE = /^[^\s@]+@sha256:[0-9a-f]{64}$/;
|
|
85
|
+
|
|
86
|
+
/** Why `image` can't run, or undefined when it is a reference pinned by digest. */
|
|
87
|
+
export function imageProblem(image: unknown): string | undefined {
|
|
88
|
+
if (typeof image !== "string") return "image must be a literal string, an image reference pinned by digest";
|
|
89
|
+
if (!PINNED_IMAGE.test(image)) return `image ${JSON.stringify(image)} is not pinned by digest: name it as <ref>@sha256:<digest>`;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Why an `outputs` path can't be copied back, or undefined when it is a relative path inside the working directory. */
|
|
94
|
+
function outputProblem(path: string): string | undefined {
|
|
95
|
+
const n = normalize(path);
|
|
96
|
+
if (path === "" || isAbsolute(path) || n === ".." || n.startsWith("../") || n === ".") return `output ${JSON.stringify(path)} must be a relative path inside the working directory`;
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function shellQuote(s: string): string {
|
|
101
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Where a containerized command sees the working directory. */
|
|
105
|
+
const WORKSPACE = "/workspace";
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The `docker run --rm` line for a step with `image` (#4015). The working
|
|
109
|
+
* directory is mounted at `/workspace`, read-only by default, and each
|
|
110
|
+
* declared output directory is mounted writable on top of it.
|
|
111
|
+
*/
|
|
112
|
+
export function containerCommand(args: ShellCmdArgs & { image: string }, where: { cwd: string; cli?: string; user?: string }): string {
|
|
113
|
+
const mode = args.mounts?.workspace ?? "ro";
|
|
114
|
+
const outputs = mode === "ro" ? (args.outputs ?? []).map((p) => normalize(p).replace(/\/+$/, "")) : [];
|
|
115
|
+
return [
|
|
116
|
+
where.cli ?? "docker",
|
|
117
|
+
"run",
|
|
118
|
+
"--rm",
|
|
119
|
+
...(where.user ? ["--user", where.user] : []),
|
|
120
|
+
"-v",
|
|
121
|
+
shellQuote(`${where.cwd}:${WORKSPACE}:${mode}`),
|
|
122
|
+
...outputs.flatMap((p) => ["-v", shellQuote(`${join(where.cwd, p)}:${WORKSPACE}/${p}:rw`)]),
|
|
123
|
+
"-w",
|
|
124
|
+
WORKSPACE,
|
|
125
|
+
...Object.keys(args.env ?? {}).flatMap((name) => ["-e", shellQuote(name)]),
|
|
126
|
+
shellQuote(args.image),
|
|
127
|
+
"sh",
|
|
128
|
+
"-c",
|
|
129
|
+
shellQuote(args.cmd),
|
|
130
|
+
].join(" ");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Throw when a declared output directory is still empty after a command that passed. */
|
|
134
|
+
async function checkOutputs(outputs: readonly string[], cwd: string): Promise<void> {
|
|
135
|
+
for (const p of outputs) {
|
|
136
|
+
const entries = await readdir(join(cwd, normalize(p))).catch(() => []);
|
|
137
|
+
if (entries.length === 0) throw new Error(`the step declares output ${JSON.stringify(p)}, and the command left it empty`);
|
|
138
|
+
}
|
|
59
139
|
}
|
|
60
140
|
|
|
61
141
|
/**
|
|
@@ -130,12 +210,20 @@ export async function shellCmd(args: ShellCmdArgs, signal?: AbortSignal): Promis
|
|
|
130
210
|
};
|
|
131
211
|
const out = stepLineSplitter();
|
|
132
212
|
const err = stepLineSplitter();
|
|
213
|
+
if (args.image === undefined && (args.outputs !== undefined || args.mounts !== undefined)) {
|
|
214
|
+
throw new Error("outputs and mounts apply only to a step that runs in a container: give the step an image");
|
|
215
|
+
}
|
|
216
|
+
const container = args.image !== undefined ? await prepareContainer(args) : undefined;
|
|
133
217
|
try {
|
|
134
|
-
const running = execAsync(args.cmd, {
|
|
218
|
+
const running = execAsync(container?.cmd ?? args.cmd, {
|
|
135
219
|
cwd: args.cwd,
|
|
136
220
|
// In a run that keeps an in-flight record, CHANT_RUN_ACTIVITY names the
|
|
137
|
-
// file the command appends activity lines to (../run-live.ts).
|
|
138
|
-
|
|
221
|
+
// file the command appends activity lines to (../run-live.ts). Under
|
|
222
|
+
// `image` the container CLI gets this process's environment, so it can
|
|
223
|
+
// reach its daemon, and the container gets `env` alone, by name.
|
|
224
|
+
env: container
|
|
225
|
+
? { ...process.env, ...args.env }
|
|
226
|
+
: { ...(args.inheritEnv === false ? {} : process.env), ...liveRunEnv(), ...args.env },
|
|
139
227
|
maxBuffer: MAX_STDOUT_BYTES,
|
|
140
228
|
signal,
|
|
141
229
|
});
|
|
@@ -152,6 +240,7 @@ export async function shellCmd(args: ShellCmdArgs, signal?: AbortSignal): Promis
|
|
|
152
240
|
}
|
|
153
241
|
const { stdout, stderr } = settled;
|
|
154
242
|
printStderr(stderr);
|
|
243
|
+
if (container) await checkOutputs(args.outputs ?? [], container.cwd);
|
|
155
244
|
return withJson({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 }, args);
|
|
156
245
|
} catch (caught) {
|
|
157
246
|
const failure = caught as ExecFailure;
|
|
@@ -179,6 +268,24 @@ export async function shellCmd(args: ShellCmdArgs, signal?: AbortSignal): Promis
|
|
|
179
268
|
}
|
|
180
269
|
}
|
|
181
270
|
|
|
271
|
+
/** The container line for a step with `image` (#4015), checked first, with each output directory made on the host. */
|
|
272
|
+
async function prepareContainer(args: ShellCmdArgs): Promise<{ cmd: string; cwd: string }> {
|
|
273
|
+
const problem = imageProblem(args.image) ?? (args.outputs ?? []).map(outputProblem).find((p) => p !== undefined);
|
|
274
|
+
if (problem) throw new Error(problem);
|
|
275
|
+
const cwd = resolve(args.cwd ?? process.cwd());
|
|
276
|
+
// A writable mount inside a read-only one needs its mountpoint to exist already.
|
|
277
|
+
for (const p of args.outputs ?? []) await mkdir(join(cwd, normalize(p)), { recursive: true });
|
|
278
|
+
// The host's uid and gid, so what the command writes belongs to whoever
|
|
279
|
+
// runs chant rather than to the image's root.
|
|
280
|
+
const user = typeof process.getuid === "function" && typeof process.getgid === "function" ? `${process.getuid()}:${process.getgid()}` : undefined;
|
|
281
|
+
const cmd = containerCommand(args as ShellCmdArgs & { image: string }, {
|
|
282
|
+
cwd,
|
|
283
|
+
...(process.env.CHANT_CONTAINER_CLI ? { cli: process.env.CHANT_CONTAINER_CLI } : {}),
|
|
284
|
+
...(user ? { user } : {}),
|
|
285
|
+
});
|
|
286
|
+
return { cmd, cwd };
|
|
287
|
+
}
|
|
288
|
+
|
|
182
289
|
/** The pending fact a command's stdout carries: the whole of it as JSON, or its last line. */
|
|
183
290
|
function pendingFromStdout(stdout: string): PendingGateRecord | undefined {
|
|
184
291
|
const lines = stdout.split("\n");
|
package/src/op/builders.ts
CHANGED
|
@@ -391,6 +391,13 @@ export const lifecycleSnapshot = (env: string, opts?: { id?: string }): NamedAct
|
|
|
391
391
|
* twenty minutes, and keeps the profile's retries: `shell("./build.sh", {
|
|
392
392
|
* timeout: "45m" })`. At most six hours (`MAX_STEP_TIMEOUT`).
|
|
393
393
|
*
|
|
394
|
+
* `image` runs the command in a container instead of on the host (#4015),
|
|
395
|
+
* pinned by digest, with the working directory mounted read-only at
|
|
396
|
+
* `/workspace` unless `mounts` says `rw`, and each `outputs` directory
|
|
397
|
+
* mounted writable:
|
|
398
|
+
* `shell("npm run build", { image: "node:24@sha256:<digest>", outputs: ["dist"] })`.
|
|
399
|
+
* OPS018 refuses an image that is not pinned by digest.
|
|
400
|
+
*
|
|
394
401
|
* `cmd` stays a plain `string` and takes no references. A value spliced into
|
|
395
402
|
* a command line is a quoting decision chant would then be making on the
|
|
396
403
|
* author's behalf, and `env` carries the same value into the same command
|
|
@@ -15,13 +15,15 @@ import { discoverOps } from "./discover";
|
|
|
15
15
|
* with whatever the current test set (`realpath`ed, since macOS's tmpdir is a
|
|
16
16
|
* symlink and the walk compares resolved paths).
|
|
17
17
|
*/
|
|
18
|
-
let fakeGitRoot = "";
|
|
18
|
+
let fakeGitRoot: string | undefined = "";
|
|
19
19
|
vi.mock("../runtime-adapter", async (importOriginal) => ({
|
|
20
20
|
...(await importOriginal<typeof import("../runtime-adapter")>()),
|
|
21
21
|
getRuntime: () => ({
|
|
22
22
|
spawn: async (cmd: string[]) =>
|
|
23
23
|
cmd[0] === "git" && cmd[1] === "rev-parse"
|
|
24
|
-
?
|
|
24
|
+
? fakeGitRoot === undefined
|
|
25
|
+
? { stdout: "", stderr: "fatal: not a git repository", exitCode: 128 }
|
|
26
|
+
: { stdout: fakeGitRoot, stderr: "", exitCode: 0 }
|
|
25
27
|
: { stdout: "", stderr: "", exitCode: 0 },
|
|
26
28
|
}),
|
|
27
29
|
}));
|
|
@@ -68,6 +70,33 @@ describe("discoverOps — the scan root is the project, not the checkout (#2058)
|
|
|
68
70
|
});
|
|
69
71
|
});
|
|
70
72
|
|
|
73
|
+
describe("discoverOps outside a git repository (#2504)", () => {
|
|
74
|
+
let dir: string;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-nogit-")));
|
|
77
|
+
fakeGitRoot = undefined;
|
|
78
|
+
mkdirSync(join(dir, "demo", "ops"), { recursive: true });
|
|
79
|
+
writeFileSync(join(dir, "demo", "ops", "apply.op.ts"), OP_FILE("k3d-apply"));
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
fakeGitRoot = "";
|
|
83
|
+
rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a chant.config names the project root, so no git root is needed", async () => {
|
|
87
|
+
writeFileSync(join(dir, "demo", "chant.config.json"), JSON.stringify({ lexicons: ["k8s"] }));
|
|
88
|
+
const { ops, errors } = await discoverOps({ cwd: join(dir, "demo", "ops") });
|
|
89
|
+
expect(errors).toEqual([]);
|
|
90
|
+
expect([...ops.keys()]).toEqual(["k3d-apply"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("with neither a config nor a git root, discovery says both are missing", async () => {
|
|
94
|
+
await expect(discoverOps({ cwd: join(dir, "demo") })).rejects.toThrow(
|
|
95
|
+
/Not in a git repository, and no chant\.config/,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
71
100
|
/**
|
|
72
101
|
* #2171. An Op may be exported by name, not only as the file's default.
|
|
73
102
|
*
|
package/src/op/discover.ts
CHANGED
|
@@ -25,10 +25,11 @@ export interface OpDiscoveryResult {
|
|
|
25
25
|
errors: string[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/** The git root `cwd` is inside, or undefined when it is in no repository. */
|
|
29
|
+
async function findGitRoot(cwd?: string): Promise<string | undefined> {
|
|
29
30
|
const rt = getRuntime();
|
|
30
31
|
const result = await rt.spawn(["git", "rev-parse", "--show-toplevel"], { cwd });
|
|
31
|
-
if (result.exitCode !== 0)
|
|
32
|
+
if (result.exitCode !== 0) return undefined;
|
|
32
33
|
return result.stdout.trim();
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -43,17 +44,26 @@ async function findGitRoot(cwd?: string): Promise<string> {
|
|
|
43
44
|
* file is the project boundary, the same one entity discovery respects.
|
|
44
45
|
* With no config anywhere up to the git root, the git root stands —
|
|
45
46
|
* #1675's original scope, kept for configless layouts.
|
|
47
|
+
*
|
|
48
|
+
* Outside any git repository the walk goes to the filesystem root, and the
|
|
49
|
+
* config is all discovery needs (#2504): a demo copy, an unpacked tarball or
|
|
50
|
+
* a fresh `chant init` before `git init`. Only a directory with neither a
|
|
51
|
+
* config above it nor a git root has no Op root, and that is the error.
|
|
46
52
|
*/
|
|
47
53
|
async function findDiscoveryRoot(cwd?: string): Promise<string> {
|
|
48
|
-
const
|
|
54
|
+
const found = await findGitRoot(cwd);
|
|
55
|
+
const gitRoot = found === undefined ? undefined : resolve(found);
|
|
49
56
|
let dir = resolve(cwd ?? process.cwd());
|
|
50
57
|
for (;;) {
|
|
51
58
|
if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) return dir;
|
|
52
59
|
if (dir === gitRoot) break;
|
|
53
60
|
const parent = dirname(dir);
|
|
54
|
-
if (parent === dir) break; // filesystem root — cwd was outside the git root
|
|
61
|
+
if (parent === dir) break; // filesystem root — cwd was outside the git root, or there is none
|
|
55
62
|
dir = parent;
|
|
56
63
|
}
|
|
64
|
+
if (gitRoot === undefined) {
|
|
65
|
+
throw new Error("Not in a git repository, and no chant.config.ts or chant.config.json above the current directory");
|
|
66
|
+
}
|
|
57
67
|
return gitRoot;
|
|
58
68
|
}
|
|
59
69
|
|
package/src/op/gate.test.ts
CHANGED
|
@@ -613,6 +613,36 @@ describe("op/gate — a rejected ledger push is reported, not swallowed (#2310)"
|
|
|
613
613
|
});
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
+
describe("op/gate — an applied record is written on the remote's ledger (#3985)", () => {
|
|
617
|
+
test("appendApplied fetches first, so a remote another writer moved still takes the push", async () => {
|
|
618
|
+
const { remote, author } = await clonePair();
|
|
619
|
+
const other = tmp("other");
|
|
620
|
+
git(["clone", "-q", remote, other], tmpdir());
|
|
621
|
+
git(["config", "user.email", "other@chant.dev"], other);
|
|
622
|
+
git(["config", "user.name", "Other"], other);
|
|
623
|
+
|
|
624
|
+
const port = gitGateLedgerPort({ cwd: author });
|
|
625
|
+
const pending = { op: "migrations", gate: "migrations-wave-1", timestamp: "2026-10-10T00:00:00.000Z", expiresAt: "2026-10-11T00:00:00.000Z" };
|
|
626
|
+
expect((await port.appendPending(pending)).pushed).toBe(true);
|
|
627
|
+
|
|
628
|
+
// Another writer moves the remote past what `author` holds.
|
|
629
|
+
git(["fetch", "-q", "origin", "chant/lifecycle:chant/lifecycle"], other);
|
|
630
|
+
expect((await gitGateLedgerPort({ cwd: other }).appendPending({ ...pending, gate: "migrations-wave-2" })).pushed).toBe(true);
|
|
631
|
+
|
|
632
|
+
const line = {
|
|
633
|
+
version: 1 as const, kind: "applied" as const, op: "migrations", gate: "migrations-wave-1", planDigest: "sha256:aa",
|
|
634
|
+
approvedAt: "2026-10-10T01:00:00.000Z", approvedBy: "alice", timestamp: "2026-10-10T02:00:00.000Z",
|
|
635
|
+
};
|
|
636
|
+
expect(await port.appendApplied!(line)).toEqual({ pushed: true });
|
|
637
|
+
|
|
638
|
+
// The remote holds the applied line and the other writer's pending fact.
|
|
639
|
+
const reader = ciCheckout(remote);
|
|
640
|
+
const readerPort = gitGateLedgerPort({ cwd: reader });
|
|
641
|
+
expect(await readerPort.readApplied!("migrations")).toEqual([line]);
|
|
642
|
+
expect((await readerPort.read("migrations")).pending.map((p) => p.gate)).toEqual(["migrations-wave-1", "migrations-wave-2"]);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
616
646
|
describe("op/gate — the approve hint on a sealed gate (#3521)", () => {
|
|
617
647
|
test("approveCommand adds --sign last, only when sealed", () => {
|
|
618
648
|
expect(approveCommand("app", "ship")).toBe("chant approve app ship");
|
package/src/op/gate.ts
CHANGED
|
@@ -176,6 +176,9 @@ export function gitGateLedgerPort(opts?: { cwd?: string }): GateLedgerPort {
|
|
|
176
176
|
async appendApplied(record) {
|
|
177
177
|
const { dir, file } = appliedGatePath(record.op);
|
|
178
178
|
const line = JSON.stringify(record, sortedJsonReplacer);
|
|
179
|
+
// #3985: write on top of the remote's ledger, not a local copy another
|
|
180
|
+
// writer has since moved past, so the push that follows can land.
|
|
181
|
+
await requireLifecycleLedger(opts);
|
|
179
182
|
let lastErr: unknown;
|
|
180
183
|
// The same append-and-retry as a ledger line: another writer to the branch is the ordinary case.
|
|
181
184
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
package/src/op/op-waves-run.ts
CHANGED
|
@@ -68,6 +68,7 @@ import type { GateAnswerRef, PendingGateRecord } from "../lifecycle/gate-ledger"
|
|
|
68
68
|
import type { GatePointAsker } from "../point-decision";
|
|
69
69
|
import type { WaitingPoint } from "./steward-points";
|
|
70
70
|
import { isApprovalSpent, type AppliedGateRecord } from "../lifecycle/gate-applied";
|
|
71
|
+
import { isLifecycleLocalNote } from "../lifecycle/local-note";
|
|
71
72
|
import type { HeadPlans, MergedReview, ReviewSource } from "./gate-review";
|
|
72
73
|
import type { TrustPolicy } from "../workspace/trust/policy";
|
|
73
74
|
import type { GateSigners } from "../workspace/trust/gate-seal";
|
|
@@ -1721,11 +1722,22 @@ async function settleDenials(ctx: WaveContext, decision: OpWaveDecision, listed:
|
|
|
1721
1722
|
}
|
|
1722
1723
|
}
|
|
1723
1724
|
|
|
1725
|
+
/**
|
|
1726
|
+
* Throws unless a spend's record reached the remote the next run reads
|
|
1727
|
+
* (#3985). Without that, the next run reads the approval or override as
|
|
1728
|
+
* unspent. A ledger with no remote is the whole ledger, so a line recorded
|
|
1729
|
+
* there is spent.
|
|
1730
|
+
*/
|
|
1731
|
+
function requireSpendPushed(result: { pushed: boolean; pushWarning?: string }): void {
|
|
1732
|
+
if (result.pushed || isLifecycleLocalNote(result.pushWarning)) return;
|
|
1733
|
+
throw new Error(`the record did not reach the remote the next run reads: ${result.pushWarning ?? "the push did not land"}`);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1724
1736
|
/**
|
|
1725
1737
|
* Record each override the wave is about to apply under (#3821), in
|
|
1726
1738
|
* `_gates/policy-override/applied.jsonl`, which spends it: written before
|
|
1727
|
-
* anything applies, once. False when the lines could not be written
|
|
1728
|
-
* wave then fails and applies nothing.
|
|
1739
|
+
* anything applies, once. False when the lines could not be written or did
|
|
1740
|
+
* not reach the remote (#3985): the wave then fails and applies nothing.
|
|
1729
1741
|
*/
|
|
1730
1742
|
async function spendOverrides(ctx: WaveContext, decision: OpWaveDecision): Promise<boolean> {
|
|
1731
1743
|
const used = decision.members.filter((m) => m.override);
|
|
@@ -1738,7 +1750,7 @@ async function spendOverrides(ctx: WaveContext, decision: OpWaveDecision): Promi
|
|
|
1738
1750
|
if (!port.appendApplied) throw new Error("this gate ledger cannot record an applied override");
|
|
1739
1751
|
for (const m of used) {
|
|
1740
1752
|
const record = og.appliedOverride({ subject: m.member, digest: m.override!.digest, at: m.override!.at, by: m.override!.by }, { timestamp: now });
|
|
1741
|
-
await port.appendApplied(record as unknown as AppliedGateRecord);
|
|
1753
|
+
requireSpendPushed(await port.appendApplied(record as unknown as AppliedGateRecord));
|
|
1742
1754
|
}
|
|
1743
1755
|
} catch (err) {
|
|
1744
1756
|
failed(ctx, `wave ${k} (${wave.name}) could not record the overrides it applies under, so nothing applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2153,9 +2165,11 @@ async function applyStage(
|
|
|
2153
2165
|
* Record that the wave applies under its approval (#3784), which spends it:
|
|
2154
2166
|
* one line in `_gates/<name>/applied.jsonl`, before anything applies, as
|
|
2155
2167
|
* terragucci's wave gate writes it. Only an approved wave under the spend
|
|
2156
|
-
* rule records, and only once. False when the line could not be written
|
|
2168
|
+
* rule records, and only once. False when the line could not be written, or
|
|
2169
|
+
* was written but did not reach the remote the next run reads (#3985): the
|
|
2157
2170
|
* wave then fails and applies nothing, since an apply the record misses
|
|
2158
|
-
* would leave its approval standing.
|
|
2171
|
+
* would leave its approval standing. A ledger with no remote is the whole
|
|
2172
|
+
* ledger, so a line recorded there is spent.
|
|
2159
2173
|
*/
|
|
2160
2174
|
async function spendApproval(options: RunOpWaveOptions, ctx: WaveContext, decision: OpWaveDecision): Promise<boolean> {
|
|
2161
2175
|
if (!decision.spend || decision.status !== "approved" || decision.spentAt || !decision.approvedAt) return true;
|
|
@@ -2177,6 +2191,7 @@ async function spendApproval(options: RunOpWaveOptions, ctx: WaveContext, decisi
|
|
|
2177
2191
|
try {
|
|
2178
2192
|
if (!port.appendApplied) throw new Error("this gate ledger cannot record an applied approval");
|
|
2179
2193
|
const pushed = await port.appendApplied(record);
|
|
2194
|
+
requireSpendPushed(pushed);
|
|
2180
2195
|
if (!pushed.pushed && pushed.pushWarning) log(`wave ${k} (${wave.name}): ${pushed.pushWarning}`);
|
|
2181
2196
|
} catch (err) {
|
|
2182
2197
|
failed(ctx, `wave ${k} (${wave.name}) could not record that it applies under the approval by ${record.approvedBy}, so nothing applied: ${err instanceof Error ? err.message : String(err)}`);
|
package/src/op/op-waves.test.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from "./op-waves-run";
|
|
18
18
|
import { memoryGateLedgerPort } from "./gate";
|
|
19
19
|
import { waveSetDigest } from "../gated-waves";
|
|
20
|
+
import type { AppliedGateRecord } from "../lifecycle/gate-applied";
|
|
21
|
+
import { LIFECYCLE_LOCAL_NOTE } from "../lifecycle/local-note";
|
|
20
22
|
|
|
21
23
|
const spec: OpWavesSpec = {
|
|
22
24
|
name: "migrations",
|
|
@@ -475,6 +477,39 @@ describe("spent approvals (#3784)", () => {
|
|
|
475
477
|
h.done();
|
|
476
478
|
});
|
|
477
479
|
|
|
480
|
+
test("a spend whose record does not reach the remote applies nothing, and the approval stays unspent (#3985)", async () => {
|
|
481
|
+
const h = harness(spending);
|
|
482
|
+
const memory = memoryGateLedgerPort({ resolutions: [approval] });
|
|
483
|
+
const gates = {
|
|
484
|
+
...memory,
|
|
485
|
+
async appendApplied(record: AppliedGateRecord) {
|
|
486
|
+
await memory.appendApplied!(record);
|
|
487
|
+
return { pushed: false, pushWarning: 'the push of chant/lifecycle to "origin" failed' };
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
const result = await runOpWave({ spec: spending, specFile: "waves.json", wave: 2, cwd: h.cwd, exec: h.exec, show: h.show, gates, now: "2026-10-11T00:00:00Z" });
|
|
491
|
+
expect(result).toMatchObject({ exitCode: 1, applied: [] });
|
|
492
|
+
expect(result.decision?.spentAt).toBeUndefined();
|
|
493
|
+
expect(h.applies()).toBe(0);
|
|
494
|
+
h.done();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("with no remote the local ledger is the whole ledger, so a spend recorded there applies (#3985)", async () => {
|
|
498
|
+
const h = harness(spending);
|
|
499
|
+
const memory = memoryGateLedgerPort({ resolutions: [approval] });
|
|
500
|
+
const gates = {
|
|
501
|
+
...memory,
|
|
502
|
+
async appendApplied(record: AppliedGateRecord) {
|
|
503
|
+
await memory.appendApplied!(record);
|
|
504
|
+
return { pushed: false, pushWarning: LIFECYCLE_LOCAL_NOTE };
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
const result = await runOpWave({ spec: spending, specFile: "waves.json", wave: 2, cwd: h.cwd, exec: h.exec, show: h.show, gates, now: "2026-10-11T00:00:00Z" });
|
|
508
|
+
expect(result).toMatchObject({ exitCode: 0, decision: { status: "approved", spentAt: "2026-10-11T00:00:00Z" } });
|
|
509
|
+
expect(memory.applied).toHaveLength(1);
|
|
510
|
+
h.done();
|
|
511
|
+
});
|
|
512
|
+
|
|
478
513
|
test("a wave that cannot record the approval applies nothing", async () => {
|
|
479
514
|
const h = harness(spending);
|
|
480
515
|
const gates = { ...memoryGateLedgerPort({ resolutions: [approval] }), appendApplied: undefined };
|
|
@@ -878,6 +913,20 @@ describe("gates and holds a product's wave needs (#3821)", () => {
|
|
|
878
913
|
other.done();
|
|
879
914
|
});
|
|
880
915
|
|
|
916
|
+
test("an override spend whose record does not reach the remote applies nothing (#3985)", async () => {
|
|
917
|
+
const { overrideDigest } = await import("../override-gate");
|
|
918
|
+
const od = overrideDigest("vpc", digest("1"), ["no-public-s3"]);
|
|
919
|
+
const h = harness({ vpc: { planDigest: digest("1"), denied: { rules: ["no-public-s3"] } }, dns: { planDigest: digest("2"), empty: true } }, { extra: { overriders: ["github:carol"] }, atBase: { ...tg, waves: [{ ...tg.waves[0]!, gate: "never" }] } });
|
|
920
|
+
const memory = memoryGateLedgerPort({
|
|
921
|
+
resolutions: [{ version: 1, op: "policy-override", gate: "vpc", resolvedBy: "github:carol", timestamp: "2026-10-10T00:00:00Z", planDigest: od, note: "the bucket is the public docs site" }],
|
|
922
|
+
});
|
|
923
|
+
const gates = { ...memory, appendApplied: async () => ({ pushed: false, pushWarning: 'the push of chant/lifecycle to "origin" failed' }) };
|
|
924
|
+
const result = await run(h, gates);
|
|
925
|
+
expect(result).toMatchObject({ exitCode: 1, applied: [] });
|
|
926
|
+
expect(h.calls.some((c) => c.startsWith("tg apply"))).toBe(false);
|
|
927
|
+
h.done();
|
|
928
|
+
});
|
|
929
|
+
|
|
881
930
|
test("a plans file's overridersSource names the list in the refusal and the decision record (#3848)", async () => {
|
|
882
931
|
const mk = (extra: Record<string, unknown>) => harness({ vpc: { planDigest: digest("1"), denied: { rules: ["no-public-s3"] } }, dns: { planDigest: digest("2") } }, { extra });
|
|
883
932
|
const logs: string[] = [];
|
package/src/op/operator.ts
CHANGED
|
@@ -399,8 +399,8 @@ const STEWARD_TURN_POLL_MS = 150;
|
|
|
399
399
|
*
|
|
400
400
|
* Contention never queues past `waitMs` (0 by default): consistent with a
|
|
401
401
|
* round's own leases, which skip and report rather than wait (see this
|
|
402
|
-
* module's doc), and with the fountain form, whose busy
|
|
403
|
-
* refused rather than retried. A caller that wants to ride out a race with a
|
|
402
|
+
* module's doc), and with the fountain form, whose busy steward conversation
|
|
403
|
+
* is likewise refused rather than retried. A caller that wants to ride out a race with a
|
|
404
404
|
* turn about to end passes `waitMs`, and this polls every
|
|
405
405
|
* {@link STEWARD_TURN_POLL_MS} until it acquires or the wait runs out.
|
|
406
406
|
*/
|
package/src/op/runtime.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface OpRunStepRecord {
|
|
|
62
62
|
gate?: { op: string; gate: string };
|
|
63
63
|
/** The run whose checkpoint supplied this step's output, for a step a resumed run skipped (#3947). */
|
|
64
64
|
resumed?: { run: string; gate: string };
|
|
65
|
+
/** The image a `shellCmd` step ran in (#4015), pinned by digest, for a step that ran in a container. */
|
|
66
|
+
image?: string;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/** One phase's steps and the verdict they add up to. */
|
|
@@ -118,13 +118,15 @@ describe("a quiet step says it is still running (#3956)", () => {
|
|
|
118
118
|
|
|
119
119
|
test("a step printing output is not interrupted by the still-running line", async () => {
|
|
120
120
|
const out = recorder();
|
|
121
|
-
|
|
121
|
+
// sh, not node: node's start can outlast the heartbeat on a loaded runner,
|
|
122
|
+
// and the gap before the first line is what the heartbeat measures.
|
|
123
|
+
const chatty = `sh -c 'for n in 0 1 2 3 4 5; do echo "tick $n"; sleep 0.04; done'`;
|
|
122
124
|
await runOpLocally(
|
|
123
125
|
{ name: "chatty", overview: "", phases: [phase("Go", [shell(chatty, { id: "ticks" })])] },
|
|
124
126
|
new Map([["shellCmd", shellFn]]),
|
|
125
127
|
ACTIVITY_PROFILES,
|
|
126
128
|
undefined,
|
|
127
|
-
{ gates: memoryGateLedgerPort(), stepOutput: out.write, stepHeartbeatMs:
|
|
129
|
+
{ gates: memoryGateLedgerPort(), stepOutput: out.write, stepHeartbeatMs: 400 },
|
|
128
130
|
);
|
|
129
131
|
expect(out.lines.filter((l) => l.text.includes("still running"))).toEqual([]);
|
|
130
132
|
expect(out.lines.map((l) => l.text)).toContain("[Go] ticks | tick 5");
|
package/src/op/steward.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* A steward runs a fixed list of Ops, each on its own `schedule` when it has
|
|
6
6
|
* one, one Op at a time. Where it runs is its form:
|
|
7
7
|
*
|
|
8
|
-
* - `fountain`: the fountain lexicon's `Steward` composite, an `acp` Agent
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* - `fountain`: the fountain lexicon's `Steward` composite, an `acp` Agent
|
|
9
|
+
* whose runs `chant run <op> --on fountain` sends to one long-lived
|
|
10
|
+
* conversation, resumed by channel_id. A turn is one `chant run <op>` in
|
|
11
|
+
* that conversation. Managoat has no scheduler, so a scheduled Op's cron
|
|
12
|
+
* fires from a clock outside it (a CI schedule, a CronJob).
|
|
11
13
|
* - `local`: `chant operator --steward <name>` in the box itself, for a box
|
|
12
14
|
* with no Fountain behind it (a docker compose box, a Sprites box). The same
|
|
13
15
|
* Ops on the same crons, each run under its operator lease, and the steward
|
|
@@ -129,7 +131,7 @@ export interface StewardBeside {
|
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
export interface StewardDeclarationConfig {
|
|
132
|
-
/** The steward's name. On Fountain, the Agent's and the
|
|
134
|
+
/** The steward's name. On Fountain, the Agent's, and the key of its conversation. */
|
|
133
135
|
name: string;
|
|
134
136
|
/** The Ops it runs. A scheduled one runs on its cron; any other runs when asked. */
|
|
135
137
|
ops: StewardOpInput[];
|