@intentius/chant 0.79.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/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +30 -0
- 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 +13 -0
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/command-group.d.ts +6 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +44 -3
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.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 +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.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/conflict-check.d.ts +6 -0
- package/dist/cli/conflict-check.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/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.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 +20 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -0
- package/dist/cli/handlers/run-generate.d.ts +39 -0
- package/dist/cli/handlers/run-generate.d.ts.map +1 -0
- 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/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +14 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +46 -1
- 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/capability-plugin-loader.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/fan-out-support.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/pilots/alb-ecs.pilot.d.ts +7 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
- package/dist/components/presets/ecs-fargate.d.ts +10 -4
- package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
- package/dist/components/promote.d.ts +264 -0
- package/dist/components/promote.d.ts.map +1 -0
- 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 +104 -5
- 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 +48 -4
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +32 -1
- 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 +17 -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 +110 -0
- package/dist/lexicon-module.d.ts.map +1 -0
- package/dist/lexicon.d.ts +82 -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 +59 -0
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
- 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 +42 -1
- 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/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.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/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.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/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/runtimes/local.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/record-source.d.ts +22 -0
- package/dist/workspace/record-source.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +81 -0
- package/dist/workspace/records-cli.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +118 -0
- package/dist/workspace/records.d.ts.map +1 -0
- 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 +4 -3
- package/src/audit/core.ts +13 -2
- package/src/audit/discover.ts +102 -36
- package/src/audit/report-html.ts +3 -1
- package/src/audit/report-model.ts +14 -1
- package/src/audit/terraform-state.test.ts +81 -1
- package/src/audit/terraform-state.ts +25 -0
- package/src/cli/command-group.ts +7 -0
- package/src/cli/commands/audit-truncation.test.ts +167 -0
- package/src/cli/commands/audit.ts +132 -29
- package/src/cli/commands/check-lexicon-examples.ts +2 -1
- package/src/cli/commands/check-lexicon.ts +24 -0
- package/src/cli/commands/doctor.test.ts +30 -0
- package/src/cli/commands/doctor.ts +21 -0
- package/src/cli/commands/import-agents.ts +15 -4
- package/src/cli/commands/init.ts +76 -25
- package/src/cli/commands/lint.test.ts +52 -1
- package/src/cli/commands/lint.ts +41 -71
- package/src/cli/commands/onboard.ts +15 -0
- package/src/cli/commands/update.ts +18 -5
- package/src/cli/commands/vendor.ts +11 -6
- package/src/cli/conflict-check.test.ts +18 -1
- package/src/cli/conflict-check.ts +13 -0
- package/src/cli/discovery-skip.test.ts +168 -0
- 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.test.ts +11 -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 +6 -1
- package/src/cli/handlers/fan-out.ts +9 -2
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +4 -2
- package/src/cli/handlers/init.ts +54 -1
- package/src/cli/handlers/lifecycle.ts +2 -1
- package/src/cli/handlers/misc.ts +16 -0
- package/src/cli/handlers/operator.test.ts +68 -0
- package/src/cli/handlers/operator.ts +39 -10
- package/src/cli/handlers/promote.test.ts +377 -0
- package/src/cli/handlers/promote.ts +391 -0
- package/src/cli/handlers/run-generate-member.test.ts +83 -0
- package/src/cli/handlers/run-generate.test.ts +158 -0
- package/src/cli/handlers/run-generate.ts +168 -0
- package/src/cli/handlers/run.test.ts +47 -0
- package/src/cli/handlers/run.ts +41 -6
- package/src/cli/handlers/vendor.ts +147 -43
- package/src/cli/main.test.ts +137 -1
- package/src/cli/main.ts +290 -24
- package/src/cli/mcp/op-tools.ts +2 -1
- package/src/cli/path-lexicon-messages.test.ts +152 -0
- package/src/cli/plugins.ts +43 -5
- package/src/cli/registry.ts +46 -1
- 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/__fixtures__/alb-ecs-service.json +0 -8
- package/src/components/capability-plugin-loader.ts +3 -1
- package/src/components/cli-support.ts +9 -3
- package/src/components/discover.test.ts +27 -57
- package/src/components/discover.ts +20 -51
- package/src/components/driver.test.ts +90 -12
- package/src/components/driver.ts +43 -5
- package/src/components/fan-out-support.ts +2 -1
- package/src/components/gate-plan.ts +53 -0
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +7 -7
- package/src/components/presets/ecs-fargate.ts +10 -5
- package/src/components/promote.test.ts +645 -0
- package/src/components/promote.ts +641 -0
- 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 +40 -0
- package/src/config.ts +161 -11
- package/src/content-digest.test.ts +120 -0
- package/src/content-digest.ts +21 -0
- package/src/discovery/files.test.ts +127 -1
- package/src/discovery/files.ts +91 -59
- package/src/discovery/fold-import.ts +127 -9
- 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/sandbox/path-lexicon-parity.test.ts +162 -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 +18 -1
- package/src/join-key.test.ts +25 -0
- package/src/join-key.ts +39 -0
- package/src/lexicon-module.test.ts +203 -0
- package/src/lexicon-module.ts +215 -0
- package/src/lexicon.ts +105 -1
- package/src/lifecycle/build-ledger-store.ts +13 -2
- 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 +206 -0
- package/src/lifecycle/legacy-digest.ts +103 -0
- 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 +53 -3
- package/src/lifecycle/status.test.ts +151 -1
- package/src/lifecycle/status.ts +228 -7
- package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
- 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/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/discover.test.ts +14 -4
- package/src/op/discover.ts +19 -23
- package/src/op/gate-approval.ts +3 -1
- 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/op/generate-pipeline.ts +2 -1
- package/src/op/runtimes/local.ts +2 -1
- 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/record-source.ts +117 -0
- package/src/workspace/records-cli.ts +198 -0
- package/src/workspace/records-contract.test.ts +93 -0
- package/src/workspace/records.schema.json +150 -0
- package/src/workspace/records.test.ts +243 -0
- package/src/workspace/records.ts +384 -0
- 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
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promote a release to another environment without rebuilding (#2530).
|
|
3
|
+
*
|
|
4
|
+
* `chant run --components <name> --env prod` runs the component's whole
|
|
5
|
+
* `deploy` composition, and a composition that builds (a `Build` phase with
|
|
6
|
+
* `docker-build`) builds again in prod. The release ledger already knows which
|
|
7
|
+
* digest staging is running, so a promote reads that record and deploys the
|
|
8
|
+
* same artifact to the target environment instead.
|
|
9
|
+
*
|
|
10
|
+
* The mechanism is a transform on the composition, not a change to the driver:
|
|
11
|
+
*
|
|
12
|
+
* - build-time steps (`BUILD_STEP_KINDS`) are removed, so nothing is built;
|
|
13
|
+
* - the publish step stays. It moves the archived bytes to the target
|
|
14
|
+
* environment, which is what "promote by digest" means in the build-archive
|
|
15
|
+
* model, and its output is checked against the recorded digest. A publish
|
|
16
|
+
* that produces any other digest fails the step, and so the component,
|
|
17
|
+
* before anything after it runs;
|
|
18
|
+
* - gates, apply, verify and every other step run exactly as a normal deploy
|
|
19
|
+
* to the target environment would run them, so the target's gates apply.
|
|
20
|
+
* A gate approval is bound to the target environment, the transformed
|
|
21
|
+
* composition and the digest being deployed (#2574), so an approval for a
|
|
22
|
+
* plain deploy or for another release does not pass it.
|
|
23
|
+
*
|
|
24
|
+
* A composition the transform cannot pin to one recorded digest is refused
|
|
25
|
+
* before anything runs: one with no publish step (nothing would carry the
|
|
26
|
+
* digest), one with more than one (the ledger records one digest per
|
|
27
|
+
* component), and one whose remaining steps read an output of a removed build
|
|
28
|
+
* step (it would resolve to nothing).
|
|
29
|
+
*
|
|
30
|
+
* A rollback (#2531) is the same run against one environment's own earlier
|
|
31
|
+
* release. There the publish step is replaced by the recorded digest rather
|
|
32
|
+
* than run, since the environment already received that artifact when the
|
|
33
|
+
* release was first deployed (see `withoutBuildSteps`'s `pinDigest`). A
|
|
34
|
+
* redeploy (#2604) is a rollback to the release the ledger already records as
|
|
35
|
+
* current, for an environment a failed deploy left running something else.
|
|
36
|
+
*
|
|
37
|
+
* The driver stays capability-agnostic. The two kind lists below are data, in
|
|
38
|
+
* the same spirit as `DEPLOY_UNIT_RULES` (./deploy-units.ts).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { parseWiringRef, type WiringRef } from "../lint/rules/comp/support";
|
|
42
|
+
import { latestPerComponent, type ReleaseRecord, type ReleaseRecordInput, type RunOrigin } from "../lifecycle/release-ledger";
|
|
43
|
+
import { CapabilityRegistry, type Capability } from "./capability";
|
|
44
|
+
import {
|
|
45
|
+
runInterpretDriver,
|
|
46
|
+
DriverRunFailure,
|
|
47
|
+
type DriverComponent,
|
|
48
|
+
type DriverComponentResult,
|
|
49
|
+
type DriverGate,
|
|
50
|
+
type DriverPhase,
|
|
51
|
+
type DriverRunResult,
|
|
52
|
+
type DriverStep,
|
|
53
|
+
} from "./driver";
|
|
54
|
+
import type { GateLedgerPort } from "../op/gate";
|
|
55
|
+
import type { RunProgressEvent } from "./run-progress";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Step kinds that produce an artifact at build time: the build family, and the
|
|
59
|
+
* SBOM steps that read the freshly built archive. A promote removes them.
|
|
60
|
+
*/
|
|
61
|
+
export const BUILD_STEP_KINDS: readonly string[] = [
|
|
62
|
+
"docker-build",
|
|
63
|
+
"zip-package",
|
|
64
|
+
"jvm-build",
|
|
65
|
+
"generate-sbom",
|
|
66
|
+
"extract-config-bom",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The publish family (the same set COMP001 recognises, plus the
|
|
71
|
+
* `publish-asset` alias). Its output digest is the one a release record binds.
|
|
72
|
+
*/
|
|
73
|
+
export const PUBLISH_STEP_KINDS: readonly string[] = [
|
|
74
|
+
"publish-image",
|
|
75
|
+
"publish-artifact",
|
|
76
|
+
"publish-asset",
|
|
77
|
+
"load-image-on-host",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/** One component a promote will deploy, and the source release it deploys. */
|
|
81
|
+
export interface PromotionItem {
|
|
82
|
+
component: string;
|
|
83
|
+
/** The recorded digest being promoted. */
|
|
84
|
+
digest: string;
|
|
85
|
+
/** The source environment's release record the digest came from. */
|
|
86
|
+
source: ReleaseRecord;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What a promote will do, decided before anything runs. */
|
|
90
|
+
export interface PromotionPlan {
|
|
91
|
+
from: string;
|
|
92
|
+
to: string;
|
|
93
|
+
items: PromotionItem[];
|
|
94
|
+
/** Components left alone, and why. Reported, never an error. */
|
|
95
|
+
notPromoted: Array<{ component: string; reason: string }>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pick one release of `component` out of an environment's records: the latest
|
|
100
|
+
* one, or, when `digest` is given, the latest one that recorded that digest.
|
|
101
|
+
* A digest the ledger never recorded for the component is an error.
|
|
102
|
+
*/
|
|
103
|
+
export function selectRelease(
|
|
104
|
+
records: ReleaseRecord[],
|
|
105
|
+
component: string,
|
|
106
|
+
env: string,
|
|
107
|
+
digest?: string,
|
|
108
|
+
): { record: ReleaseRecord } | { error: string } {
|
|
109
|
+
const own = records.filter((r) => r.component === component);
|
|
110
|
+
if (own.length === 0) return { error: `no release of "${component}" is recorded in "${env}"` };
|
|
111
|
+
if (digest === undefined) return { record: latestPerComponent(own).get(component)! };
|
|
112
|
+
const matching = own.filter((r) => r.digest === digest);
|
|
113
|
+
if (matching.length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
error: `digest ${digest} is not recorded for "${component}" in "${env}"; ` +
|
|
116
|
+
`its recorded digests are ${[...new Set(own.map((r) => r.digest))].join(", ")}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { record: latestPerComponent(matching).get(component)! };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read `--digest <component>=<digest>` values (#2602) into a component-to-
|
|
124
|
+
* digest map. A value with no `=` is left to the single-component form
|
|
125
|
+
* (`--component <name> --digest <digest>`), so it is an error here. An empty
|
|
126
|
+
* digest is an error too: a generated promote job passes one when the job that
|
|
127
|
+
* deployed the component recorded no release, and promoting whatever is
|
|
128
|
+
* latest instead is the thing the pin is there to prevent.
|
|
129
|
+
*/
|
|
130
|
+
export function parseDigestPins(values: string[]): { pins: Record<string, string> } | { error: string } {
|
|
131
|
+
const pins: Record<string, string> = {};
|
|
132
|
+
for (const value of values) {
|
|
133
|
+
const at = value.indexOf("=");
|
|
134
|
+
if (at < 0) return { error: "--digest picks one release of one component, so it needs --component <name>, or the form <component>=<digest>" };
|
|
135
|
+
const component = value.slice(0, at);
|
|
136
|
+
const digest = value.slice(at + 1);
|
|
137
|
+
if (component === "") return { error: `--digest ${value} names no component` };
|
|
138
|
+
if (digest === "") {
|
|
139
|
+
return { error: `--digest ${value} names no digest; the run that deployed "${component}" recorded no release for it` };
|
|
140
|
+
}
|
|
141
|
+
if (component in pins && pins[component] !== digest) {
|
|
142
|
+
return { error: `--digest pins "${component}" twice, to ${pins[component]} and ${digest}` };
|
|
143
|
+
}
|
|
144
|
+
pins[component] = digest;
|
|
145
|
+
}
|
|
146
|
+
return { pins };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Decide which releases a promote deploys. With `component`, exactly that
|
|
151
|
+
* component's latest source release (or the one recording `digest`). With
|
|
152
|
+
* `pins` (#2602), exactly the pinned components, each at the source release
|
|
153
|
+
* recording its pinned digest. Otherwise, the latest source release of every
|
|
154
|
+
* component this checkout declares; a recorded component the checkout no
|
|
155
|
+
* longer declares is reported and left alone, since there is no composition
|
|
156
|
+
* to deploy it with.
|
|
157
|
+
*/
|
|
158
|
+
export function planPromotion(input: {
|
|
159
|
+
from: string;
|
|
160
|
+
to: string;
|
|
161
|
+
sourceRecords: ReleaseRecord[];
|
|
162
|
+
/** Names of the components this checkout declares. */
|
|
163
|
+
declared: string[];
|
|
164
|
+
component?: string;
|
|
165
|
+
digest?: string;
|
|
166
|
+
/** Component name to the digest to promote for it, from `--digest <component>=<digest>`. */
|
|
167
|
+
pins?: Record<string, string>;
|
|
168
|
+
}): PromotionPlan | { error: string } {
|
|
169
|
+
const { from, to, sourceRecords, declared, component, digest, pins } = input;
|
|
170
|
+
if (from === to) return { error: `--from and --to name the same environment ("${from}")` };
|
|
171
|
+
if (digest !== undefined && component === undefined) {
|
|
172
|
+
return { error: "--digest picks one release of one component, so it needs --component <name>" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (pins !== undefined && Object.keys(pins).length > 0) {
|
|
176
|
+
if (component !== undefined) {
|
|
177
|
+
return { error: "--digest <component>=<digest> names its own component, so it does not take --component" };
|
|
178
|
+
}
|
|
179
|
+
const items: PromotionItem[] = [];
|
|
180
|
+
for (const [name, pinned] of Object.entries(pins).sort(([a], [b]) => a.localeCompare(b))) {
|
|
181
|
+
if (!declared.includes(name)) return { error: `component "${name}" is not declared in this checkout` };
|
|
182
|
+
const picked = selectRelease(sourceRecords, name, from, pinned);
|
|
183
|
+
if ("error" in picked) return picked;
|
|
184
|
+
items.push({ component: name, digest: picked.record.digest, source: picked.record });
|
|
185
|
+
}
|
|
186
|
+
return { from, to, items, notPromoted: [] };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (component !== undefined) {
|
|
190
|
+
if (!declared.includes(component)) {
|
|
191
|
+
return { error: `component "${component}" is not declared in this checkout` };
|
|
192
|
+
}
|
|
193
|
+
const picked = selectRelease(sourceRecords, component, from, digest);
|
|
194
|
+
if ("error" in picked) return picked;
|
|
195
|
+
return { from, to, items: [{ component, digest: picked.record.digest, source: picked.record }], notPromoted: [] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const latest = latestPerComponent(sourceRecords);
|
|
199
|
+
const items: PromotionItem[] = [];
|
|
200
|
+
const notPromoted: PromotionPlan["notPromoted"] = [];
|
|
201
|
+
for (const [name, record] of [...latest].sort(([a], [b]) => a.localeCompare(b))) {
|
|
202
|
+
if (declared.includes(name)) items.push({ component: name, digest: record.digest, source: record });
|
|
203
|
+
else notPromoted.push({ component: name, reason: "recorded but not declared in this checkout" });
|
|
204
|
+
}
|
|
205
|
+
if (items.length === 0) {
|
|
206
|
+
return { error: `nothing to promote: no component declared here has a release recorded in "${from}"` };
|
|
207
|
+
}
|
|
208
|
+
return { from, to, items, notPromoted };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Decide which release a rollback restores: the release of `component` in
|
|
213
|
+
* `env` given by `digest`, or by default the most recent release whose digest
|
|
214
|
+
* differs from the current one. Restoring the release already current is
|
|
215
|
+
* refused, since there is nothing to roll back.
|
|
216
|
+
*/
|
|
217
|
+
export function planRollback(input: {
|
|
218
|
+
env: string;
|
|
219
|
+
records: ReleaseRecord[];
|
|
220
|
+
declared: string[];
|
|
221
|
+
component: string;
|
|
222
|
+
digest?: string;
|
|
223
|
+
}): PromotionPlan | { error: string } {
|
|
224
|
+
const { env, records, declared, component, digest } = input;
|
|
225
|
+
if (!declared.includes(component)) {
|
|
226
|
+
return { error: `component "${component}" is not declared in this checkout` };
|
|
227
|
+
}
|
|
228
|
+
const own = records.filter((r) => r.component === component);
|
|
229
|
+
const current = latestPerComponent(own).get(component);
|
|
230
|
+
if (!current) return { error: `no release of "${component}" is recorded in "${env}"` };
|
|
231
|
+
|
|
232
|
+
let target: ReleaseRecord;
|
|
233
|
+
if (digest !== undefined) {
|
|
234
|
+
const picked = selectRelease(own, component, env, digest);
|
|
235
|
+
if ("error" in picked) return picked;
|
|
236
|
+
target = picked.record;
|
|
237
|
+
} else {
|
|
238
|
+
const earlier = own.filter((r) => r.digest !== current.digest);
|
|
239
|
+
if (earlier.length === 0) {
|
|
240
|
+
return { error: `"${component}" has no earlier release in "${env}" with a digest other than the current ${current.digest}` };
|
|
241
|
+
}
|
|
242
|
+
target = latestPerComponent(earlier).get(component)!;
|
|
243
|
+
}
|
|
244
|
+
if (target.digest === current.digest) {
|
|
245
|
+
return { error: `${target.digest} is already the current release of "${component}" in "${env}"; nothing to roll back` };
|
|
246
|
+
}
|
|
247
|
+
return { from: env, to: env, items: [{ component, digest: target.digest, source: target }], notPromoted: [] };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Decide what a redeploy deploys: the release of `component` the ledger in
|
|
252
|
+
* `env` records as current. A deploy that fails partway records nothing, so
|
|
253
|
+
* after one the ledger still names the last good release while the
|
|
254
|
+
* environment may be running part of the failed one. `digest`, when given,
|
|
255
|
+
* has to be that current digest. It guards against a deploy recorded since
|
|
256
|
+
* the caller last looked; any earlier release is a rollback.
|
|
257
|
+
*/
|
|
258
|
+
export function planRedeploy(input: {
|
|
259
|
+
env: string;
|
|
260
|
+
records: ReleaseRecord[];
|
|
261
|
+
declared: string[];
|
|
262
|
+
component: string;
|
|
263
|
+
digest?: string;
|
|
264
|
+
}): PromotionPlan | { error: string } {
|
|
265
|
+
const { env, records, declared, component, digest } = input;
|
|
266
|
+
if (!declared.includes(component)) {
|
|
267
|
+
return { error: `component "${component}" is not declared in this checkout` };
|
|
268
|
+
}
|
|
269
|
+
const current = latestPerComponent(records.filter((r) => r.component === component)).get(component);
|
|
270
|
+
if (!current) return { error: `no release of "${component}" is recorded in "${env}"` };
|
|
271
|
+
if (digest !== undefined && digest !== current.digest) {
|
|
272
|
+
return {
|
|
273
|
+
error: `${digest} is not the current release of "${component}" in "${env}" (that is ${current.digest}); ` +
|
|
274
|
+
`use \`chant components rollback ${env} --component ${component} --digest ${digest}\` to restore an earlier release`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return { from: env, to: env, items: [{ component, digest: current.digest, source: current }], notPromoted: [] };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
|
|
281
|
+
return (step as { kind?: unknown }).kind === "gate";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
|
|
285
|
+
return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The build steps that write the archive a promote's publish step reads, each
|
|
290
|
+
* to its `into` path. The SBOM steps are left out: a promote refuses a step
|
|
291
|
+
* that reads their output, so it never needs it.
|
|
292
|
+
*/
|
|
293
|
+
export const ARCHIVE_STEP_KINDS: readonly string[] = ["docker-build", "zip-package", "jvm-build"];
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The paths `component`'s build steps write its archive to, in composition
|
|
297
|
+
* order without repeats (#2575). A generated pipeline carries these files from
|
|
298
|
+
* the job that deployed to the source environment to the job that promotes,
|
|
299
|
+
* since a promote publishes from the archive on disk. Throws when an `into` is
|
|
300
|
+
* not a literal path, such as a wiring reference or an `$env.` value, because
|
|
301
|
+
* a generator cannot name a file that is only known at run time.
|
|
302
|
+
*/
|
|
303
|
+
export function promoteArchivePaths(component: DriverComponent): string[] {
|
|
304
|
+
const paths: string[] = [];
|
|
305
|
+
const walk = (phases: DriverPhase[]): void => {
|
|
306
|
+
for (const phase of phases) {
|
|
307
|
+
for (const step of phase.steps) {
|
|
308
|
+
if (isPhase(step)) walk([step]);
|
|
309
|
+
else if (!isGate(step) && ARCHIVE_STEP_KINDS.includes(step.kind)) {
|
|
310
|
+
const into = step.into;
|
|
311
|
+
if (typeof into !== "string" || into === "" || into.startsWith("@") || into.startsWith("$")) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`component "${component.name}": the ${step.kind} step in phase "${phase.phase}" writes to ` +
|
|
314
|
+
`${JSON.stringify(into)}, which is not a literal path, so a generated promote job cannot carry its archive`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (!paths.includes(into)) paths.push(into);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
walk(component.deploy);
|
|
323
|
+
return paths;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Whether `component`'s deploy has a publish step (#2602). Only such a
|
|
328
|
+
* component records a release when it deploys, so only its release can be
|
|
329
|
+
* promoted; a generated pipeline pins exactly these components' digests.
|
|
330
|
+
*/
|
|
331
|
+
export function hasPublishStep(component: DriverComponent): boolean {
|
|
332
|
+
const walk = (steps: Array<DriverStep | DriverGate | DriverPhase>): boolean =>
|
|
333
|
+
steps.some((step) => isPhase(step) ? walk(step.steps) : !isGate(step) && PUBLISH_STEP_KINDS.includes(step.kind));
|
|
334
|
+
return walk(component.deploy);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** A component's composition with its build-time steps taken out. */
|
|
338
|
+
export interface PromotableComponent {
|
|
339
|
+
component: DriverComponent;
|
|
340
|
+
/** The kinds removed, in composition order. */
|
|
341
|
+
removed: string[];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The step a rollback puts where the publish step was: it publishes nothing
|
|
346
|
+
* and returns the recorded digest, so `@Publish.digest` resolves to it.
|
|
347
|
+
*/
|
|
348
|
+
export const RECORDED_DIGEST_KIND = "recorded-digest";
|
|
349
|
+
|
|
350
|
+
/** The capability behind {@link RECORDED_DIGEST_KIND}. */
|
|
351
|
+
export const recordedDigestCapability: Capability<{ digest: string }, { digest: string }> = {
|
|
352
|
+
kind: RECORDED_DIGEST_KIND,
|
|
353
|
+
rollbackPolicy: "none-by-design",
|
|
354
|
+
async run(_ctx, input) {
|
|
355
|
+
return { digest: input.digest };
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Remove every build-time step from `component`'s `deploy` composition and
|
|
361
|
+
* check that what is left can be pinned to one recorded digest. A phase left
|
|
362
|
+
* with nothing to run is dropped. `rollback` phases are untouched.
|
|
363
|
+
*
|
|
364
|
+
* With `pinDigest` (a rollback, #2531, or a redeploy, #2604), the publish step is replaced too, by a
|
|
365
|
+
* {@link RECORDED_DIGEST_KIND} step carrying that digest: the environment
|
|
366
|
+
* already received the artifact when the release being restored was
|
|
367
|
+
* deployed, and the archive it came from is usually long gone. Only the
|
|
368
|
+
* digest is known then, so a step reading any other publish output (such as
|
|
369
|
+
* `@Publish.uri`) is refused.
|
|
370
|
+
*/
|
|
371
|
+
export function withoutBuildSteps(
|
|
372
|
+
component: DriverComponent,
|
|
373
|
+
opts: { pinDigest?: string; verb?: string } = {},
|
|
374
|
+
): PromotableComponent | { error: string } {
|
|
375
|
+
const verb = opts.verb ?? (opts.pinDigest === undefined ? "a promote" : "a rollback");
|
|
376
|
+
const removed: string[] = [];
|
|
377
|
+
/** Phases a build step was removed from, and whether anything else is left in them. */
|
|
378
|
+
const emptied = new Map<string, boolean>();
|
|
379
|
+
/** Phases a publish step was pinned in, and whether anything else is left in them. */
|
|
380
|
+
const pinned = new Map<string, boolean>();
|
|
381
|
+
let publishSteps = 0;
|
|
382
|
+
const kept: DriverStep[] = [];
|
|
383
|
+
|
|
384
|
+
const strip = (phase: DriverPhase): DriverPhase | undefined => {
|
|
385
|
+
const steps: DriverPhase["steps"] = [];
|
|
386
|
+
let lostBuildStep = false;
|
|
387
|
+
let pinnedHere = false;
|
|
388
|
+
for (const step of phase.steps) {
|
|
389
|
+
if (isGate(step)) {
|
|
390
|
+
steps.push(step);
|
|
391
|
+
} else if (isPhase(step)) {
|
|
392
|
+
const nested = strip(step);
|
|
393
|
+
if (nested) steps.push(nested);
|
|
394
|
+
} else if (BUILD_STEP_KINDS.includes(step.kind)) {
|
|
395
|
+
removed.push(step.kind);
|
|
396
|
+
lostBuildStep = true;
|
|
397
|
+
} else if (PUBLISH_STEP_KINDS.includes(step.kind)) {
|
|
398
|
+
publishSteps++;
|
|
399
|
+
if (opts.pinDigest === undefined) {
|
|
400
|
+
kept.push(step);
|
|
401
|
+
steps.push(step);
|
|
402
|
+
} else {
|
|
403
|
+
removed.push(step.kind);
|
|
404
|
+
pinnedHere = true;
|
|
405
|
+
steps.push({
|
|
406
|
+
kind: RECORDED_DIGEST_KIND,
|
|
407
|
+
digest: opts.pinDigest,
|
|
408
|
+
replaces: step.kind,
|
|
409
|
+
noRollback: "publishes nothing; it hands on a digest the environment already received",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
kept.push(step);
|
|
414
|
+
steps.push(step);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const otherWork = steps.some((s) => !isGate(s) && (s as DriverStep).kind !== RECORDED_DIGEST_KIND);
|
|
418
|
+
if (lostBuildStep) emptied.set(phase.phase, !otherWork && !pinnedHere);
|
|
419
|
+
if (pinnedHere) pinned.set(phase.phase, !otherWork);
|
|
420
|
+
// A phase holding only gates after the strip still decides them.
|
|
421
|
+
if (steps.length === 0) return undefined;
|
|
422
|
+
return { ...phase, steps };
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const deploy = component.deploy.map(strip).filter((p): p is DriverPhase => p !== undefined);
|
|
426
|
+
|
|
427
|
+
if (publishSteps === 0) {
|
|
428
|
+
return {
|
|
429
|
+
error: `component "${component.name}" has no publish step, so nothing in its deploy carries a recorded digest; ` +
|
|
430
|
+
`${verb} would deploy whatever its current source produces`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (publishSteps > 1) {
|
|
434
|
+
return {
|
|
435
|
+
error: `component "${component.name}" has ${publishSteps} publish steps, but its release record carries one digest`,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
for (const step of kept) {
|
|
439
|
+
const { kind: _kind, ...fields } = step;
|
|
440
|
+
for (const ref of refsIn(fields)) {
|
|
441
|
+
if (ref.kind === "prior-step" && emptied.get(ref.phaseName) === true) {
|
|
442
|
+
return {
|
|
443
|
+
error: `component "${component.name}" reads "@${ref.phaseName}.${ref.field}", an output of a build step ` +
|
|
444
|
+
`${verb} does not run`,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const readsPinned =
|
|
448
|
+
(ref.kind === "prior-step" && pinned.get(ref.phaseName) === true && ref.field !== "digest") ||
|
|
449
|
+
(ref.kind === "component-artifact" && ref.componentName === component.name && ref.field !== "digest" && pinned.size > 0);
|
|
450
|
+
if (readsPinned) {
|
|
451
|
+
const spelled = ref.kind === "prior-step" ? `@${ref.phaseName}.${ref.field}` : `@${ref.componentName}.publish.${ref.field}`;
|
|
452
|
+
return {
|
|
453
|
+
error: `component "${component.name}" reads "${spelled}", but ${verb} knows only the recorded digest; ` +
|
|
454
|
+
`wire the digest instead`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return { component: { ...component, deploy }, removed };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Every same-project wiring reference anywhere in a step's fields (cross-stack `stackOutput` objects excluded). */
|
|
463
|
+
function refsIn(value: unknown): WiringRef[] {
|
|
464
|
+
const refs: WiringRef[] = [];
|
|
465
|
+
const walk = (v: unknown): void => {
|
|
466
|
+
if (typeof v === "string") {
|
|
467
|
+
const ref = parseWiringRef(v);
|
|
468
|
+
if (ref) refs.push(ref);
|
|
469
|
+
} else if (Array.isArray(v)) {
|
|
470
|
+
v.forEach(walk);
|
|
471
|
+
} else if (v && typeof v === "object" && !("stackOutput" in v)) {
|
|
472
|
+
Object.values(v as Record<string, unknown>).forEach(walk);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
walk(value);
|
|
476
|
+
return refs;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* A registry whose publish-family capabilities check what they published
|
|
481
|
+
* against the digest being promoted for that component. A mismatch fails the
|
|
482
|
+
* step, which fails the component through the driver's ordinary path. Every
|
|
483
|
+
* other capability is passed through untouched, and the recorded-digest step a
|
|
484
|
+
* rollback's composition carries is added.
|
|
485
|
+
*/
|
|
486
|
+
export function checkPublishedDigest(
|
|
487
|
+
registry: CapabilityRegistry,
|
|
488
|
+
expected: ReadonlyMap<string, string>,
|
|
489
|
+
): CapabilityRegistry {
|
|
490
|
+
const checked = new CapabilityRegistry();
|
|
491
|
+
if (!registry.has(RECORDED_DIGEST_KIND)) checked.register(recordedDigestCapability);
|
|
492
|
+
for (const kind of registry.kinds()) {
|
|
493
|
+
const inner = registry.resolve(kind) as Capability<unknown, unknown>;
|
|
494
|
+
if (!PUBLISH_STEP_KINDS.includes(kind)) {
|
|
495
|
+
checked.register(inner);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const wrapped: Capability<unknown, unknown> = {
|
|
499
|
+
kind,
|
|
500
|
+
...(inner.rollbackPolicy ? { rollbackPolicy: inner.rollbackPolicy } : {}),
|
|
501
|
+
async run(ctx, input) {
|
|
502
|
+
let output: unknown;
|
|
503
|
+
try {
|
|
504
|
+
output = await inner.run(ctx, input);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`${kind} could not publish from the build archive: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
508
|
+
`A promote publishes the archive the source release was built from, so it has to run where that archive is on disk`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const want = expected.get(ctx.component);
|
|
512
|
+
const got = (output as { digest?: unknown } | undefined)?.digest;
|
|
513
|
+
if (want !== undefined && got !== want) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`${kind} published ${typeof got === "string" ? got : "no digest"}, but the release being promoted recorded ${want}; ` +
|
|
516
|
+
`the build archive on disk is not the one that release was built from`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return output;
|
|
520
|
+
},
|
|
521
|
+
...(inner.rollback
|
|
522
|
+
? { rollback: (ctx, input, output) => inner.rollback!(ctx, input, output) }
|
|
523
|
+
: {}),
|
|
524
|
+
};
|
|
525
|
+
checked.register(wrapped);
|
|
526
|
+
}
|
|
527
|
+
return checked;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Options for {@link runPromotion}. */
|
|
531
|
+
export interface RunPromotionOptions {
|
|
532
|
+
plan: PromotionPlan;
|
|
533
|
+
/** The promotable compositions (see {@link withoutBuildSteps}), one per plan item. */
|
|
534
|
+
components: DriverComponent[];
|
|
535
|
+
registry: CapabilityRegistry;
|
|
536
|
+
componentOutputs?: Record<string, Record<string, unknown>>;
|
|
537
|
+
gates?: GateLedgerPort;
|
|
538
|
+
now?: string;
|
|
539
|
+
onProgress?: (event: RunProgressEvent) => void;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Deploy the plan's components to `plan.to`, in dependency order. A
|
|
544
|
+
* `dependsOn` edge to a component outside the promotion is dropped, the same
|
|
545
|
+
* way a single-component `chant run --components` runs without its
|
|
546
|
+
* dependencies; their outputs come from `componentOutputs`. Returns the
|
|
547
|
+
* driver's result for every outcome, including a failure.
|
|
548
|
+
*/
|
|
549
|
+
export async function runPromotion(options: RunPromotionOptions): Promise<DriverRunResult> {
|
|
550
|
+
const { plan } = options;
|
|
551
|
+
const names = new Set(plan.items.map((i) => i.component));
|
|
552
|
+
const targets = options.components
|
|
553
|
+
.filter((c) => names.has(c.name))
|
|
554
|
+
.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => names.has(d)) }));
|
|
555
|
+
const expected = new Map(plan.items.map((i) => [i.component, i.digest]));
|
|
556
|
+
try {
|
|
557
|
+
return await runInterpretDriver(targets, checkPublishedDigest(options.registry, expected), {
|
|
558
|
+
env: plan.to,
|
|
559
|
+
componentOutputs: options.componentOutputs ?? {},
|
|
560
|
+
// #2574: a gate approval for this promote or rollback is bound to the
|
|
561
|
+
// release it deploys, as well as the environment and composition.
|
|
562
|
+
releases: expected,
|
|
563
|
+
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
564
|
+
...(options.gates ? { gates: options.gates } : {}),
|
|
565
|
+
...(options.now ? { now: options.now } : {}),
|
|
566
|
+
});
|
|
567
|
+
} catch (err) {
|
|
568
|
+
if (err instanceof DriverRunFailure) return err.result;
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Who approved the last gate a component passed in this run, if any. */
|
|
574
|
+
export function gateApprover(result: DriverComponentResult | undefined): string | undefined {
|
|
575
|
+
let approver: string | undefined;
|
|
576
|
+
for (const record of result?.records ?? []) {
|
|
577
|
+
if (record.approval) approver = record.approval.resolvedBy;
|
|
578
|
+
}
|
|
579
|
+
return approver;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** What a promote, rollback or redeploy knows about its own run, for the records it writes. */
|
|
583
|
+
export interface DeployRunInfo {
|
|
584
|
+
runId: string;
|
|
585
|
+
runOrigin?: RunOrigin;
|
|
586
|
+
actor: string;
|
|
587
|
+
timestamp: string;
|
|
588
|
+
approver?: string;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** The fields a promote, a rollback and a redeploy record share: the artifact is the earlier release's own. */
|
|
592
|
+
function sharedRecord(item: PromotionItem, env: string, run: DeployRunInfo): ReleaseRecordInput {
|
|
593
|
+
const { source } = item;
|
|
594
|
+
return {
|
|
595
|
+
component: item.component,
|
|
596
|
+
env,
|
|
597
|
+
digest: item.digest,
|
|
598
|
+
gitSha: source.gitSha,
|
|
599
|
+
runId: run.runId,
|
|
600
|
+
...(run.runOrigin ? { runOrigin: run.runOrigin } : {}),
|
|
601
|
+
timestamp: run.timestamp,
|
|
602
|
+
actor: run.actor,
|
|
603
|
+
...(run.approver ? { approver: run.approver } : {}),
|
|
604
|
+
...(source.manifestDigest ? { manifestDigest: source.manifestDigest } : {}),
|
|
605
|
+
...(source.inputDigest ? { inputDigest: source.inputDigest } : {}),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Name an earlier record the way `promotedFrom`, `restores` and `redeploys` do. */
|
|
610
|
+
function refTo(record: ReleaseRecord) {
|
|
611
|
+
return { env: record.env, runId: record.runId, timestamp: record.timestamp };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The release record a successful promotion appends to the target ledger.
|
|
616
|
+
* The digest, git sha and archive identities are the source release's own,
|
|
617
|
+
* since the artifact is the one built there; `promotedFrom` names that
|
|
618
|
+
* release.
|
|
619
|
+
*/
|
|
620
|
+
export function promotionRecord(item: PromotionItem, to: string, run: DeployRunInfo): ReleaseRecordInput {
|
|
621
|
+
return { ...sharedRecord(item, to, run), promotedFrom: refTo(item.source) };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The release record a successful rollback appends. Like a promotion's, it
|
|
626
|
+
* carries the restored release's artifact identities; `restores` names that
|
|
627
|
+
* release.
|
|
628
|
+
*/
|
|
629
|
+
export function rollbackRecord(item: PromotionItem, env: string, run: DeployRunInfo): ReleaseRecordInput {
|
|
630
|
+
return { ...sharedRecord(item, env, run), restores: refTo(item.source) };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* The release record a successful redeploy appends. It carries the current
|
|
635
|
+
* release's artifact identities, as a rollback's carries the restored one's;
|
|
636
|
+
* `redeploys` names that release. The environment's current digest does not
|
|
637
|
+
* change, and the record says when it was last put back.
|
|
638
|
+
*/
|
|
639
|
+
export function redeployRecord(item: PromotionItem, env: string, run: DeployRunInfo): ReleaseRecordInput {
|
|
640
|
+
return { ...sharedRecord(item, env, run), redeploys: refTo(item.source) };
|
|
641
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { params as currentBuildParams } from "../../params";
|
|
3
|
+
import { sendFunctionSource } from "../../discovery/sandbox/driver";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* chant #1051 — generates the source of the "driver" module that runs INSIDE
|
|
@@ -60,15 +61,14 @@ export function generateComponentDriverSource(options: GenerateComponentDriverOp
|
|
|
60
61
|
`import { collectComponents } from ${lit(DISCOVER_MODULE)};`,
|
|
61
62
|
`import { classifyChildError } from ${lit(CHILD_ERRORS_MODULE)};`,
|
|
62
63
|
`import { setBuildParams } from ${lit(PARAMS_MODULE)};`,
|
|
64
|
+
`import { writeSync } from "node:fs";`,
|
|
63
65
|
``,
|
|
64
66
|
// chant #1108 — snapshot of the parent's resolved build-time parameter
|
|
65
67
|
// values (scalars only), bound before any component file is imported.
|
|
66
68
|
`setBuildParams(${lit({ ...currentBuildParams })});`,
|
|
67
69
|
``,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
` else console.log(JSON.stringify(payload));`,
|
|
71
|
-
`}`,
|
|
70
|
+
// chant#2461 — awaits the send and fails loudly if it cannot be delivered.
|
|
71
|
+
...sendFunctionSource(),
|
|
72
72
|
``,
|
|
73
73
|
`async function main() {`,
|
|
74
74
|
` const modules = [];`,
|
|
@@ -114,12 +114,12 @@ export function generateComponentDriverSource(options: GenerateComponentDriverOp
|
|
|
114
114
|
` for (const [name, discovered] of collected.components) {`,
|
|
115
115
|
` components.push({ name, component: discovered.component, exportName: discovered.exportName, filePath: discovered.filePath });`,
|
|
116
116
|
` }`,
|
|
117
|
-
` send({ components, errors });`,
|
|
117
|
+
` await send({ components, errors });`,
|
|
118
118
|
`}`,
|
|
119
119
|
``,
|
|
120
|
-
`main().catch((err)
|
|
121
|
-
` send({ components: [], errors: [classifyChildError("", err).toJSON()], fatal: true })
|
|
122
|
-
`
|
|
120
|
+
`main().catch((err) =>`,
|
|
121
|
+
` send({ components: [], errors: [classifyChildError("", err).toJSON()], fatal: true }),`,
|
|
122
|
+
`);`,
|
|
123
123
|
);
|
|
124
124
|
|
|
125
125
|
return lines.join("\n");
|