@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
|
@@ -58,11 +58,23 @@ export interface RunOrigin {
|
|
|
58
58
|
repo?: string;
|
|
59
59
|
/** Resolved link to the run, when the environment provided enough to build one. */
|
|
60
60
|
url?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The forge's id for the job within the run (#4156): `GITHUB_JOB` on GitHub
|
|
63
|
+
* and Forgejo, the job's key in the workflow, and `CI_JOB_ID` on GitLab.
|
|
64
|
+
* Only `chant ci run` in a forge job writes it ({@link resolveCiJob}),
|
|
65
|
+
* from {@link RUN_ORIGIN_JOB_FLOOR}.
|
|
66
|
+
*/
|
|
67
|
+
jobId?: string;
|
|
68
|
+
/** The run's attempt, `GITHUB_RUN_ATTEMPT` on GitHub and Forgejo (#4156). GitLab gives a retried job a new `jobId` instead. */
|
|
69
|
+
attempt?: number;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/** The first chant that writes `runOrigin.forge: "chant"`, for a job `chant ci run` ran (#4094). */
|
|
64
73
|
export const RUN_ORIGIN_CHANT_FLOOR = "0.124.30";
|
|
65
74
|
|
|
75
|
+
/** The first chant that writes `runOrigin.jobId` and `runOrigin.attempt`, for a forge job that ran `chant ci run` (#4156). */
|
|
76
|
+
export const RUN_ORIGIN_JOB_FLOOR = "0.124.32";
|
|
77
|
+
|
|
66
78
|
/**
|
|
67
79
|
* The gate resolution that admitted a deploy (#4020): the gate's address in the
|
|
68
80
|
* gate ledger (`_gates/<op>.jsonl`, `op` and `gate`) and when the resolution
|
|
@@ -310,6 +322,28 @@ export function resolveCiRun(env: NodeJS.ProcessEnv = process.env): { runId: str
|
|
|
310
322
|
return undefined;
|
|
311
323
|
}
|
|
312
324
|
|
|
325
|
+
/**
|
|
326
|
+
* The CI run the environment names, as {@link resolveCiRun} has it, with the
|
|
327
|
+
* job within the run and the run's attempt when the forge gives them
|
|
328
|
+
* (#4156). `chant ci run` in a forge job records this as its run's origin;
|
|
329
|
+
* a release record keeps {@link resolveCiRun}'s shape.
|
|
330
|
+
*/
|
|
331
|
+
export function resolveCiJob(env: NodeJS.ProcessEnv = process.env): { runId: string; runOrigin: RunOrigin } | undefined {
|
|
332
|
+
const ci = resolveCiRun(env);
|
|
333
|
+
if (!ci) return undefined;
|
|
334
|
+
const github = ci.runOrigin.forge === "github";
|
|
335
|
+
const jobId = github ? env.GITHUB_JOB : env.CI_JOB_ID;
|
|
336
|
+
const attempt = github ? Number(env.GITHUB_RUN_ATTEMPT) : NaN;
|
|
337
|
+
return {
|
|
338
|
+
runId: ci.runId,
|
|
339
|
+
runOrigin: {
|
|
340
|
+
...ci.runOrigin,
|
|
341
|
+
...(jobId ? { jobId } : {}),
|
|
342
|
+
...(Number.isInteger(attempt) && attempt > 0 ? { attempt } : {}),
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
313
347
|
/**
|
|
314
348
|
* Append one immutable release record to the `<env>` ledger on the
|
|
315
349
|
* `chant/lifecycle` orphan branch. Throws `InvalidReleaseRecordError` before
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #4027: `chant release publish`'s contract. It refuses a release no cut
|
|
3
|
-
* names, a revoked one, and
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* names, a revoked one, and under identity.gates.release (even with
|
|
4
|
+
* --allow-unsigned) or the verified-releases security group (#4049, #4168)
|
|
5
|
+
* one with no verified signature; under supply-chain it warns when the run
|
|
6
|
+
* has no OIDC token. With no group a missing signature or OIDC token says
|
|
7
|
+
* nothing (#4168). With no publish target declared it has nothing to
|
|
8
|
+
* publish. With a target (#4047)
|
|
6
9
|
* it reads them from release.publish and refuses a checkout that is not the
|
|
7
10
|
* release's commit with a clean tree.
|
|
8
11
|
*/
|
|
@@ -76,26 +79,37 @@ describe("provenance", () => {
|
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
describe("chant release publish", () => {
|
|
79
|
-
test("
|
|
82
|
+
test("publishes an unsigned release on a workstation quietly, records it unsigned, and has no target to publish to (#4168)", async () => {
|
|
80
83
|
const { r } = setup(null);
|
|
81
84
|
await cut(r, "v1");
|
|
82
|
-
const plan = await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION
|
|
85
|
+
const plan = await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION });
|
|
83
86
|
expect(refusal(plan)).toBeNull();
|
|
84
87
|
const p = plan as PublishPlan;
|
|
85
88
|
expect(p.label).toBe("v1");
|
|
86
89
|
expect(p.signature.status).toBe("unsigned");
|
|
87
90
|
expect(p.provenance.oidc).toBe(false);
|
|
91
|
+
expect(p.warning).toBeNull();
|
|
92
|
+
expect(p.signatureWarning).toBeNull();
|
|
93
|
+
expect(p.requireSbom).toBe(false);
|
|
94
|
+
expect(p.targets).toEqual([]);
|
|
95
|
+
expect(p.releaseTag).toBe("release/v1");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("under supply-chain, warns on a workstation that what it publishes carries no provenance, and not in CI with OIDC (#4168)", async () => {
|
|
99
|
+
const { r } = setup({ security: ["supply-chain"] });
|
|
100
|
+
await cut(r, "v1");
|
|
101
|
+
const plan = await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION });
|
|
102
|
+
expect(refusal(plan)).toBeNull();
|
|
103
|
+
const p = plan as PublishPlan;
|
|
104
|
+
expect(p.requireSbom).toBe(true);
|
|
88
105
|
expect(p.warning).toBe(
|
|
89
106
|
"this publish runs without OIDC, so what it publishes carries no provenance: this run is not in CI, and a workstation has no OIDC token. Publish from CI to get provenance.",
|
|
90
107
|
);
|
|
91
|
-
expect(p.targets).toEqual([]);
|
|
92
|
-
expect(p.releaseTag).toBe("release/v1");
|
|
93
108
|
|
|
94
109
|
const inCi = (await preparePublish({
|
|
95
110
|
cwd: r.dir,
|
|
96
111
|
ref: p.release.releaseDigest,
|
|
97
112
|
env: { GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://x", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" },
|
|
98
|
-
allowUnsigned: true,
|
|
99
113
|
})) as PublishPlan;
|
|
100
114
|
expect(inCi.warning).toBeNull();
|
|
101
115
|
expect(inCi.label).toBe("v1");
|
|
@@ -146,12 +160,14 @@ describe("chant release publish", () => {
|
|
|
146
160
|
expect(refusal(await preparePublish({ cwd: r.dir, ref: "v2", env: WORKSTATION, allowUnsigned: true }))?.code).toBe("release-signature-unverified");
|
|
147
161
|
});
|
|
148
162
|
|
|
149
|
-
test("refuses an unsigned release with no release gate, and publishes it under --allow-unsigned with a warning", async () => {
|
|
150
|
-
const { r } = setup(
|
|
163
|
+
test("under verified-releases, refuses an unsigned release with no release gate, and publishes it under --allow-unsigned with a warning", async () => {
|
|
164
|
+
const { r } = setup({ security: ["verified-releases"] });
|
|
151
165
|
await cut(r, "v1");
|
|
152
166
|
const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION }));
|
|
153
167
|
expect(refused?.code).toBe("release-unsigned");
|
|
154
|
-
expect(refused?.message).toBe(
|
|
168
|
+
expect(refused?.message).toBe(
|
|
169
|
+
"v1 has no signed cut, and the workspace opts into verified-releases (security in chant.workspace.json), which publishes only a release signed by a key in .chant/allowed_signers",
|
|
170
|
+
);
|
|
155
171
|
expect(refused?.fix).toContain("chant release cut v1 --sign --yes");
|
|
156
172
|
expect(refused?.fix).toContain("--allow-unsigned");
|
|
157
173
|
|
|
@@ -161,8 +177,12 @@ describe("chant release publish", () => {
|
|
|
161
177
|
expect(allowed.cut).toMatchObject({ actor: "alice", label: "v1", signature: null });
|
|
162
178
|
});
|
|
163
179
|
|
|
164
|
-
test.skipIf(!hasSshKeygen)("with no release gate, publishes a release signed by a listed key and refuses one signed by a key the signers file does not list", async () => {
|
|
165
|
-
const { r, alice, bob } = setup(
|
|
180
|
+
test.skipIf(!hasSshKeygen)("under verified-releases with no release gate, publishes a release signed by a listed key and refuses one signed by a key the signers file does not list", async () => {
|
|
181
|
+
const { r, alice, bob } = setup({ security: ["verified-releases"] });
|
|
182
|
+
// Only alice is a signer.
|
|
183
|
+
r.write(".chant/allowed_signers", `alice ${alice.pub}\n`);
|
|
184
|
+
const head = r.commit("only alice signs");
|
|
185
|
+
r.git(["tag", `ci/green/${head}`]);
|
|
166
186
|
await cut(r, "v1", alice);
|
|
167
187
|
const signed = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION })) as PublishPlan;
|
|
168
188
|
expect(refusal(signed)).toBeNull();
|
|
@@ -180,6 +200,16 @@ describe("chant release publish", () => {
|
|
|
180
200
|
expect(unlisted?.message).toMatch(/bob has no key in \.chant\/allowed_signers at base/);
|
|
181
201
|
});
|
|
182
202
|
|
|
203
|
+
test.skipIf(!hasSshKeygen)("with no security group, publishes a release whose signature does not verify, records it unverified, and says nothing (#4168)", async () => {
|
|
204
|
+
const { r, bob } = setup(null, { signers: true });
|
|
205
|
+
await cut(r, "v1", bob, "bob");
|
|
206
|
+
const p = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION })) as PublishPlan;
|
|
207
|
+
expect(refusal(p)).toBeNull();
|
|
208
|
+
expect(p.signature.status).toBe("unverified");
|
|
209
|
+
expect(p.signatureWarning).toBeNull();
|
|
210
|
+
expect(p.warning).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
183
213
|
test("--require-provenance refuses a run with no OIDC token and passes one in CI", async () => {
|
|
184
214
|
const { r } = setup(null);
|
|
185
215
|
await cut(r, "v1");
|
|
@@ -7,16 +7,23 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - a release no cut names (`release-not-found`);
|
|
9
9
|
* - a release cut from a revoked commit (`release-revoked`, #4033);
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* `release-signature-unverified
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* - under an `identity.gates` entry for `release` (#4031), a release with no
|
|
11
|
+
* cut signed by a signer in the gate's class (`release-unsigned` or
|
|
12
|
+
* `release-signature-unverified`);
|
|
13
|
+
* - when the workspace opts into `verified-releases` (`security` in
|
|
14
|
+
* chant.workspace.json, #4168), a release none of whose cuts carries a
|
|
15
|
+
* signature that verifies for its actor against `.chant/allowed_signers`
|
|
16
|
+
* at base (#4049). `--allow-unsigned` publishes it anyway, with a warning;
|
|
17
|
+
* it never overrides the release gate.
|
|
18
|
+
*
|
|
19
|
+
* With neither, an unsigned release publishes quietly, and the plan records
|
|
20
|
+
* its `signature` status (#4168: security features are opt-in).
|
|
16
21
|
*
|
|
17
22
|
* It then says whether the run has an OIDC token to sign provenance with
|
|
18
|
-
* ({@link detectProvenance})
|
|
19
|
-
* runs; `--require-provenance` refuses instead
|
|
23
|
+
* ({@link detectProvenance}). Under `supply-chain` it warns when there is
|
|
24
|
+
* none, before any target runs; `--require-provenance` refuses instead
|
|
25
|
+
* (`publish-provenance-missing`). Without either, provenance is only the
|
|
26
|
+
* plan's `provenance` field.
|
|
20
27
|
* The targets come from {@link releasePublishTargets}: the member declares them under `release.publish` in its `chant.config.ts` (#4047).
|
|
21
28
|
* With a target, the checkout must be the release's commit with a clean
|
|
22
29
|
* tree ({@link checkCheckoutIsRelease}), every target is planned before any
|
|
@@ -154,6 +161,12 @@ export interface PublishContext {
|
|
|
154
161
|
redactor: Redactor;
|
|
155
162
|
/** Print one line of progress. The line is redacted. */
|
|
156
163
|
log(line: string): void;
|
|
164
|
+
/**
|
|
165
|
+
* Whether a target with no SBOM source refuses (`publish-sbom-missing`),
|
|
166
|
+
* as under the `supply-chain` group (#4168). False or absent: it publishes
|
|
167
|
+
* without an SBOM. A declared `sbom` file is attached either way.
|
|
168
|
+
*/
|
|
169
|
+
requireSbom?: boolean;
|
|
157
170
|
}
|
|
158
171
|
|
|
159
172
|
/** A target refused or failed. The message never holds a credential. */
|
|
@@ -252,7 +265,7 @@ export interface PublishPlan {
|
|
|
252
265
|
/** The verified cut's signature when one verifies, else the newest cut's. */
|
|
253
266
|
signature: { status: "verified" | "unsigned" | "unverified"; actor: string | null; key: string | null; reason: string };
|
|
254
267
|
provenance: ProvenanceCheck;
|
|
255
|
-
/** The warning printed before publishing, when the run has no OIDC token. */
|
|
268
|
+
/** The warning printed before publishing, when the run has no OIDC token and the workspace opts into `supply-chain` (#4168). */
|
|
256
269
|
warning: string | null;
|
|
257
270
|
/** The warning printed before publishing a release with no verified signature under `--allow-unsigned` (#4049). */
|
|
258
271
|
signatureWarning: string | null;
|
|
@@ -263,6 +276,8 @@ export interface PublishPlan {
|
|
|
263
276
|
targets: PublishTarget[];
|
|
264
277
|
/** Masks the run's credentials in what the publish prints. */
|
|
265
278
|
redactor: Redactor;
|
|
279
|
+
/** Whether a target with no SBOM source is refused (`publish-sbom-missing`): the workspace opts into `supply-chain` (#4168). Otherwise it publishes without one. */
|
|
280
|
+
requireSbom: boolean;
|
|
266
281
|
}
|
|
267
282
|
|
|
268
283
|
/**
|
|
@@ -270,9 +285,10 @@ export interface PublishPlan {
|
|
|
270
285
|
* whether it may be published, reading the local `chant/lifecycle` branch,
|
|
271
286
|
* the local tags and the declaration at base. Fetches nothing.
|
|
272
287
|
*
|
|
273
|
-
* `allowUnsigned` publishes a release with no verified signature, which
|
|
274
|
-
*
|
|
275
|
-
* `requireProvenance` refuses a run with no OIDC token,
|
|
288
|
+
* `allowUnsigned` publishes a release with no verified signature, which
|
|
289
|
+
* `verified-releases` refuses otherwise (#4049, #4168); it does not override
|
|
290
|
+
* the release gate. `requireProvenance` refuses a run with no OIDC token,
|
|
291
|
+
* where `supply-chain` warns and no group says nothing.
|
|
276
292
|
*/
|
|
277
293
|
export async function preparePublish(req: {
|
|
278
294
|
cwd: string;
|
|
@@ -287,6 +303,7 @@ export async function preparePublish(req: {
|
|
|
287
303
|
const { scopeSource } = await import("../workspace/write-scope");
|
|
288
304
|
const { releaseRuleFrom } = await import("../workspace/identity");
|
|
289
305
|
const { checkReleaseSignature } = await import("../workspace/identity-gate-rule");
|
|
306
|
+
const { securityGroupsIn } = await import("../workspace/security");
|
|
290
307
|
const { cwd, ref } = req;
|
|
291
308
|
const found = await resolveRelease(ref, { cwd });
|
|
292
309
|
if (!found) return { refusal: { code: "release-not-found", message: `no release is labelled or digested "${ref}" in this member`, fix: "List them with: chant release list" } };
|
|
@@ -304,7 +321,9 @@ export async function preparePublish(req: {
|
|
|
304
321
|
};
|
|
305
322
|
}
|
|
306
323
|
|
|
307
|
-
const
|
|
324
|
+
const source = scopeSource(cwd);
|
|
325
|
+
const rule = releaseRuleFrom(source);
|
|
326
|
+
const groups = securityGroupsIn(source, cwd);
|
|
308
327
|
const cuts = (await readCuts({ cwd })).cuts.filter((c) => c.releaseDigest === release.releaseDigest);
|
|
309
328
|
const checks = cuts.map((c) => ({
|
|
310
329
|
cut: c,
|
|
@@ -323,9 +342,9 @@ export async function preparePublish(req: {
|
|
|
323
342
|
},
|
|
324
343
|
};
|
|
325
344
|
}
|
|
326
|
-
// #4049:
|
|
345
|
+
// #4049, #4168: under verified-releases a publish refuses a release whose signature does not verify, unless --allow-unsigned. Without it an unsigned release publishes quietly.
|
|
327
346
|
let signatureWarning: string | null = null;
|
|
328
|
-
if (!verified) {
|
|
347
|
+
if (!verified && groups.has("verified-releases")) {
|
|
329
348
|
const why = unsigned
|
|
330
349
|
? `${name} has no signed cut`
|
|
331
350
|
: `no cut of ${name} carries a signature that verifies against the signers file at base: ${checks.map((c) => c.check.reason).join("; ")}`;
|
|
@@ -333,7 +352,7 @@ export async function preparePublish(req: {
|
|
|
333
352
|
return {
|
|
334
353
|
refusal: {
|
|
335
354
|
code: unsigned ? "release-unsigned" : "release-signature-unverified",
|
|
336
|
-
message: `${why}, and chant
|
|
355
|
+
message: `${why}, and the workspace opts into verified-releases (security in chant.workspace.json), which publishes only a release signed by a key in .chant/allowed_signers`,
|
|
337
356
|
fix: `Cut it signed with a key .chant/allowed_signers lists for the chant-release namespace: chant release cut ${label ?? "<label>"} --sign --yes. To publish it unsigned anyway, pass --allow-unsigned.`,
|
|
338
357
|
},
|
|
339
358
|
};
|
|
@@ -371,7 +390,7 @@ export async function preparePublish(req: {
|
|
|
371
390
|
},
|
|
372
391
|
};
|
|
373
392
|
}
|
|
374
|
-
const warning = provenance.oidc
|
|
393
|
+
const warning = provenance.oidc || !groups.has("supply-chain")
|
|
375
394
|
? null
|
|
376
395
|
: `this publish runs without OIDC, so what it publishes carries no provenance: ${provenance.reason}. Publish from CI to get provenance.`;
|
|
377
396
|
const { resolveMemberLedger } = await import("./member-ledger");
|
|
@@ -380,7 +399,7 @@ export async function preparePublish(req: {
|
|
|
380
399
|
const member = ledger.members.length > 0 ? ledger.members.join("/") : null;
|
|
381
400
|
const releaseTag = label !== null ? releaseTagName(member, label) : null;
|
|
382
401
|
const redactor = createRedactor(env, targets.flatMap((t) => t.credentialEnv));
|
|
383
|
-
return { release, label, signature, provenance, warning, signatureWarning, cut, releaseTag, targets, redactor };
|
|
402
|
+
return { release, label, signature, provenance, warning, signatureWarning, cut, releaseTag, targets, redactor, requireSbom: groups.has("supply-chain") };
|
|
384
403
|
}
|
|
385
404
|
|
|
386
405
|
/**
|
|
@@ -432,6 +451,7 @@ export function publishContext(plan: PublishPlan, req: { cwd: string; env?: Node
|
|
|
432
451
|
env: req.env ?? process.env,
|
|
433
452
|
redactor: plan.redactor,
|
|
434
453
|
log: (line) => req.log(plan.redactor.redact(line)),
|
|
454
|
+
requireSbom: plan.requireSbom,
|
|
435
455
|
};
|
|
436
456
|
}
|
|
437
457
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* _releases/<releaseDigest>.json the release, written once and never rewritten
|
|
15
15
|
* _releases/cuts.jsonl one line per cut, with the release's label
|
|
16
16
|
* _releases/templates/<digest>.txt each template's bytes, by content digest
|
|
17
|
+
* _releases/templates/<digest>.b64 a file that is not UTF-8, base64 (#4155)
|
|
17
18
|
*
|
|
18
19
|
* `:` is written as `_` in file names, as `_plans/` and `_builds/` do. A
|
|
19
20
|
* workspace member's copy sits under `_members/<member>/` through the same
|
|
@@ -24,6 +25,14 @@
|
|
|
24
25
|
* (ws-104 question 5). They are named by content digest, so a template that
|
|
25
26
|
* two environments or two releases share is stored once.
|
|
26
27
|
*
|
|
28
|
+
* Since #4155 a cut records every local file a deploy step deploys from, not
|
|
29
|
+
* only `template`, `manifest` and `values` files: a chart, a kustomize
|
|
30
|
+
* directory, a site's tree. A directory is recorded as one template per file
|
|
31
|
+
* under it, and named in the target's `trees`, so a promote deploys the
|
|
32
|
+
* recorded tree. A target with `trees` is one such cut; a target without the
|
|
33
|
+
* field was cut before #4155, and a promote of it reads directories and the
|
|
34
|
+
* other fields from the checkout, as it did then.
|
|
35
|
+
*
|
|
27
36
|
* `releaseDigest` is `computePlanDigest("release", subject)` over the member,
|
|
28
37
|
* the commit, the artifact digests and each target's env, templates and the
|
|
29
38
|
* name and value of each param. A param's `source`, the label, the actor, the
|
|
@@ -32,7 +41,7 @@
|
|
|
32
41
|
*/
|
|
33
42
|
|
|
34
43
|
import { sortedJsonReplacer } from "../utils";
|
|
35
|
-
import { contentDigest } from "../content-digest";
|
|
44
|
+
import { contentDigest, sha256Hex } from "../content-digest";
|
|
36
45
|
import { computePlanDigest } from "./plan-digest";
|
|
37
46
|
import {
|
|
38
47
|
ledgerDir,
|
|
@@ -74,6 +83,15 @@ export interface ReleaseTemplate {
|
|
|
74
83
|
path: string;
|
|
75
84
|
/** `sha256:<hex>` of the file's bytes. */
|
|
76
85
|
digest: string;
|
|
86
|
+
/** `base64` for a file that is not UTF-8 (#4155): its stored copy is base64. Absent for text. */
|
|
87
|
+
encoding?: "base64";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A directory a component's deploy names, whose files the target records as templates under its path (#4155). */
|
|
91
|
+
export interface ReleaseTree {
|
|
92
|
+
component: string | null;
|
|
93
|
+
/** The directory as the deploy step names it, relative to the member's directory. */
|
|
94
|
+
path: string;
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
/** One resolved build parameter of a target. */
|
|
@@ -89,6 +107,8 @@ export interface ReleaseTarget {
|
|
|
89
107
|
env: string;
|
|
90
108
|
templates: ReleaseTemplate[];
|
|
91
109
|
params: ReleaseParam[];
|
|
110
|
+
/** The directories the target records (#4155). Absent on a target cut before #4155. */
|
|
111
|
+
trees?: ReleaseTree[];
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
/** The release file, `_releases/<releaseDigest>.json`. */
|
|
@@ -147,9 +167,12 @@ export function normalizeRelease<T extends ReleaseSubject>(release: T): T {
|
|
|
147
167
|
.map((t) => ({
|
|
148
168
|
env: t.env,
|
|
149
169
|
templates: [...t.templates]
|
|
150
|
-
.map(({ component, path, digest }) => ({ component, path, digest }))
|
|
170
|
+
.map(({ component, path, digest, encoding }) => ({ component, path, digest, ...(encoding ? { encoding } : {}) }))
|
|
151
171
|
.sort((a, b) => byString(a.component ?? "", b.component ?? "") || byString(a.path, b.path)),
|
|
152
172
|
params: [...t.params].map(({ name, value, source }) => ({ name, value, source })).sort((a, b) => byString(a.name, b.name)),
|
|
173
|
+
...(t.trees
|
|
174
|
+
? { trees: [...t.trees].map(({ component, path }) => ({ component, path })).sort((a, b) => byString(a.component ?? "", b.component ?? "") || byString(a.path, b.path)) }
|
|
175
|
+
: {}),
|
|
153
176
|
}))
|
|
154
177
|
.sort((a, b) => byString(a.env, b.env)),
|
|
155
178
|
};
|
|
@@ -162,7 +185,12 @@ export function releaseHashInput(release: ReleaseSubject): unknown {
|
|
|
162
185
|
member: n.member,
|
|
163
186
|
commit: n.commit,
|
|
164
187
|
artifactDigests: n.artifactDigests,
|
|
165
|
-
targets: n.targets.map((t) => ({
|
|
188
|
+
targets: n.targets.map((t) => ({
|
|
189
|
+
env: t.env,
|
|
190
|
+
templates: t.templates,
|
|
191
|
+
params: t.params.map(({ name, value }) => ({ name, value })),
|
|
192
|
+
...(t.trees ? { trees: t.trees } : {}),
|
|
193
|
+
})),
|
|
166
194
|
};
|
|
167
195
|
}
|
|
168
196
|
|
|
@@ -184,7 +212,12 @@ export function verifyReleaseDigest(release: Release): boolean {
|
|
|
184
212
|
|
|
185
213
|
const stem = (digest: string): string => digest.replace(/:/g, "_");
|
|
186
214
|
const releaseFile = (digest: string): string => `${stem(digest)}.json`;
|
|
187
|
-
const templateFile = (digest: string): string => `${stem(digest)}
|
|
215
|
+
const templateFile = (digest: string, encoding?: "base64"): string => `${stem(digest)}.${encoding === "base64" ? "b64" : "txt"}`;
|
|
216
|
+
|
|
217
|
+
/** The digest of a template's bytes: its text, or what its base64 decodes to. */
|
|
218
|
+
export function templateBytesDigest(content: string, encoding?: "base64"): string {
|
|
219
|
+
return encoding === "base64" ? `sha256:${sha256Hex(Buffer.from(content, "base64"))}` : contentDigest(content);
|
|
220
|
+
}
|
|
188
221
|
|
|
189
222
|
/** A label names a release in commands such as `chant release cut <label>`, so it is one plain word. */
|
|
190
223
|
const LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
@@ -209,7 +242,9 @@ export class ReleaseLabelError extends Error {
|
|
|
209
242
|
/** A release's template bytes, as the cut read them. */
|
|
210
243
|
export interface TemplateContent {
|
|
211
244
|
digest: string;
|
|
245
|
+
/** The text, or the base64 of the bytes when `encoding` is `base64`. */
|
|
212
246
|
content: string;
|
|
247
|
+
encoding?: "base64";
|
|
213
248
|
}
|
|
214
249
|
|
|
215
250
|
/**
|
|
@@ -226,7 +261,7 @@ export async function persistRelease(
|
|
|
226
261
|
throw new Error(`release ${release.releaseDigest} does not hash to its own digest; refusing to store it`);
|
|
227
262
|
}
|
|
228
263
|
for (const t of templates) {
|
|
229
|
-
if (
|
|
264
|
+
if (templateBytesDigest(t.content, t.encoding) !== t.digest) throw new Error(`template bytes do not hash to ${t.digest}; refusing to store them`);
|
|
230
265
|
}
|
|
231
266
|
const dir = await ledgerDir(RELEASES_DIR, opts);
|
|
232
267
|
const files: LedgerFileWrite[] = [];
|
|
@@ -235,10 +270,11 @@ export async function persistRelease(
|
|
|
235
270
|
if (!exists) files.push({ path: releasePath, content: `${JSON.stringify(release, sortedJsonReplacer)}\n`, expectPriorSha: null });
|
|
236
271
|
const seen = new Set<string>();
|
|
237
272
|
for (const t of templates) {
|
|
238
|
-
|
|
239
|
-
seen.
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
const file = templateFile(t.digest, t.encoding);
|
|
274
|
+
if (seen.has(file)) continue;
|
|
275
|
+
seen.add(file);
|
|
276
|
+
if ((await readPathSha(`${RELEASES_DIR}/${TEMPLATES_SUBDIR}`, file, opts)) !== null) continue;
|
|
277
|
+
files.push({ path: `${dir}/${TEMPLATES_SUBDIR}/${file}`, content: t.content, expectPriorSha: null });
|
|
242
278
|
}
|
|
243
279
|
if (files.length === 0) return { commit: null, written: false };
|
|
244
280
|
try {
|
|
@@ -274,7 +310,15 @@ export function parseRelease(text: string): Release | null {
|
|
|
274
310
|
t &&
|
|
275
311
|
typeof t.env === "string" &&
|
|
276
312
|
Array.isArray(t.templates) &&
|
|
277
|
-
t.templates.every(
|
|
313
|
+
t.templates.every(
|
|
314
|
+
(x) =>
|
|
315
|
+
x &&
|
|
316
|
+
(x.component === null || typeof x.component === "string") &&
|
|
317
|
+
typeof x.path === "string" &&
|
|
318
|
+
typeof x.digest === "string" &&
|
|
319
|
+
(x.encoding === undefined || x.encoding === "base64"),
|
|
320
|
+
) &&
|
|
321
|
+
(t.trees === undefined || (Array.isArray(t.trees) && t.trees.every((x) => x && (x.component === null || typeof x.component === "string") && typeof x.path === "string"))) &&
|
|
278
322
|
Array.isArray(t.params) &&
|
|
279
323
|
t.params.every((p) => p && typeof p.name === "string" && ["string", "number", "boolean"].includes(typeof p.value) && typeof p.source === "string"),
|
|
280
324
|
);
|
|
@@ -414,9 +458,9 @@ export async function appendCut(
|
|
|
414
458
|
throw lastErr;
|
|
415
459
|
}
|
|
416
460
|
|
|
417
|
-
/** A template's bytes by digest, or null when none is stored. */
|
|
418
|
-
export async function readReleaseTemplate(digest: string, opts?: LedgerLocation): Promise<string | null> {
|
|
419
|
-
return readBlobFromPath(`${RELEASES_DIR}/${TEMPLATES_SUBDIR}`, templateFile(digest), opts);
|
|
461
|
+
/** A template's bytes by digest, or null when none is stored. Under `base64` (#4155), the stored base64 of the bytes. */
|
|
462
|
+
export async function readReleaseTemplate(digest: string, opts?: LedgerLocation, encoding?: "base64"): Promise<string | null> {
|
|
463
|
+
return readBlobFromPath(`${RELEASES_DIR}/${TEMPLATES_SUBDIR}`, templateFile(digest, encoding), opts);
|
|
420
464
|
}
|
|
421
465
|
|
|
422
466
|
/**
|
|
@@ -92,6 +92,19 @@ describe("buildRunRecord", () => {
|
|
|
92
92
|
expect(input.phases[1].steps.map((s) => s.status)).toEqual(["ok", "skipped"]);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
test("keeps a shell step's image on its record (#4015), and nothing for a step without one", () => {
|
|
96
|
+
const image = `node:24@sha256:${"c".repeat(64)}`;
|
|
97
|
+
const input = buildRunRecord(
|
|
98
|
+
{ name: "build" },
|
|
99
|
+
[
|
|
100
|
+
{ phase: "Build", fn: "shellCmd", args: { cmd: "npm test", image }, status: "ok", durationMs: 3 },
|
|
101
|
+
{ phase: "Build", fn: "shellCmd", args: { cmd: "echo hi" }, status: "ok", durationMs: 1 },
|
|
102
|
+
],
|
|
103
|
+
{ started: "2026-01-01T00:00:00.000Z", ended: "2026-01-01T00:00:01.000Z", status: "ok" },
|
|
104
|
+
);
|
|
105
|
+
expect(input.phases[0].steps.map((s) => s.image)).toEqual([image, undefined]);
|
|
106
|
+
});
|
|
107
|
+
|
|
95
108
|
test("carries the commit and the CI run origin when given (#4020), and neither when not", () => {
|
|
96
109
|
const times = { started: "2026-01-01T00:00:00.000Z", ended: "2026-01-01T00:00:01.000Z", status: "ok" as const };
|
|
97
110
|
const withBoth = buildRunRecord({ name: "ship" }, [], { ...times, gitSha: "a".repeat(40), runOrigin: { forge: "github", repo: "acme/app" } });
|
|
@@ -119,6 +119,7 @@ export function buildRunRecord(
|
|
|
119
119
|
...(record.point ? { point: record.point } : {}),
|
|
120
120
|
...(record.gate ? { gate: record.gate } : {}),
|
|
121
121
|
...(record.resumed ? { resumed: record.resumed } : {}),
|
|
122
|
+
...(record.fn === "shellCmd" && typeof record.args?.image === "string" ? { image: record.args.image } : {}),
|
|
122
123
|
});
|
|
123
124
|
if (record.outcome) outcomes[record.outcome.name] = record.outcome.value;
|
|
124
125
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2893: work worktrees live at `.chant/work/[<member>/]<item>` in the
|
|
3
|
+
* checkout, excluded through `<git-common-dir>/info/exclude`, and the
|
|
4
|
+
* hand-written walkers skip them.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { excludeWorkDir, isWorkDir, removeEmptyWorkDirs, workWorktreePath, legacyWorkWorktreePath, WORK_DIR_EXCLUDE } from "./work-dir";
|
|
11
|
+
import { listFiles } from "../workspace/export";
|
|
12
|
+
|
|
13
|
+
describe("the work directory", () => {
|
|
14
|
+
test("a worktree is .chant/work/[<member>/]<item> in the checkout; older versions made it under the git directory", () => {
|
|
15
|
+
expect(workWorktreePath("/repo", [], "W-1")).toBe(join("/repo", ".chant", "work", "W-1"));
|
|
16
|
+
expect(workWorktreePath("/repo", ["app"], "W-1")).toBe(join("/repo", ".chant", "work", "app", "W-1"));
|
|
17
|
+
expect(legacyWorkWorktreePath("/repo/.git", ["app"], "W-1")).toBe(join("/repo/.git", "chant-work", "app", "W-1"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("isWorkDir is .chant/work at the walk's root or below it, and nothing else in .chant", () => {
|
|
21
|
+
expect(isWorkDir(".chant/work")).toBe(true);
|
|
22
|
+
expect(isWorkDir("members/app/.chant/work")).toBe(true);
|
|
23
|
+
expect(isWorkDir(".chant")).toBe(false);
|
|
24
|
+
expect(isWorkDir(".chant/factory")).toBe(false);
|
|
25
|
+
expect(isWorkDir("x.chant/work")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("the exclude line is written once, after any last line without a newline, and not when git already ignores the directory", async () => {
|
|
29
|
+
await withTestDir(async (dir) => {
|
|
30
|
+
const common = join(dir, "git");
|
|
31
|
+
expect(excludeWorkDir(common, false)).toBe(true);
|
|
32
|
+
expect(readFileSync(join(common, "info", "exclude"), "utf-8")).toBe(`${WORK_DIR_EXCLUDE}\n`);
|
|
33
|
+
expect(excludeWorkDir(common, false)).toBe(false);
|
|
34
|
+
expect(readFileSync(join(common, "info", "exclude"), "utf-8")).toBe(`${WORK_DIR_EXCLUDE}\n`);
|
|
35
|
+
|
|
36
|
+
const other = join(dir, "other");
|
|
37
|
+
mkdirSync(join(other, "info"), { recursive: true });
|
|
38
|
+
writeFileSync(join(other, "info", "exclude"), "*.log");
|
|
39
|
+
expect(excludeWorkDir(other, false)).toBe(true);
|
|
40
|
+
expect(readFileSync(join(other, "info", "exclude"), "utf-8")).toBe(`*.log\n${WORK_DIR_EXCLUDE}\n`);
|
|
41
|
+
|
|
42
|
+
const ignored = join(dir, "ignored");
|
|
43
|
+
expect(excludeWorkDir(ignored, true)).toBe(false);
|
|
44
|
+
expect(existsSync(join(ignored, "info", "exclude"))).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("removing a worktree takes the empty directories above it up to .chant, and stops at one that holds anything", async () => {
|
|
49
|
+
await withTestDir(async (dir) => {
|
|
50
|
+
// The worktree itself is gone already: removed, its parent left empty.
|
|
51
|
+
const tree = workWorktreePath(dir, ["app"], "W-1");
|
|
52
|
+
mkdirSync(dirname(tree), { recursive: true });
|
|
53
|
+
mkdirSync(workWorktreePath(dir, [], "W-2"), { recursive: true });
|
|
54
|
+
removeEmptyWorkDirs(tree);
|
|
55
|
+
expect(existsSync(dirname(tree))).toBe(false);
|
|
56
|
+
expect(existsSync(join(dir, ".chant", "work", "W-2"))).toBe(true);
|
|
57
|
+
|
|
58
|
+
const only = join(dir, "only");
|
|
59
|
+
mkdirSync(join(only, ".chant", "work"), { recursive: true });
|
|
60
|
+
removeEmptyWorkDirs(workWorktreePath(only, [], "W-3"));
|
|
61
|
+
expect(existsSync(join(only, ".chant"))).toBe(false);
|
|
62
|
+
expect(existsSync(only)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("walkers skip .chant/work", () => {
|
|
68
|
+
test("export's file list outside git leaves out a work worktree and keeps the rest of .chant", async () => {
|
|
69
|
+
await withTestDir(async (dir) => {
|
|
70
|
+
for (const p of ["a.ts", ".chant/workspace.lock.json", ".chant/work/W-1/a.ts", "m/.chant/work/W-2/a.ts"]) {
|
|
71
|
+
mkdirSync(dirname(join(dir, p)), { recursive: true });
|
|
72
|
+
writeFileSync(join(dir, p), "x");
|
|
73
|
+
}
|
|
74
|
+
expect(listFiles(dir)).toEqual([".chant/workspace.lock.json", "a.ts"]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an Op that changes the checkout makes its work worktrees (#2893).
|
|
3
|
+
*
|
|
4
|
+
* A work item's worktree is `<checkout>/.chant/work/[<member>/]<item>`. It
|
|
5
|
+
* sits inside the checkout, so a coding agent whose project root is the
|
|
6
|
+
* checkout may write there, and `node_modules` above it still resolves. It
|
|
7
|
+
* used to be `<git-common-dir>/chant-work/...`, where Claude Code refuses
|
|
8
|
+
* every write. `/.chant/work/` goes in `<git-common-dir>/info/exclude`, so the
|
|
9
|
+
* checkout's `git status` never lists it, and the rest of `.chant/` (the
|
|
10
|
+
* workspace lock, the factory's check logs) stays tracked.
|
|
11
|
+
*
|
|
12
|
+
* This module imports nothing from the workspace or the Op runner, so the
|
|
13
|
+
* hand-written walkers can ask it which directory to skip.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync } from "node:fs";
|
|
17
|
+
import { dirname, join, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
/** The work directory, relative to a checkout's top, `/`-separated. */
|
|
20
|
+
export const WORK_DIR = ".chant/work";
|
|
21
|
+
|
|
22
|
+
/** The line `<git-common-dir>/info/exclude` gets, anchored at each checkout's top. */
|
|
23
|
+
export const WORK_DIR_EXCLUDE = `/${WORK_DIR}/`;
|
|
24
|
+
|
|
25
|
+
/** The directory where earlier versions made work worktrees, under the git common directory. */
|
|
26
|
+
export const LEGACY_WORK_DIR = "chant-work";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether a directory, given as a `/`-separated path relative to the root a
|
|
30
|
+
* walker started from, is a work directory: `.chant/work` at that root or at
|
|
31
|
+
* any depth below it. A walker skips it, since each worktree in it is a whole
|
|
32
|
+
* copy of the checkout.
|
|
33
|
+
*/
|
|
34
|
+
export function isWorkDir(rel: string): boolean {
|
|
35
|
+
return rel === WORK_DIR || rel.endsWith(`/${WORK_DIR}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The worktree for `id` under the checkout whose top is `top`. */
|
|
39
|
+
export function workWorktreePath(top: string, members: readonly string[], id: string): string {
|
|
40
|
+
return join(top, ...WORK_DIR.split("/"), ...members, id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The worktree an earlier version made for `id`, under the git common directory. */
|
|
44
|
+
export function legacyWorkWorktreePath(commonDir: string, members: readonly string[], id: string): string {
|
|
45
|
+
return join(commonDir, LEGACY_WORK_DIR, ...members, id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Add {@link WORK_DIR_EXCLUDE} to `<commonDir>/info/exclude` unless the file
|
|
50
|
+
* already has it, or `ignored` says git already ignores the work directory
|
|
51
|
+
* (a `.gitignore` of the project's, or the global excludes file). Returns
|
|
52
|
+
* whether it wrote the line.
|
|
53
|
+
*/
|
|
54
|
+
export function excludeWorkDir(commonDir: string, ignored: boolean): boolean {
|
|
55
|
+
const file = join(commonDir, "info", "exclude");
|
|
56
|
+
const text = existsSync(file) ? readFileSync(file, "utf-8") : "";
|
|
57
|
+
if (text.split(/\r?\n/).some((l) => l.trim() === WORK_DIR_EXCLUDE)) return false;
|
|
58
|
+
if (ignored) return false;
|
|
59
|
+
mkdirSync(join(commonDir, "info"), { recursive: true });
|
|
60
|
+
appendFileSync(file, `${text === "" || text.endsWith("\n") ? "" : "\n"}${WORK_DIR_EXCLUDE}\n`);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* After a work worktree is removed, remove the directories above it that are
|
|
66
|
+
* left empty, up to and including the checkout's `.chant`, so a run leaves
|
|
67
|
+
* nothing behind in a checkout that had no `.chant` before it.
|
|
68
|
+
*/
|
|
69
|
+
export function removeEmptyWorkDirs(worktree: string): void {
|
|
70
|
+
const marker = `${sep}${WORK_DIR.split("/").join(sep)}${sep}`;
|
|
71
|
+
const at = worktree.lastIndexOf(marker);
|
|
72
|
+
if (at < 0) return;
|
|
73
|
+
const stop = worktree.slice(0, at);
|
|
74
|
+
for (let dir = dirname(worktree); dir.length > stop.length; dir = dirname(dir)) {
|
|
75
|
+
try {
|
|
76
|
+
if (readdirSync(dir).length > 0) return;
|
|
77
|
+
rmdirSync(dir);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|