@intentius/chant 0.80.0 → 0.81.0
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/dist/audit/discover.d.ts +8 -16
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts +5 -0
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +13 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +4 -5
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +25 -9
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +8 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +6 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/vendor.d.ts +5 -0
- package/dist/cli/commands/vendor.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/init.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +7 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +7 -4
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/run-generate.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +4 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/vendor.d.ts +8 -3
- package/dist/cli/handlers/vendor.d.ts.map +1 -1
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +12 -7
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +32 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/fetch.d.ts +10 -0
- package/dist/codegen/fetch.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +3 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts +16 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/gate-plan.d.ts +40 -0
- package/dist/components/gate-plan.d.ts.map +1 -0
- package/dist/components/promote.d.ts +60 -7
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/components/sandbox/run.d.ts.map +1 -1
- package/dist/components/verbs/build-archive.d.ts +22 -11
- package/dist/components/verbs/build-archive.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/config-static.d.ts +59 -0
- package/dist/config-static.d.ts.map +1 -0
- package/dist/config.d.ts +32 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/content-digest.d.ts +17 -0
- package/dist/content-digest.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +10 -5
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +20 -4
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.d.ts +20 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +20 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/walk.d.ts +119 -0
- package/dist/discovery/walk.d.ts.map +1 -0
- package/dist/graph-ir.d.ts +4 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/join-key.d.ts +31 -0
- package/dist/join-key.d.ts.map +1 -0
- package/dist/lexicon-module.d.ts +51 -6
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon.d.ts +58 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/digest.d.ts +5 -1
- package/dist/lifecycle/digest.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +20 -5
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +64 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +8 -1
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +41 -8
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +67 -0
- package/dist/lifecycle/member-ledger.d.ts.map +1 -0
- package/dist/lifecycle/plan-digest.d.ts +4 -3
- package/dist/lifecycle/plan-digest.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +18 -2
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +90 -5
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +32 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts +75 -0
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +7 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +16 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +16 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/project-root.d.ts +26 -0
- package/dist/project-root.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
- package/dist/workspace/checks/generated.d.ts +156 -0
- package/dist/workspace/checks/generated.d.ts.map +1 -0
- package/dist/workspace/checks/ledgers.d.ts +78 -0
- package/dist/workspace/checks/ledgers.d.ts.map +1 -0
- package/dist/workspace/checks/links.d.ts +25 -0
- package/dist/workspace/checks/links.d.ts.map +1 -0
- package/dist/workspace/checks/pipelines.d.ts +76 -0
- package/dist/workspace/checks/pipelines.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +161 -0
- package/dist/workspace/checks.d.ts.map +1 -0
- package/dist/workspace/compose-graph.d.ts +115 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -0
- package/dist/workspace/compose-reports.d.ts +68 -0
- package/dist/workspace/compose-reports.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +220 -0
- package/dist/workspace/declaration.d.ts.map +1 -0
- package/dist/workspace/declaration.schema.json +434 -0
- package/dist/workspace/generated-files.d.ts +65 -0
- package/dist/workspace/generated-files.d.ts.map +1 -0
- package/dist/workspace/glob.d.ts +13 -0
- package/dist/workspace/glob.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +75 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +96 -0
- package/dist/workspace/init.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +38 -0
- package/dist/workspace/jsonc.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +192 -0
- package/dist/workspace/kinds.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +84 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -0
- package/dist/workspace/lineage-cli.d.ts +31 -0
- package/dist/workspace/lineage-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +101 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -0
- package/dist/workspace/lineage-lock.d.ts +258 -0
- package/dist/workspace/lineage-lock.d.ts.map +1 -0
- package/dist/workspace/lineage-merge.d.ts +14 -0
- package/dist/workspace/lineage-merge.d.ts.map +1 -0
- package/dist/workspace/lineage-migrations.d.ts +178 -0
- package/dist/workspace/lineage-migrations.d.ts.map +1 -0
- package/dist/workspace/lineage-update.d.ts +59 -0
- package/dist/workspace/lineage-update.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +146 -0
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
- package/dist/workspace/lineage-vendor.d.ts +63 -0
- package/dist/workspace/lineage-vendor.d.ts.map +1 -0
- package/dist/workspace/lineage-version.d.ts +32 -0
- package/dist/workspace/lineage-version.d.ts.map +1 -0
- package/dist/workspace/links.d.ts +150 -0
- package/dist/workspace/links.d.ts.map +1 -0
- package/dist/workspace/ls.d.ts +99 -0
- package/dist/workspace/ls.d.ts.map +1 -0
- package/dist/workspace/member-commands.d.ts +139 -0
- package/dist/workspace/member-commands.d.ts.map +1 -0
- package/dist/workspace/member-handles.d.ts +54 -0
- package/dist/workspace/member-handles.d.ts.map +1 -0
- package/dist/workspace/member-pipeline.d.ts +143 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -0
- package/dist/workspace/member-run.d.ts +67 -0
- package/dist/workspace/member-run.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +65 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +28 -4
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/root-refusal.d.ts +53 -0
- package/dist/workspace/root-refusal.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +150 -0
- package/dist/workspace/status.d.ts.map +1 -0
- package/dist/workspace/template-manifest.d.ts +90 -0
- package/dist/workspace/template-manifest.d.ts.map +1 -0
- package/dist/workspace/tree.d.ts +36 -0
- package/dist/workspace/tree.d.ts.map +1 -0
- package/dist/workspace/trust/attestor.d.ts +67 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +111 -0
- package/dist/workspace/trust/policy.d.ts.map +1 -0
- package/dist/workspace/trust/provenance.d.ts +67 -0
- package/dist/workspace/trust/provenance.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +43 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
- package/dist/workspace/trust/test-repo.d.ts +42 -0
- package/dist/workspace/trust/test-repo.d.ts.map +1 -0
- package/dist/workspace/trust/verify-cli.d.ts +9 -0
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
- package/dist/workspace/trust/verify.d.ts +66 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -0
- package/dist/workspace/which-chant.d.ts +58 -0
- package/dist/workspace/which-chant.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +133 -0
- package/package.json +2 -1
- package/src/audit/discover.ts +66 -78
- package/src/audit/report-html.ts +3 -1
- package/src/audit/report-model.ts +14 -1
- package/src/audit/terraform-state.test.ts +4 -9
- package/src/audit/terraform-state.ts +4 -5
- package/src/cli/commands/audit-truncation.test.ts +167 -0
- package/src/cli/commands/audit.ts +89 -60
- package/src/cli/commands/check-lexicon.ts +24 -0
- package/src/cli/commands/doctor.test.ts +30 -0
- package/src/cli/commands/doctor.ts +18 -0
- package/src/cli/commands/import-agents.ts +6 -2
- package/src/cli/commands/init.ts +22 -3
- package/src/cli/commands/lint.test.ts +17 -23
- package/src/cli/commands/lint.ts +24 -84
- package/src/cli/commands/update.ts +13 -4
- package/src/cli/commands/vendor.ts +11 -6
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
- package/src/cli/handlers/build-member.test.ts +127 -0
- package/src/cli/handlers/build.ts +35 -1
- package/src/cli/handlers/components.test.ts +127 -0
- package/src/cli/handlers/components.ts +70 -8
- package/src/cli/handlers/dev.ts +5 -3
- package/src/cli/handlers/fan-out.ts +9 -2
- package/src/cli/handlers/init.ts +54 -1
- package/src/cli/handlers/operator.test.ts +68 -0
- package/src/cli/handlers/operator.ts +37 -9
- package/src/cli/handlers/promote.test.ts +118 -2
- package/src/cli/handlers/promote.ts +82 -16
- package/src/cli/handlers/run-generate-member.test.ts +83 -0
- package/src/cli/handlers/run-generate.test.ts +6 -8
- package/src/cli/handlers/run-generate.ts +36 -1
- package/src/cli/handlers/run.test.ts +47 -0
- package/src/cli/handlers/run.ts +28 -5
- package/src/cli/handlers/vendor.ts +147 -43
- package/src/cli/main.test.ts +105 -1
- package/src/cli/main.ts +230 -17
- package/src/cli/plugins.ts +17 -17
- package/src/cli/registry.ts +32 -0
- package/src/cli/static-config-read.test.ts +208 -0
- package/src/codegen/fetch.test.ts +63 -1
- package/src/codegen/fetch.ts +30 -1
- package/src/components/cli-support.ts +6 -1
- package/src/components/discover.test.ts +27 -57
- package/src/components/discover.ts +13 -68
- package/src/components/driver.test.ts +78 -1
- package/src/components/driver.ts +43 -7
- package/src/components/gate-plan.ts +53 -0
- package/src/components/promote.test.ts +276 -2
- package/src/components/promote.ts +119 -15
- package/src/components/sandbox/driver.ts +8 -8
- package/src/components/sandbox/run.ts +4 -1
- package/src/components/verbs/build-archive.ts +27 -21
- package/src/components/verbs/run-agent.ts +3 -5
- package/src/config-static.test.ts +136 -0
- package/src/config-static.ts +232 -0
- package/src/config.test.ts +2 -2
- package/src/config.ts +48 -15
- package/src/content-digest.test.ts +120 -0
- package/src/content-digest.ts +21 -0
- package/src/discovery/files.test.ts +10 -10
- package/src/discovery/files.ts +25 -81
- package/src/discovery/fold-import.ts +49 -5
- package/src/discovery/index.ts +1 -0
- package/src/discovery/sandbox/driver.ts +73 -35
- package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
- package/src/discovery/sandbox/fork.ts +83 -43
- package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
- package/src/discovery/walk.test.ts +263 -0
- package/src/discovery/walk.ts +334 -0
- package/src/effect-receipt.ts +2 -2
- package/src/graph-ir.ts +4 -1
- package/src/join-key.test.ts +25 -0
- package/src/join-key.ts +39 -0
- package/src/lexicon-module.ts +108 -10
- package/src/lexicon.ts +79 -1
- package/src/lifecycle/build-ledger-store.ts +11 -10
- package/src/lifecycle/digest.ts +9 -5
- package/src/lifecycle/gate-ledger.test.ts +20 -0
- package/src/lifecycle/gate-ledger.ts +28 -6
- package/src/lifecycle/git.test.ts +13 -1
- package/src/lifecycle/git.ts +221 -176
- package/src/lifecycle/lease.ts +25 -10
- package/src/lifecycle/legacy-digest.test.ts +115 -23
- package/src/lifecycle/legacy-digest.ts +60 -19
- package/src/lifecycle/member-ledger.test.ts +319 -0
- package/src/lifecycle/member-ledger.ts +145 -0
- package/src/lifecycle/plan-digest.ts +7 -7
- package/src/lifecycle/release-ledger.test.ts +20 -0
- package/src/lifecycle/release-ledger.ts +23 -7
- package/src/lifecycle/status.test.ts +151 -1
- package/src/lifecycle/status.ts +228 -7
- package/src/op/activities/activity-contracts.ts +37 -0
- package/src/op/activities/index.ts +10 -0
- package/src/op/activities/propose-upgrade.ts +206 -0
- package/src/op/discover.test.ts +14 -4
- package/src/op/discover.ts +19 -27
- package/src/op/gate-name.ts +8 -0
- package/src/op/gate-summary.test.ts +14 -0
- package/src/op/gate-summary.ts +25 -2
- package/src/op/gate.ts +36 -7
- package/src/op/generate-pipeline.test.ts +14 -5
- package/src/project-root.test.ts +66 -1
- package/src/project-root.ts +48 -0
- package/src/workspace/__fixtures__/contract-repo.ts +100 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
- package/src/workspace/behold-kinds.test.ts +162 -0
- package/src/workspace/check-contract.test.ts +127 -0
- package/src/workspace/check.schema.json +347 -0
- package/src/workspace/checks/generated.test.ts +287 -0
- package/src/workspace/checks/generated.ts +475 -0
- package/src/workspace/checks/ledgers.test.ts +139 -0
- package/src/workspace/checks/ledgers.ts +246 -0
- package/src/workspace/checks/links.ts +183 -0
- package/src/workspace/checks/pipelines.test.ts +133 -0
- package/src/workspace/checks/pipelines.ts +225 -0
- package/src/workspace/checks-repo.test.ts +71 -0
- package/src/workspace/checks.test.ts +450 -0
- package/src/workspace/checks.ts +461 -0
- package/src/workspace/compose-graph.test.ts +146 -0
- package/src/workspace/compose-graph.ts +228 -0
- package/src/workspace/compose-reports.test.ts +109 -0
- package/src/workspace/compose-reports.ts +186 -0
- package/src/workspace/declaration.schema.json +434 -0
- package/src/workspace/declaration.test.ts +347 -0
- package/src/workspace/declaration.ts +623 -0
- package/src/workspace/generated-files.test.ts +114 -0
- package/src/workspace/generated-files.ts +94 -0
- package/src/workspace/glob.ts +49 -0
- package/src/workspace/graph-cli.ts +220 -0
- package/src/workspace/graph-contract.test.ts +204 -0
- package/src/workspace/graph.schema.json +662 -0
- package/src/workspace/init.test.ts +223 -0
- package/src/workspace/init.ts +585 -0
- package/src/workspace/jsonc.test.ts +65 -0
- package/src/workspace/jsonc.ts +231 -0
- package/src/workspace/kinds.test.ts +288 -0
- package/src/workspace/kinds.ts +506 -0
- package/src/workspace/lineage-check.ts +264 -0
- package/src/workspace/lineage-cli.ts +114 -0
- package/src/workspace/lineage-init.test.ts +231 -0
- package/src/workspace/lineage-init.ts +365 -0
- package/src/workspace/lineage-lock.test.ts +224 -0
- package/src/workspace/lineage-lock.ts +357 -0
- package/src/workspace/lineage-merge.ts +44 -0
- package/src/workspace/lineage-migrations.test.ts +178 -0
- package/src/workspace/lineage-migrations.ts +343 -0
- package/src/workspace/lineage-update.ts +160 -0
- package/src/workspace/lineage-upgrade-cli.ts +151 -0
- package/src/workspace/lineage-upgrade.test.ts +474 -0
- package/src/workspace/lineage-upgrade.ts +624 -0
- package/src/workspace/lineage-vendor.test.ts +126 -0
- package/src/workspace/lineage-vendor.ts +215 -0
- package/src/workspace/lineage-version.ts +127 -0
- package/src/workspace/links.test.ts +344 -0
- package/src/workspace/links.ts +331 -0
- package/src/workspace/ls-contract.test.ts +200 -0
- package/src/workspace/ls.schema.json +190 -0
- package/src/workspace/ls.ts +254 -0
- package/src/workspace/member-commands.e2e.test.ts +244 -0
- package/src/workspace/member-commands.test.ts +217 -0
- package/src/workspace/member-commands.ts +540 -0
- package/src/workspace/member-handles.test.ts +76 -0
- package/src/workspace/member-handles.ts +228 -0
- package/src/workspace/member-pipeline.test.ts +135 -0
- package/src/workspace/member-pipeline.ts +317 -0
- package/src/workspace/member-run.ts +146 -0
- package/src/workspace/read-contract.test.ts +129 -0
- package/src/workspace/reason-codes.test.ts +113 -0
- package/src/workspace/reason-codes.ts +81 -0
- package/src/workspace/records-cli.ts +86 -14
- package/src/workspace/records-contract.test.ts +5 -0
- package/src/workspace/records.schema.json +32 -2
- package/src/workspace/records.ts +3 -2
- package/src/workspace/root-refusal.ts +97 -0
- package/src/workspace/status-contract.test.ts +308 -0
- package/src/workspace/status.schema.json +210 -0
- package/src/workspace/status.ts +412 -0
- package/src/workspace/template-manifest.test.ts +79 -0
- package/src/workspace/template-manifest.ts +221 -0
- package/src/workspace/tree.ts +132 -0
- package/src/workspace/trust/attestor.ts +93 -0
- package/src/workspace/trust/policy.ts +306 -0
- package/src/workspace/trust/provenance.ts +181 -0
- package/src/workspace/trust/ssh-commit.ts +138 -0
- package/src/workspace/trust/test-repo.ts +138 -0
- package/src/workspace/trust/trust.test.ts +397 -0
- package/src/workspace/trust/verify-cli.ts +56 -0
- package/src/workspace/trust/verify.ts +194 -0
- package/src/workspace/which-chant.test.ts +117 -0
- package/src/workspace/which-chant.ts +151 -0
- package/src/workspace/workspace-kinds.schema.json +133 -0
- package/dist/components/unbound-gate-approval.d.ts +0 -24
- package/dist/components/unbound-gate-approval.d.ts.map +0 -1
- package/dist/discovery/convergence.d.ts +0 -97
- package/dist/discovery/convergence.d.ts.map +0 -1
- package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
- package/src/components/unbound-gate-approval.test.ts +0 -82
- package/src/components/unbound-gate-approval.ts +0 -51
- package/src/discovery/convergence.test.ts +0 -231
- package/src/discovery/convergence.ts +0 -399
- package/src/op/discover-convergence.test.ts +0 -70
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant components promote` (#2530),
|
|
2
|
+
* `chant components promote` (#2530), `rollback` (#2531) and `redeploy`
|
|
3
|
+
* (#2604), with the ledger, discovery and the
|
|
3
4
|
* capability registry replaced by fakes.
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -66,7 +67,7 @@ vi.mock("../../op/gate", async () => {
|
|
|
66
67
|
// The run handler pulls in every runtime; the promote needs only its exit code.
|
|
67
68
|
vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
|
|
68
69
|
|
|
69
|
-
const { runComponentsPromote, runComponentsRollback } = await import("./promote");
|
|
70
|
+
const { runComponentsPromote, runComponentsRollback, runComponentsRedeploy } = await import("./promote");
|
|
70
71
|
|
|
71
72
|
const api: DriverComponent = {
|
|
72
73
|
name: "api",
|
|
@@ -197,6 +198,37 @@ describe("chant components promote", () => {
|
|
|
197
198
|
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ component: "api", env: "prod" });
|
|
198
199
|
});
|
|
199
200
|
|
|
201
|
+
test("--digest <component>=<digest> promotes the pinned release, not the latest one (#2602)", async () => {
|
|
202
|
+
const web: DriverComponent = { ...api, name: "web" };
|
|
203
|
+
resolveTargetsMock.mockResolvedValue({ success: true, targets: [api, web] });
|
|
204
|
+
// Another pipeline recorded sha256:newer after this one recorded sha256:api.
|
|
205
|
+
readReleaseLedgerMock.mockResolvedValue({
|
|
206
|
+
records: [staging, { ...staging, digest: "sha256:newer", timestamp: "2026-01-03T00:00:00.000Z", runId: "run-8" }, { ...staging, component: "web" }],
|
|
207
|
+
malformed: 0,
|
|
208
|
+
});
|
|
209
|
+
expect(await runComponentsPromote(ctx({ digests: ["api=sha256:api"] }))).toBe(0);
|
|
210
|
+
expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({
|
|
212
|
+
component: "api",
|
|
213
|
+
env: "prod",
|
|
214
|
+
digest: "sha256:api",
|
|
215
|
+
promotedFrom: { env: "staging", runId: "run-7" },
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("an empty pinned digest fails and deploys nothing (#2602)", async () => {
|
|
220
|
+
expect(await runComponentsPromote(ctx({ digests: ["api="] }))).toBe(1);
|
|
221
|
+
expect(errors.join("\n")).toMatch(/--digest api= names no digest/);
|
|
222
|
+
expect(ran).toEqual([]);
|
|
223
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a bare --digest without --component is refused", async () => {
|
|
227
|
+
expect(await runComponentsPromote(ctx({ digest: "sha256:api", digests: ["sha256:api"] }))).toBe(1);
|
|
228
|
+
expect(errors.join("\n")).toMatch(/needs --component/);
|
|
229
|
+
expect(ran).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
200
232
|
test("a component with no publish step is refused before anything runs", async () => {
|
|
201
233
|
resolveTargetsMock.mockResolvedValue({
|
|
202
234
|
success: true,
|
|
@@ -259,3 +291,87 @@ describe("chant components rollback", () => {
|
|
|
259
291
|
expect(errors.join("\n")).toMatch(/chant approve api rollback-ok/);
|
|
260
292
|
});
|
|
261
293
|
});
|
|
294
|
+
|
|
295
|
+
describe("chant components redeploy", () => {
|
|
296
|
+
const prod = (digest: string, timestamp: string, runId: string): ReleaseRecord =>
|
|
297
|
+
({ ...staging, env: "prod", digest, timestamp, runId });
|
|
298
|
+
|
|
299
|
+
beforeEach(() => {
|
|
300
|
+
readReleaseLedgerMock.mockResolvedValue({
|
|
301
|
+
records: [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
|
|
302
|
+
malformed: 0,
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const rd = (a: Partial<ParsedArgs>) => ctx({ migrateFrom: undefined, migrateTo: undefined, path: "redeploy", extraPositional: "prod", component: "api", ...a });
|
|
307
|
+
|
|
308
|
+
test("an environment and --component are required", async () => {
|
|
309
|
+
expect(await runComponentsRedeploy(rd({ extraPositional: undefined }))).toBe(1);
|
|
310
|
+
expect(errors.join("\n")).toMatch(/--component <name> are required/);
|
|
311
|
+
expect(errors.join("\n")).toMatch(/chant components redeploy <env>/);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("redeploys the current release from its recorded digest and records it", async () => {
|
|
315
|
+
expect(await runComponentsRedeploy(rd({}))).toBe(0);
|
|
316
|
+
expect(readReleaseLedgerMock).toHaveBeenCalledWith("prod");
|
|
317
|
+
// Neither a build nor a publish: the environment already has the artifact.
|
|
318
|
+
expect(ran).toEqual(["prod:apply"]);
|
|
319
|
+
expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
|
|
320
|
+
const written = appendReleaseRecordMock.mock.calls[0][0];
|
|
321
|
+
expect(written).toMatchObject({
|
|
322
|
+
component: "api",
|
|
323
|
+
env: "prod",
|
|
324
|
+
digest: "sha256:new",
|
|
325
|
+
runId: "run-9",
|
|
326
|
+
actor: "bob",
|
|
327
|
+
redeploys: { env: "prod", runId: "run-2", timestamp: "2026-01-02T00:00:00.000Z" },
|
|
328
|
+
});
|
|
329
|
+
expect(written).not.toHaveProperty("restores");
|
|
330
|
+
expect(written).not.toHaveProperty("promotedFrom");
|
|
331
|
+
expect(pushLifecycleMock).toHaveBeenCalledTimes(1);
|
|
332
|
+
expect(errors.join("\n")).toMatch(/Redeployed .*api.* in prod at sha256:new/);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("the same digest rollback refuses is the one redeploy accepts", async () => {
|
|
336
|
+
expect(await runComponentsRollback(rd({ path: "rollback", digest: "sha256:new" }))).toBe(1);
|
|
337
|
+
expect(errors.join("\n")).toMatch(/already the current release/);
|
|
338
|
+
expect(await runComponentsRedeploy(rd({ digest: "sha256:new" }))).toBe(0);
|
|
339
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ digest: "sha256:new" });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("a --digest other than the current one is refused before anything runs", async () => {
|
|
343
|
+
expect(await runComponentsRedeploy(rd({ digest: "sha256:old" }))).toBe(1);
|
|
344
|
+
expect(errors.join("\n")).toMatch(/sha256:old is not the current release/);
|
|
345
|
+
expect(ran).toEqual([]);
|
|
346
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("--dry-run prints the plan and deploys nothing", async () => {
|
|
350
|
+
expect(await runComponentsRedeploy(rd({ dryRun: true, json: true }))).toBe(0);
|
|
351
|
+
expect(ran).toEqual([]);
|
|
352
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
353
|
+
const plan = JSON.parse(logs.join("\n"));
|
|
354
|
+
expect(plan).toMatchObject({ from: "prod", to: "prod" });
|
|
355
|
+
expect(plan.items).toEqual([expect.objectContaining({ component: "api", digest: "sha256:new", notRun: ["docker-build", "publish-image"] })]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("the environment's gate applies to a redeploy", async () => {
|
|
359
|
+
const gated: DriverComponent = {
|
|
360
|
+
...api,
|
|
361
|
+
deploy: [...api.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "gate", gate: "redeploy-ok" }, { kind: "apply" }] }],
|
|
362
|
+
};
|
|
363
|
+
resolveTargetsMock.mockResolvedValue({ success: true, targets: [gated] });
|
|
364
|
+
expect(await runComponentsRedeploy(rd({}))).toBe(3);
|
|
365
|
+
expect(ran).not.toContain("prod:apply");
|
|
366
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
367
|
+
expect(errors.join("\n")).toMatch(/chant approve api redeploy-ok/);
|
|
368
|
+
expect(errors.join("\n")).toMatch(/then run the same redeploy again/);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("a failed redeploy records nothing and exits 1", async () => {
|
|
372
|
+
failApplyFor = "api";
|
|
373
|
+
expect(await runComponentsRedeploy(rd({}))).toBe(1);
|
|
374
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
375
|
+
expect(errors.join("\n")).toMatch(/redeploy failed at "api"/);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant components promote --from <env> --to <env>` (#2530)
|
|
3
|
-
* components rollback <env>` (#2531)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* `chant components promote --from <env> --to <env>` (#2530), `chant
|
|
3
|
+
* components rollback <env>` (#2531) and `chant components redeploy <env>`
|
|
4
|
+
* (#2604): deploy digests a release ledger already records, without a build.
|
|
5
|
+
* A promote takes them from another environment's ledger; a rollback takes an
|
|
6
|
+
* earlier release from the environment's own, and a redeploy the release it
|
|
7
|
+
* records as current. The
|
|
6
8
|
* mechanism lives in ../../components/promote.ts; these handlers read the
|
|
7
9
|
* ledger, print the plan, run it, and append the release records.
|
|
8
10
|
*
|
|
@@ -29,34 +31,39 @@ import { fanOutRegistry } from "../../components/fan-out-support";
|
|
|
29
31
|
import { renderDriverHuman } from "../../components/driver-output";
|
|
30
32
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
31
33
|
import {
|
|
34
|
+
parseDigestPins,
|
|
32
35
|
planPromotion,
|
|
33
36
|
planRollback,
|
|
37
|
+
planRedeploy,
|
|
34
38
|
withoutBuildSteps,
|
|
35
39
|
runPromotion,
|
|
36
40
|
promotionRecord,
|
|
37
41
|
rollbackRecord,
|
|
42
|
+
redeployRecord,
|
|
38
43
|
gateApprover,
|
|
39
44
|
type DeployRunInfo,
|
|
40
45
|
type PromotionItem,
|
|
41
46
|
type PromotionPlan,
|
|
42
47
|
} from "../../components/promote";
|
|
43
48
|
import type { DriverComponent } from "../../components/driver";
|
|
44
|
-
import { writeGatedRunSummary } from "../../op/gate-summary";
|
|
45
|
-
import { approveCommand } from "../../op/gate";
|
|
49
|
+
import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
|
|
50
|
+
import { approveCommand, describeGateMismatch } from "../../op/gate";
|
|
46
51
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
47
52
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
48
53
|
import { GATED_EXIT_CODE } from "./run";
|
|
49
54
|
import type { CommandContext } from "../registry";
|
|
50
55
|
|
|
51
56
|
const PROMOTE_USAGE =
|
|
52
|
-
"chant components promote --from <env> --to <env> [--component <name> [--digest <sha256:...>]] [--dry-run] [--json]";
|
|
57
|
+
"chant components promote --from <env> --to <env> [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json]";
|
|
53
58
|
const ROLLBACK_USAGE =
|
|
54
59
|
"chant components rollback <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
|
|
60
|
+
const REDEPLOY_USAGE =
|
|
61
|
+
"chant components redeploy <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
|
|
55
62
|
|
|
56
|
-
type Verb = "promote" | "rollback";
|
|
63
|
+
type Verb = "promote" | "rollback" | "redeploy";
|
|
57
64
|
|
|
58
65
|
function renderPlan(verb: Verb, plan: PromotionPlan, removed: Map<string, string[]>): void {
|
|
59
|
-
console.error(formatBold(verb === "promote" ? `promote ${plan.from} -> ${plan.to}` :
|
|
66
|
+
console.error(formatBold(verb === "promote" ? `promote ${plan.from} -> ${plan.to}` : `${verb} ${plan.to}`));
|
|
60
67
|
for (const item of plan.items) {
|
|
61
68
|
console.error(` ${item.component} ${item.digest}`);
|
|
62
69
|
console.error(` released to ${plan.from} at ${item.source.timestamp} (run ${item.source.runId}, git ${item.source.gitSha.slice(0, 12)})`);
|
|
@@ -117,9 +124,9 @@ async function readLedger(env: string): Promise<ReleaseRecord[]> {
|
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|
|
120
|
-
* Prepare, run and record a plan. Shared by promote and
|
|
121
|
-
* only in how the plan was chosen, whether the publish step runs,
|
|
122
|
-
* field names the earlier release on the new record.
|
|
127
|
+
* Prepare, run and record a plan. Shared by promote, rollback and redeploy,
|
|
128
|
+
* which differ only in how the plan was chosen, whether the publish step runs,
|
|
129
|
+
* and which field names the earlier release on the new record.
|
|
123
130
|
*/
|
|
124
131
|
async function deployPlan(
|
|
125
132
|
ctx: CommandContext,
|
|
@@ -135,7 +142,12 @@ async function deployPlan(
|
|
|
135
142
|
const removed = new Map<string, string[]>();
|
|
136
143
|
const refusals: string[] = [];
|
|
137
144
|
for (const item of plan.items) {
|
|
138
|
-
|
|
145
|
+
// A rollback and a redeploy deploy a release this environment already
|
|
146
|
+
// received, so neither runs the publish step.
|
|
147
|
+
const result = withoutBuildSteps(
|
|
148
|
+
byName.get(item.component)!,
|
|
149
|
+
verb === "promote" ? {} : { pinDigest: item.digest, verb: `a ${verb}` },
|
|
150
|
+
);
|
|
139
151
|
if ("error" in result) refusals.push(result.error);
|
|
140
152
|
else {
|
|
141
153
|
prepared.push(result.component);
|
|
@@ -217,7 +229,9 @@ async function deployPlan(
|
|
|
217
229
|
console.error(formatSuccess(
|
|
218
230
|
verb === "promote"
|
|
219
231
|
? `Promoted ${formatBold(r.component)} ${plan.from} -> ${plan.to}: ${r.digest}`
|
|
220
|
-
:
|
|
232
|
+
: verb === "rollback"
|
|
233
|
+
? `Rolled back ${formatBold(r.component)} in ${plan.to} to ${r.digest}`
|
|
234
|
+
: `Redeployed ${formatBold(r.component)} in ${plan.to} at ${r.digest}`,
|
|
221
235
|
));
|
|
222
236
|
}
|
|
223
237
|
}
|
|
@@ -233,7 +247,11 @@ async function deployPlan(
|
|
|
233
247
|
if (run.status === "gated" && run.gate) {
|
|
234
248
|
const pending = run.gate;
|
|
235
249
|
console.error(formatWarning({ message: `component "${run.gatedComponent}" is gated on "${pending.gate}" — pending approval` }));
|
|
236
|
-
|
|
250
|
+
// #2574: an approval stands, but for another environment, plan or none.
|
|
251
|
+
if (run.gateMismatch) {
|
|
252
|
+
console.error(formatWarning({ message: describeGateMismatch(pending.op, pending.gate, run.gateMismatch) }));
|
|
253
|
+
}
|
|
254
|
+
console.error(formatInfo(`approve : ${approveCommand(pending.op, pending.gate, pending.environment)}`));
|
|
237
255
|
console.error(formatInfo(`then run the same ${verb} again`));
|
|
238
256
|
writeGatedRunSummary({
|
|
239
257
|
op: pending.op,
|
|
@@ -242,6 +260,8 @@ async function deployPlan(
|
|
|
242
260
|
expiresAt: pending.expiresAt,
|
|
243
261
|
...(pending.url ? { url: pending.url } : {}),
|
|
244
262
|
...(pending.planDigest ? { planDigest: pending.planDigest } : {}),
|
|
263
|
+
...(await summaryLedgerPrefix()),
|
|
264
|
+
...(pending.environment ? { environment: pending.environment } : {}),
|
|
245
265
|
});
|
|
246
266
|
return GATED_EXIT_CODE;
|
|
247
267
|
}
|
|
@@ -280,6 +300,23 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
280
300
|
const project = await loadProject(ctx, args.component ?? "all");
|
|
281
301
|
if (typeof project === "number") return project;
|
|
282
302
|
|
|
303
|
+
// Without --component, each --digest is <component>=<digest> (#2602): a
|
|
304
|
+
// generated promote job pins every component to the release its own
|
|
305
|
+
// pipeline run recorded, rather than whatever is latest in --from.
|
|
306
|
+
const digests = args.digests ?? (args.digest ? [args.digest] : []);
|
|
307
|
+
let pins: Record<string, string> | undefined;
|
|
308
|
+
if (!args.component && digests.length > 0) {
|
|
309
|
+
const parsed = parseDigestPins(digests);
|
|
310
|
+
if ("error" in parsed) {
|
|
311
|
+
console.error(formatError({ message: parsed.error, hint: PROMOTE_USAGE }));
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
pins = parsed.pins;
|
|
315
|
+
} else if (args.component && digests.length > 1) {
|
|
316
|
+
console.error(formatError({ message: "--component takes one --digest", hint: PROMOTE_USAGE }));
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
283
320
|
await fetchLifecycle().catch(() => false);
|
|
284
321
|
const plan = planPromotion({
|
|
285
322
|
from,
|
|
@@ -287,7 +324,8 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
287
324
|
sourceRecords: await readLedger(from),
|
|
288
325
|
declared: project.targets.map((c) => c.name),
|
|
289
326
|
...(args.component ? { component: args.component } : {}),
|
|
290
|
-
...(args.digest ? { digest: args.digest } : {}),
|
|
327
|
+
...(args.component && args.digest ? { digest: args.digest } : {}),
|
|
328
|
+
...(pins ? { pins } : {}),
|
|
291
329
|
});
|
|
292
330
|
if ("error" in plan) {
|
|
293
331
|
console.error(formatError({ message: plan.error, hint: PROMOTE_USAGE }));
|
|
@@ -323,3 +361,31 @@ export async function runComponentsRollback(ctx: CommandContext): Promise<number
|
|
|
323
361
|
}
|
|
324
362
|
return deployPlan(ctx, "rollback", plan, project, who.actor, rollbackRecord);
|
|
325
363
|
}
|
|
364
|
+
|
|
365
|
+
export async function runComponentsRedeploy(ctx: CommandContext): Promise<number> {
|
|
366
|
+
const { args } = ctx;
|
|
367
|
+
const env = args.extraPositional;
|
|
368
|
+
if (!env || !args.component) {
|
|
369
|
+
console.error(formatError({ message: "an environment and --component <name> are required", hint: REDEPLOY_USAGE }));
|
|
370
|
+
return 1;
|
|
371
|
+
}
|
|
372
|
+
const who = actorOrRefuse(ctx);
|
|
373
|
+
if (typeof who === "number") return who;
|
|
374
|
+
|
|
375
|
+
const project = await loadProject(ctx, args.component);
|
|
376
|
+
if (typeof project === "number") return project;
|
|
377
|
+
|
|
378
|
+
await fetchLifecycle().catch(() => false);
|
|
379
|
+
const plan = planRedeploy({
|
|
380
|
+
env,
|
|
381
|
+
records: await readLedger(env),
|
|
382
|
+
declared: project.targets.map((c) => c.name),
|
|
383
|
+
component: args.component,
|
|
384
|
+
...(args.digest ? { digest: args.digest } : {}),
|
|
385
|
+
});
|
|
386
|
+
if ("error" in plan) {
|
|
387
|
+
console.error(formatError({ message: plan.error, hint: REDEPLOY_USAGE }));
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
return deployPlan(ctx, "redeploy", plan, project, who.actor, redeployRecord);
|
|
391
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant run --generate` inside a workspace member (#2542). Discovery is
|
|
3
|
+
* stubbed; the lexicon plugins are the real ones.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import type { ParsedArgs } from "../registry";
|
|
10
|
+
import type { OpConfig } from "../../op/types";
|
|
11
|
+
import { twoMemberWorkspace } from "../../workspace/__fixtures__/two-member-workspace";
|
|
12
|
+
|
|
13
|
+
const discovered = new Map<string, { config: Partial<OpConfig>; filePath: string; exportName: string }>();
|
|
14
|
+
vi.mock("../../op/discover", () => ({
|
|
15
|
+
discoverOps: async () => ({ ops: discovered, errors: [] }),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const { runOp } = await import("./run");
|
|
19
|
+
const { generateOpsPipeline } = await import("../../op/generate-pipeline");
|
|
20
|
+
const { parseArgs } = await import("../main");
|
|
21
|
+
|
|
22
|
+
function ctx(argv: string[]) {
|
|
23
|
+
return { args: parseArgs(argv) as ParsedArgs, plugins: [], serializers: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let repo: string;
|
|
27
|
+
let cwd: string;
|
|
28
|
+
let logs: string[];
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
repo = twoMemberWorkspace();
|
|
31
|
+
cwd = process.cwd();
|
|
32
|
+
discovered.clear();
|
|
33
|
+
discovered.set("nightly-audit", {
|
|
34
|
+
config: { name: "nightly-audit", phases: [], schedule: { cron: "0 6 * * *" } } as Partial<OpConfig>,
|
|
35
|
+
filePath: "nightly-audit.op.ts",
|
|
36
|
+
exportName: "default",
|
|
37
|
+
});
|
|
38
|
+
logs = [];
|
|
39
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
40
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => {
|
|
41
|
+
logs.push(s);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
process.chdir(cwd);
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
rmSync(repo, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("chant run --generate in a workspace member", () => {
|
|
51
|
+
test("github: the file is named after the member, at the repository root, and recorded", async () => {
|
|
52
|
+
process.chdir(join(repo, "services/api"));
|
|
53
|
+
expect(await runOp(ctx(["run", "--generate", "github"]))).toBe(0);
|
|
54
|
+
|
|
55
|
+
expect(readdirSync(join(repo, ".github/workflows"))).toEqual(["api-nightly-audit.yml"]);
|
|
56
|
+
expect(existsSync(join(repo, "services/api/.github"))).toBe(false);
|
|
57
|
+
const member = { name: "api", dir: "services/api", fileDir: ".github/workflows" };
|
|
58
|
+
const direct = await generateOpsPipeline([{ name: "nightly-audit" }], "github", { member });
|
|
59
|
+
const text = readFileSync(join(repo, ".github/workflows/api-nightly-audit.yml"), "utf-8");
|
|
60
|
+
expect(text).toBe(
|
|
61
|
+
"# Generated by chant — do not edit directly. Regenerate with: chant run --generate github (in the member's directory)\n" + direct.files![0].yaml,
|
|
62
|
+
);
|
|
63
|
+
const record = JSON.parse(readFileSync(join(repo, "services/api/.chant/generated.json"), "utf-8"));
|
|
64
|
+
expect(record.files).toEqual([{ path: ".github/workflows/api-nightly-audit.yml", command: "chant run --generate github" }]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("gitlab: the member's file goes in .gitlab/ci", async () => {
|
|
68
|
+
process.chdir(join(repo, "apps/web"));
|
|
69
|
+
expect(await runOp(ctx(["run", "--generate", "gitlab"]))).toBe(0);
|
|
70
|
+
const text = readFileSync(join(repo, ".gitlab/ci/web-ops.gitlab-ci.yml"), "utf-8");
|
|
71
|
+
expect(text).toContain("web-nightly-audit:");
|
|
72
|
+
expect(text).toContain("cd apps/web");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("--format json names the member and writes nothing", async () => {
|
|
76
|
+
process.chdir(join(repo, "services/api"));
|
|
77
|
+
expect(await runOp(ctx(["run", "--generate", "forgejo", "--format", "json"]))).toBe(0);
|
|
78
|
+
const out = JSON.parse(logs.join("\n"));
|
|
79
|
+
expect(out.member).toBe("api");
|
|
80
|
+
expect(out.files.map((f: { path: string }) => f.path)).toEqual([".forgejo/workflows/api-nightly-audit.yml"]);
|
|
81
|
+
expect(existsSync(join(repo, ".forgejo"))).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -86,16 +86,14 @@ describe("chant run --generate <provider>", () => {
|
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test("each github workflow is named after its Op
|
|
89
|
+
test("each github (#2580) and forgejo (#2601) workflow is named after its Op", async () => {
|
|
90
90
|
for (const provider of ["github", "forgejo"]) {
|
|
91
91
|
expect(await runOp(ctx(["run", "--generate", provider, "--output", join(dir, provider)]))).toBe(0);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const forgejo = readFileSync(join(dir, "forgejo", `${op}.yml`), "utf-8");
|
|
98
|
-
expect(forgejo).not.toMatch(/^name:/m);
|
|
92
|
+
const header = `# ${GENERATED_MARKER}. Regenerate with: chant run --generate ${provider}\n`;
|
|
93
|
+
for (const op of ["nightly-audit", "weekly-report"]) {
|
|
94
|
+
const written = readFileSync(join(dir, provider, `${op}.yml`), "utf-8");
|
|
95
|
+
expect(written.startsWith(`${header}name: ${op}\n\non:\n`)).toBe(true);
|
|
96
|
+
}
|
|
99
97
|
}
|
|
100
98
|
});
|
|
101
99
|
|
|
@@ -28,6 +28,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
28
28
|
import { discoverOps } from "../../op/discover";
|
|
29
29
|
import { generateOpsPipeline } from "../../op/generate-pipeline";
|
|
30
30
|
import { GENERATED_MARKER } from "../../discovery/files";
|
|
31
|
+
import { findWorkspaceRoot } from "../../project-root";
|
|
31
32
|
import type { ComponentPipelineOptions, ScheduledOpSpec } from "../../lexicon";
|
|
32
33
|
import { formatBold, formatError, formatSuccess } from "../format";
|
|
33
34
|
import type { CommandContext } from "../registry";
|
|
@@ -104,12 +105,46 @@ export async function runOpGenerate(ctx: CommandContext): Promise<number> {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
// Inside a workspace member (#2542), the files are the member's: filtered
|
|
109
|
+
// to its paths, run in its directory, named after it at the repository
|
|
110
|
+
// root's forge directory, and recorded as its generated files. The
|
|
111
|
+
// workspace module loads only when a declaration was found.
|
|
112
|
+
const found = findWorkspaceRoot(process.cwd());
|
|
113
|
+
let plan: import("../../workspace/member-pipeline").MemberOpPlan | undefined;
|
|
114
|
+
let memberPipeline: typeof import("../../workspace/member-pipeline") | undefined;
|
|
115
|
+
if (found) {
|
|
116
|
+
memberPipeline = await import("../../workspace/member-pipeline");
|
|
117
|
+
try {
|
|
118
|
+
plan = memberPipeline.planMemberOpPipelines({ projectDir: process.cwd(), found, provider, output: args.output, specFile: specPath });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(formatError({ message: memberPipeline.describeError(err) }));
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = await generateOpsPipeline(ops, provider, plan ? { ...options, member: plan.member } : options);
|
|
108
126
|
if (!result.success) {
|
|
109
127
|
console.error(formatError({ message: result.error ?? "Failed to generate Op pipelines" }));
|
|
110
128
|
return 1;
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
if (plan && memberPipeline) {
|
|
132
|
+
const header = memberPipeline.generatedHeader(plan.command);
|
|
133
|
+
if (args.format === "json") {
|
|
134
|
+
const files = (result.files ?? []).map((f) => ({
|
|
135
|
+
name: f.name,
|
|
136
|
+
path: memberPipeline.repoRelative(plan.ctx, join(plan.outDir, f.name)),
|
|
137
|
+
content: header + f.yaml,
|
|
138
|
+
}));
|
|
139
|
+
console.log(JSON.stringify({ member: plan.member.name, files, jobs: result.jobs ?? [] }, null, 2));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
for (const path of memberPipeline.writeMemberOpPipelines(plan, result.files ?? [])) {
|
|
143
|
+
console.error(formatSuccess(`wrote ${formatBold(path)} for member ${formatBold(plan.member.name)}`));
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
113
148
|
const header = opPipelineHeader(provider, specPath);
|
|
114
149
|
const outDir = args.output ?? DEFAULT_OP_PIPELINE_DIRS[provider] ?? ".";
|
|
115
150
|
const files = (result.files ?? []).map((f) => ({
|
|
@@ -1157,6 +1157,53 @@ describe("runOpComponents", () => {
|
|
|
1157
1157
|
vi.restoreAllMocks();
|
|
1158
1158
|
});
|
|
1159
1159
|
|
|
1160
|
+
test("--digest-file writes <component>=<digest> for each release the run recorded (#2602)", async () => {
|
|
1161
|
+
runComponentsMock.mockResolvedValue({
|
|
1162
|
+
success: true,
|
|
1163
|
+
selected: ["svc", "infra"],
|
|
1164
|
+
run: {
|
|
1165
|
+
order: ["svc", "infra"],
|
|
1166
|
+
waves: [["svc", "infra"]],
|
|
1167
|
+
results: [
|
|
1168
|
+
{ component: "svc", ok: true, status: "ok", records: [] },
|
|
1169
|
+
{ component: "infra", ok: true, status: "ok", records: [] },
|
|
1170
|
+
],
|
|
1171
|
+
ok: true,
|
|
1172
|
+
status: "ok",
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
maybeRecordAutoReleaseMock.mockImplementation(async (info: { component: string }) =>
|
|
1176
|
+
info.component === "svc"
|
|
1177
|
+
? { recorded: true, commit: "a".repeat(40), record: { version: 1, component: "svc", env: "staging", digest: "sha256:abc", gitSha: "x", runId: "local-1", timestamp: "t", actor: "a" } }
|
|
1178
|
+
: { recorded: false, reason: "no-digest" });
|
|
1179
|
+
writeFileSyncMock.mockClear();
|
|
1180
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
1181
|
+
|
|
1182
|
+
const exit = await runOpComponents({ args: makeArgs({ path: "all", env: "staging", digestFile: "out/svc.digest" }), plugins: [], serializers: [] });
|
|
1183
|
+
|
|
1184
|
+
expect(exit).toBe(0);
|
|
1185
|
+
const call = writeFileSyncMock.mock.calls.find(([path]) => String(path).endsWith("out/svc.digest"));
|
|
1186
|
+
expect(call?.[1]).toBe("svc=sha256:abc\n");
|
|
1187
|
+
vi.restoreAllMocks();
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test("--digest-file is written empty when nothing was recorded (#2602)", async () => {
|
|
1191
|
+
runComponentsMock.mockResolvedValue({
|
|
1192
|
+
success: true,
|
|
1193
|
+
selected: ["svc"],
|
|
1194
|
+
run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
|
|
1195
|
+
});
|
|
1196
|
+
maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
|
|
1197
|
+
writeFileSyncMock.mockClear();
|
|
1198
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
1199
|
+
|
|
1200
|
+
await runOpComponents({ args: makeArgs({ path: "svc", digestFile: "svc.digest" }), plugins: [], serializers: [] });
|
|
1201
|
+
|
|
1202
|
+
const call = writeFileSyncMock.mock.calls.find(([path]) => String(path).endsWith("svc.digest"));
|
|
1203
|
+
expect(call?.[1]).toBe("");
|
|
1204
|
+
vi.restoreAllMocks();
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1160
1207
|
test("a release-write error is surfaced as a warning but does not change the exit code", async () => {
|
|
1161
1208
|
runComponentsMock.mockResolvedValue({
|
|
1162
1209
|
success: true,
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { discoverOps } from "../../op/discover";
|
|
|
6
6
|
import type { OpConfig } from "../../op/types";
|
|
7
7
|
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
8
8
|
import { runOpLocally, findPolicyGateStep, OpRunFailure, type StepRecord } from "../../op/local-executor";
|
|
9
|
-
import { approveCommand } from "../../op/gate";
|
|
10
|
-
import { writeGatedRunSummary, type GatedRunSummary } from "../../op/gate-summary";
|
|
9
|
+
import { approveCommand, describeGateMismatch } from "../../op/gate";
|
|
10
|
+
import { summaryLedgerPrefix, writeGatedRunSummary, type GatedRunSummary } from "../../op/gate-summary";
|
|
11
11
|
import { createLocalOpRuntime } from "../../op/runtimes/local";
|
|
12
12
|
import type { OpRuntimeProvider, OpRunStatus } from "../../op/runtime";
|
|
13
13
|
import { renderHuman, renderJson } from "../../op/local-output";
|
|
@@ -21,6 +21,7 @@ import { ndjsonProgressSink } from "../../components/run-progress";
|
|
|
21
21
|
import { maybeRecordAutoRelease } from "../../components/auto-release";
|
|
22
22
|
import { maybePersistBuildManifest } from "../../components/manifest-persistence";
|
|
23
23
|
import type { DriverComponentResult } from "../../components/driver";
|
|
24
|
+
import type { ReleaseRecord } from "../../lifecycle/release-ledger";
|
|
24
25
|
import { runOpGenerate } from "./run-generate";
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -588,13 +589,16 @@ export async function runOp(ctx: CommandContext): Promise<number> {
|
|
|
588
589
|
* ledger for work `chant run --components` records. Each result is filtered on
|
|
589
590
|
* its own `ok`, so a fan-out that partly succeeded records exactly the
|
|
590
591
|
* components that did.
|
|
592
|
+
*
|
|
593
|
+
* Returns the records it wrote, for `--digest-file` (#2602).
|
|
591
594
|
*/
|
|
592
595
|
export async function recordAutoReleasesForRun(
|
|
593
596
|
results: DriverComponentResult[],
|
|
594
597
|
env: string,
|
|
595
598
|
runId: string,
|
|
596
599
|
disabled: boolean,
|
|
597
|
-
): Promise<
|
|
600
|
+
): Promise<ReleaseRecord[]> {
|
|
601
|
+
const recorded: ReleaseRecord[] = [];
|
|
598
602
|
for (const componentResult of results) {
|
|
599
603
|
if (!componentResult.ok) continue;
|
|
600
604
|
const outcome = await maybeRecordAutoRelease(
|
|
@@ -614,6 +618,7 @@ export async function recordAutoReleasesForRun(
|
|
|
614
618
|
message: `release record for "${componentResult.component}"@${env} was not recorded: ${outcome.error}`,
|
|
615
619
|
}));
|
|
616
620
|
} else if (outcome.recorded) {
|
|
621
|
+
recorded.push(outcome.record);
|
|
617
622
|
console.error(formatInfo(
|
|
618
623
|
`Recorded release: ${formatBold(componentResult.component)}@${env} -> ${outcome.record.digest} (commit ${outcome.commit.slice(0, 7)})`,
|
|
619
624
|
));
|
|
@@ -633,6 +638,7 @@ export async function recordAutoReleasesForRun(
|
|
|
633
638
|
));
|
|
634
639
|
}
|
|
635
640
|
}
|
|
641
|
+
return recorded;
|
|
636
642
|
}
|
|
637
643
|
|
|
638
644
|
// ── chant run --components <name|all> ────────────────────────────────────────
|
|
@@ -760,7 +766,12 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
760
766
|
console.error(formatWarning({
|
|
761
767
|
message: `component "${result.gated.component}" is gated on "${gate.gate}" — pending approval`,
|
|
762
768
|
}));
|
|
763
|
-
|
|
769
|
+
// #2574: an approval stands, but for another plan or for none. Say so,
|
|
770
|
+
// with the command that replaces it.
|
|
771
|
+
if (result.gated.mismatch) {
|
|
772
|
+
console.error(formatWarning({ message: describeGateMismatch(gate.op, gate.gate, result.gated.mismatch) }));
|
|
773
|
+
}
|
|
774
|
+
console.error(formatInfo(`approve : ${approveCommand(gate.op, gate.gate, gate.environment)}`));
|
|
764
775
|
if (gate.url) console.error(formatInfo(`approve at: ${gate.url}`));
|
|
765
776
|
console.error(formatInfo(`expires : ${gate.expiresAt}`));
|
|
766
777
|
// #2310: this run's own append reached only the local chant/lifecycle
|
|
@@ -782,6 +793,8 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
782
793
|
...(gate.url ? { url: gate.url } : {}),
|
|
783
794
|
...(result.gated.pushed === false ? { pushed: false, pushWarning: result.gated.pushWarning } : {}),
|
|
784
795
|
...(gate.planDigest ? { planDigest: gate.planDigest } : {}),
|
|
796
|
+
...(await summaryLedgerPrefix()),
|
|
797
|
+
...(gate.environment ? { environment: gate.environment } : {}),
|
|
785
798
|
},
|
|
786
799
|
gatedExit,
|
|
787
800
|
);
|
|
@@ -790,7 +803,16 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
790
803
|
|
|
791
804
|
if (result.success && result.run) {
|
|
792
805
|
const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
|
|
793
|
-
await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
|
|
806
|
+
const recorded = await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
|
|
807
|
+
// `--digest-file` (#2602): the releases this run recorded, one
|
|
808
|
+
// `<component>=<digest>` line each, the form `chant components promote
|
|
809
|
+
// --digest` takes. A generated promote job reads it to promote exactly
|
|
810
|
+
// what this job built, not whatever is latest in the environment.
|
|
811
|
+
if (ctx.args.digestFile) {
|
|
812
|
+
const digestPath = resolve(ctx.args.digestFile);
|
|
813
|
+
mkdirSync(dirname(digestPath), { recursive: true });
|
|
814
|
+
writeFileSync(digestPath, recorded.map((r) => `${r.component}=${r.digest}\n`).join(""));
|
|
815
|
+
}
|
|
794
816
|
}
|
|
795
817
|
|
|
796
818
|
return result.success ? 0 : 1;
|
|
@@ -903,6 +925,7 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
903
925
|
? { pushed: false, pushWarning: status.result.gatePushWarning }
|
|
904
926
|
: {}),
|
|
905
927
|
...(pending?.planDigest ? { planDigest: pending.planDigest } : {}),
|
|
928
|
+
...(await summaryLedgerPrefix()),
|
|
906
929
|
},
|
|
907
930
|
gatedExit,
|
|
908
931
|
);
|