@intentius/chant 0.124.39 → 0.124.41
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/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/component-endpoint.d.ts +20 -0
- package/dist/cli/component-endpoint.d.ts.map +1 -0
- package/dist/cli/component-endpoint.js +36 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +26 -4
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +74 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +514 -58
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +16 -2
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/handlers/run.d.ts +4 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +17 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +41 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -2
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +35 -3
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +29 -2
- package/dist/components/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +2 -0
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/deploy-units.js +5 -0
- package/dist/components/fan-out-run.d.ts +28 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +27 -3
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +72 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +96 -5
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +5 -3
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +39 -16
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +6 -1
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +4 -3
- package/dist/lifecycle/publish-npm.d.ts +2 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +31 -8
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/release-ledger.d.ts +31 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +19 -1
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/status.d.ts +5 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/status.js +10 -5
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/live-endpoint.d.ts +18 -0
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/live-endpoint.js +27 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +62 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +190 -11
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +36 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +82 -0
- package/dist/pr-loop.d.ts +90 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +77 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +239 -239
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +24 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +85 -6
- package/dist/workspace/declaration.d.ts +63 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +147 -0
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +23 -0
- package/dist/workspace/pr-report.schema.json +73 -2
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts +3 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +7 -3
- package/dist/workspace/status.schema.json +1 -0
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/component-endpoint.ts +43 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/fan-out.test.ts +10 -0
- package/src/cli/handlers/fan-out.ts +29 -4
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-endpoint.test.ts +169 -0
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +508 -64
- package/src/cli/handlers/promote.ts +25 -2
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.test.ts +2 -1
- package/src/cli/handlers/run.ts +16 -5
- package/src/cli/main.ts +41 -2
- package/src/cli/parse-args.ts +23 -2
- package/src/cli/registry.ts +12 -2
- package/src/components/auto-release-infra.test.ts +131 -0
- package/src/components/auto-release.test.ts +40 -1
- package/src/components/auto-release.ts +50 -4
- package/src/components/cli-support.ts +4 -0
- package/src/components/deploy-units.ts +5 -0
- package/src/components/fan-out-run.ts +39 -2
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +130 -5
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +5 -2
- package/src/components/promote.test.ts +39 -0
- package/src/components/promote.ts +38 -14
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +6 -1
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/publish-declaration.ts +4 -3
- package/src/lifecycle/publish-npm.ts +33 -8
- package/src/lifecycle/publish-targets.test.ts +36 -3
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/release-ledger.ts +42 -1
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/status.test.ts +19 -0
- package/src/lifecycle/status.ts +14 -6
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/live-endpoint.ts +31 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +233 -11
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +114 -0
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +146 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +133 -8
- package/src/workspace/ci-run.ts +92 -7
- package/src/workspace/declaration.schema.json +147 -0
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +134 -2
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +73 -2
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.schema.json +1 -0
- package/src/workspace/status.ts +11 -4
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
* env vars, timestamp is a real `new Date()` taken at record time).
|
|
27
27
|
*/
|
|
28
28
|
import { getHeadCommit, pushLifecycle, StaleLifecycleBranchError } from "../lifecycle/git";
|
|
29
|
-
import { appendReleaseRecord, InvalidReleaseRecordError, type ReleaseAdmission, type ReleaseRecord, type RunOrigin } from "../lifecycle/release-ledger";
|
|
30
|
-
import
|
|
29
|
+
import { appendReleaseRecord, InvalidReleaseRecordError, type ReleaseAdmission, type ReleaseDigestKind, type ReleaseRecord, type RunOrigin } from "../lifecycle/release-ledger";
|
|
30
|
+
import { changeSetDigest } from "../change-set";
|
|
31
|
+
import type { DriverComponent, DriverStepRecord } from "./driver";
|
|
32
|
+
import { componentInputDigests } from "./fan-out-inputs";
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* A step output that represents a *promoted* artifact — a publish-family result
|
|
@@ -68,6 +70,32 @@ export function extractRunDigest(records: DriverStepRecord[]): string | undefine
|
|
|
68
70
|
return found;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What each component of a run applied, for the ones that publish no artifact
|
|
75
|
+
* (#4274). A component with planned members (`pr-apply`'s change set) gets its
|
|
76
|
+
* plan digest: the one member's `planDigest`, or with several the
|
|
77
|
+
* change-set digest over them, the way a set's digest is taken. Any other gets
|
|
78
|
+
* its deploy-input digest from `componentInputDigests`, computed on first use.
|
|
79
|
+
*/
|
|
80
|
+
export function appliedDigestOf(input: {
|
|
81
|
+
/** Every component of the project, so a component's upstream digests count. */
|
|
82
|
+
components: DriverComponent[];
|
|
83
|
+
env: string;
|
|
84
|
+
projectRoot: string;
|
|
85
|
+
/** The planned members a pull request applied, when there were any. */
|
|
86
|
+
members?: ReadonlyArray<{ member: string; component: string; planDigest: string | null }>;
|
|
87
|
+
}): (component: string) => AppliedDigest | null {
|
|
88
|
+
let inputs: Record<string, string> | undefined;
|
|
89
|
+
return (component) => {
|
|
90
|
+
const planned = (input.members ?? []).filter((m): m is typeof m & { planDigest: string } => m.component === component && m.planDigest !== null);
|
|
91
|
+
if (planned.length === 1) return { digest: planned[0]!.planDigest, kind: "plan" };
|
|
92
|
+
if (planned.length > 1) return { digest: changeSetDigest(planned), kind: "plan" };
|
|
93
|
+
inputs ??= componentInputDigests(input.components, { projectRoot: input.projectRoot, env: input.env });
|
|
94
|
+
const digest = inputs[component];
|
|
95
|
+
return digest === undefined ? null : { digest, kind: "deploy-input" };
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
/** Explicit reasons `maybeRecordAutoRelease` declined to write a record — never a thrown error, since "nothing to record" (or "opted out") is an expected, common outcome. */
|
|
72
100
|
export type AutoReleaseSkipReason =
|
|
73
101
|
| "opted-out"
|
|
@@ -109,6 +137,19 @@ export interface AutoReleaseRunInfo {
|
|
|
109
137
|
* when the run published no artifact. Set only with `releaseDigest`.
|
|
110
138
|
*/
|
|
111
139
|
templatesDigest?: string;
|
|
140
|
+
/**
|
|
141
|
+
* What the run applied, for a component that published no artifact (#4274):
|
|
142
|
+
* its plan digest, or its deploy-input digest when nothing planned. Recorded
|
|
143
|
+
* as `digest` with `digestKind`, after an artifact digest and
|
|
144
|
+
* `templatesDigest`, so an infrastructure-only component has a record too.
|
|
145
|
+
*/
|
|
146
|
+
appliedDigest?: AppliedDigest;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A digest naming what a component applied, not an artifact (#4274). */
|
|
150
|
+
export interface AppliedDigest {
|
|
151
|
+
digest: string;
|
|
152
|
+
kind: Extract<ReleaseDigestKind, "plan" | "deploy-input">;
|
|
112
153
|
}
|
|
113
154
|
|
|
114
155
|
/** Opt-out + field-override knobs for auto-release recording, threaded from CLI flags/config (#597: "opt-out-able (flag/config), documented default"). */
|
|
@@ -142,7 +183,8 @@ function resolveActor(override?: string): string | undefined {
|
|
|
142
183
|
* - `run.success` false — defensive; callers should not reach here on failure.
|
|
143
184
|
* - no digest found in the run's records — a component with no publish step
|
|
144
185
|
* has nothing to bind a release record to, unless the run was made under a
|
|
145
|
-
* release that records its templates (`templatesDigest`, #4222)
|
|
186
|
+
* release that records its templates (`templatesDigest`, #4222) or the
|
|
187
|
+
* caller names what it applied (`appliedDigest`, #4274).
|
|
146
188
|
* - no resolvable actor — mirrors `runComponentsReleaseRecord`'s hard
|
|
147
189
|
* requirement; an unattributed record defeats the ledger's purpose.
|
|
148
190
|
*
|
|
@@ -166,7 +208,10 @@ export async function maybeRecordAutoRelease(
|
|
|
166
208
|
return { recorded: false, reason: "run-not-successful" };
|
|
167
209
|
}
|
|
168
210
|
|
|
169
|
-
const
|
|
211
|
+
const artifact = run.digest ?? extractRunDigest(run.records ?? []);
|
|
212
|
+
const templates = artifact === undefined && run.releaseDigest ? run.templatesDigest : undefined;
|
|
213
|
+
const digest = artifact ?? templates ?? run.appliedDigest?.digest;
|
|
214
|
+
const digestKind: ReleaseDigestKind | undefined = artifact !== undefined ? undefined : templates !== undefined ? "templates" : run.appliedDigest?.kind;
|
|
170
215
|
if (!digest) {
|
|
171
216
|
return { recorded: false, reason: "no-digest", detail: `component "${run.component}" published no digest-bearing artifact` };
|
|
172
217
|
}
|
|
@@ -201,6 +246,7 @@ export async function maybeRecordAutoRelease(
|
|
|
201
246
|
...(run.profileOverride ? { profileOverride: run.profileOverride } : {}),
|
|
202
247
|
...(run.inputDigest ? { inputDigest: run.inputDigest } : {}),
|
|
203
248
|
...(run.releaseDigest ? { releaseDigest: run.releaseDigest } : {}),
|
|
249
|
+
...(digestKind ? { digestKind } : {}),
|
|
204
250
|
},
|
|
205
251
|
{ cwd: options.cwd },
|
|
206
252
|
);
|
|
@@ -489,6 +489,8 @@ export interface RunComponentsResult {
|
|
|
489
489
|
run?: DriverRunResult;
|
|
490
490
|
/** Component names actually dispatched to the driver, in run order. */
|
|
491
491
|
selected: string[];
|
|
492
|
+
/** The components dispatched, as they ran (#4274): what a component's deploy-input digest is taken over. */
|
|
493
|
+
components?: DriverComponent[];
|
|
492
494
|
error?: string;
|
|
493
495
|
/** Set when the run stopped at a `gate` nobody has approved (#2119): the component it stopped at and the pending fact recorded for it. */
|
|
494
496
|
gated?: {
|
|
@@ -718,6 +720,7 @@ export async function runComponents(
|
|
|
718
720
|
success: run.status === "ok",
|
|
719
721
|
run,
|
|
720
722
|
selected,
|
|
723
|
+
components: resolvedTargets,
|
|
721
724
|
buildParams: options.buildParams,
|
|
722
725
|
...(run.status === "gated" && run.gate
|
|
723
726
|
? {
|
|
@@ -785,6 +788,7 @@ export async function runComponents(
|
|
|
785
788
|
success: componentResult.ok,
|
|
786
789
|
run,
|
|
787
790
|
selected,
|
|
791
|
+
components: resolvedTargets,
|
|
788
792
|
buildParams: options.buildParams,
|
|
789
793
|
...(componentResult.status === "gated" && componentResult.gate
|
|
790
794
|
? {
|
|
@@ -59,6 +59,11 @@ export const DEPLOY_UNIT_RULES: readonly DeployUnitRule[] = [
|
|
|
59
59
|
// A Terraform-family root (#3049). Its unit is the root's name in
|
|
60
60
|
// `terraform.roots`, which is also the member a gated wave's digest names.
|
|
61
61
|
{ kind: "terraform-apply", field: "root", lexicon: "terraform" },
|
|
62
|
+
// An ARM template applied into one resource group (#4273). Its unit is the group.
|
|
63
|
+
{ kind: "arm-deploy", field: "resourceGroup", lexicon: "azure" },
|
|
64
|
+
// A Config Connector manifest applied over REST (#4273). Its unit is the
|
|
65
|
+
// step's `stack`, the member its plan names.
|
|
66
|
+
{ kind: "gcp-apply", field: "stack", lexicon: "gcp" },
|
|
62
67
|
];
|
|
63
68
|
|
|
64
69
|
/** One resolved unit a component's deploy composition targets. */
|
|
@@ -129,6 +129,19 @@ export interface FanOutWaveGate {
|
|
|
129
129
|
* do not exist yet. A later wave is left for its own job.
|
|
130
130
|
*/
|
|
131
131
|
only?: number;
|
|
132
|
+
/**
|
|
133
|
+
* With `only` (#4291): members of earlier waves the record says failed to
|
|
134
|
+
* plan or to apply. Each blocks its dependents in this wave and names
|
|
135
|
+
* itself as what blocked them; the rest of the wave runs. An earlier
|
|
136
|
+
* member that neither applied nor failed still refuses the run.
|
|
137
|
+
*/
|
|
138
|
+
failedEarlier?: readonly string[];
|
|
139
|
+
/**
|
|
140
|
+
* With `only` (#4291): earlier waves the record says stopped at their gate.
|
|
141
|
+
* A member left in one of them stops this wave too ({@link EarlierWaveWaitingError}),
|
|
142
|
+
* since a gate nobody approved stops every wave after it.
|
|
143
|
+
*/
|
|
144
|
+
waitingEarlier?: ReadonlyArray<{ wave: number; gate: string; components: readonly string[] }>;
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
/** Thrown when `FanOutWaveGate.only` names a wave whose predecessors have not all applied. */
|
|
@@ -146,6 +159,22 @@ export class EarlierWaveNotAppliedError extends Error {
|
|
|
146
159
|
}
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Thrown when `FanOutWaveGate.only` names a wave after one that waits at its
|
|
164
|
+
* gate (#4291): nothing in this wave runs until that gate is approved and
|
|
165
|
+
* its job runs again. The CLI exits 3, as the waiting wave's job does.
|
|
166
|
+
*/
|
|
167
|
+
export class EarlierWaveWaitingError extends Error {
|
|
168
|
+
constructor(
|
|
169
|
+
readonly wave: number,
|
|
170
|
+
readonly earlier: number,
|
|
171
|
+
readonly gate: string,
|
|
172
|
+
) {
|
|
173
|
+
super(`wave ${wave} does not run yet: wave ${earlier} waits at its gate ${gate}. Approve it and run wave ${earlier} again, then this wave.`);
|
|
174
|
+
this.name = "EarlierWaveWaitingError";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
149
178
|
/** The single gate over a whole fan-out. Omit it to run without one. */
|
|
150
179
|
export interface FanOutGate {
|
|
151
180
|
/** The name `chant approve <op> <gate>` takes — the fan-out's name, not a component's. */
|
|
@@ -518,7 +547,13 @@ async function runGatedWaves(
|
|
|
518
547
|
if (!wave) return { end: "stop" };
|
|
519
548
|
const runnable = wave.filter((name) => left.has(name) && !blockedBy.has(name));
|
|
520
549
|
if (waveGate.only !== undefined && waveNum < waveGate.only) {
|
|
521
|
-
|
|
550
|
+
// #4291: an earlier member that failed blocks only its dependents; one that waits at its gate stops this wave.
|
|
551
|
+
const failedBefore = new Set(waveGate.failedEarlier ?? []);
|
|
552
|
+
const open = runnable.filter((name) => !failedBefore.has(name));
|
|
553
|
+
const waiting = waveGate.waitingEarlier?.find((w) => w.wave === waveNum && open.some((name) => w.components.includes(name)));
|
|
554
|
+
if (waiting) throw new EarlierWaveWaitingError(waveGate.only, waveNum, waiting.gate);
|
|
555
|
+
if (open.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, open);
|
|
556
|
+
for (const name of runnable) markBlocked(name);
|
|
522
557
|
return { end: "next" };
|
|
523
558
|
}
|
|
524
559
|
if (waveGate.only !== undefined && waveNum > waveGate.only) return { end: "stop" };
|
|
@@ -664,7 +699,9 @@ async function runGatedWaves(
|
|
|
664
699
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
665
700
|
.map(([component, by]) => ({ component, reason: "blocked" as const, blockedBy: by }));
|
|
666
701
|
|
|
667
|
-
|
|
702
|
+
// #4291: a member of this job's wave that an earlier job's failure blocked fails the job, though the rest of the wave applied.
|
|
703
|
+
const blockedHere = waveGate.only !== undefined && (plan.waves[waveGate.only - 1] ?? []).some((name) => blockedBy.has(name));
|
|
704
|
+
const status: FanOutRunResult["status"] = failed.length > 0 || blockedHere ? "fail" : gatedFact ? "gated" : "ok";
|
|
668
705
|
emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
|
|
669
706
|
|
|
670
707
|
return {
|
|
@@ -11,7 +11,7 @@ import { describe, expect, test } from "vitest";
|
|
|
11
11
|
import { CapabilityRegistry, type DeployContext } from "./capability";
|
|
12
12
|
import { memoryGateLedgerPort } from "../op/gate";
|
|
13
13
|
import { planFanOut } from "./fan-out";
|
|
14
|
-
import { EarlierWaveNotAppliedError, runFanOut, type FanOutRunOptions } from "./fan-out-run";
|
|
14
|
+
import { EarlierWaveNotAppliedError, EarlierWaveWaitingError, runFanOut, type FanOutRunOptions } from "./fan-out-run";
|
|
15
15
|
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
16
16
|
import type { DriverComponent } from "./driver";
|
|
17
17
|
import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
@@ -354,6 +354,50 @@ describe("one wave per CI job", () => {
|
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
describe("a failed member blocks only its dependents across wave jobs (#4291)", () => {
|
|
358
|
+
/** net, then a and b, then app (reads a) and web (reads b). */
|
|
359
|
+
const WIDE: DriverComponent[] = [...ESTATE, root("web", ["b"])];
|
|
360
|
+
|
|
361
|
+
test("wave 3's job blocks what an earlier job's failure holds, and plans the rest", async () => {
|
|
362
|
+
const w = world();
|
|
363
|
+
const plan = planFanOut({ components: WIDE, changed: ["net"] });
|
|
364
|
+
const result = await runFanOut(plan, WIDE, w.registry, {
|
|
365
|
+
...waveOptions(ledger()),
|
|
366
|
+
waveGate: { op: "fan-out", gate: "release", only: 3, failedEarlier: ["a"] },
|
|
367
|
+
progress: { completed: ["net", "b"] },
|
|
368
|
+
});
|
|
369
|
+
expect(result.blocked).toEqual([{ component: "app", reason: "blocked", blockedBy: "a" }]);
|
|
370
|
+
expect(w.planned.map((p) => p.root)).toEqual(["web"]);
|
|
371
|
+
// A member of the job's own wave was blocked, so the job fails though web went on to its gate.
|
|
372
|
+
expect(result.status).toBe("fail");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("an earlier member that neither applied nor failed still refuses the job", async () => {
|
|
376
|
+
const w = world();
|
|
377
|
+
const plan = planFanOut({ components: WIDE, changed: ["net"] });
|
|
378
|
+
await expect(
|
|
379
|
+
runFanOut(plan, WIDE, w.registry, {
|
|
380
|
+
...waveOptions(ledger()),
|
|
381
|
+
waveGate: { op: "fan-out", gate: "release", only: 3, failedEarlier: ["a"] },
|
|
382
|
+
progress: { completed: ["net"] },
|
|
383
|
+
}),
|
|
384
|
+
).rejects.toThrow(EarlierWaveNotAppliedError);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("an earlier wave that waits at its gate stops the job", async () => {
|
|
388
|
+
const w = world();
|
|
389
|
+
const plan = planFanOut({ components: WIDE, changed: ["net"] });
|
|
390
|
+
await expect(
|
|
391
|
+
runFanOut(plan, WIDE, w.registry, {
|
|
392
|
+
...waveOptions(ledger()),
|
|
393
|
+
waveGate: { op: "fan-out", gate: "release", only: 3, waitingEarlier: [{ wave: 2, gate: "release-wave-2", components: ["a", "b"] }] },
|
|
394
|
+
progress: { completed: ["net"] },
|
|
395
|
+
}),
|
|
396
|
+
).rejects.toThrow(EarlierWaveWaitingError);
|
|
397
|
+
expect(w.planned).toEqual([]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
357
401
|
describe("both modes", () => {
|
|
358
402
|
test("a run takes one gate over the set or one per wave, not both", async () => {
|
|
359
403
|
const w = world();
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pull request's environment (#4298): what it derives from the template,
|
|
3
|
+
* the records that say which environments stand, and the ownership check a
|
|
4
|
+
* teardown runs before it deletes anything.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import type { ChantConfig } from "../config";
|
|
9
|
+
import type { TeardownPlan, TeardownReport } from "../lifecycle/teardown";
|
|
10
|
+
import type { ObservationLexicon, TeardownCandidate } from "../lexicon";
|
|
11
|
+
import {
|
|
12
|
+
acceptHandedRecord,
|
|
13
|
+
derivePrEnvironment,
|
|
14
|
+
handedPrEnvReport,
|
|
15
|
+
expired,
|
|
16
|
+
isPrEnvironmentName,
|
|
17
|
+
livePrEnvironments,
|
|
18
|
+
ownedEntries,
|
|
19
|
+
parsePrEnvRecords,
|
|
20
|
+
prEnvStatusState,
|
|
21
|
+
releaseTargetRefusal,
|
|
22
|
+
renderPrEnvNote,
|
|
23
|
+
teardownComplete,
|
|
24
|
+
tearDownPrEnvironment,
|
|
25
|
+
teardownRefusal,
|
|
26
|
+
ttlMs,
|
|
27
|
+
type PrEnvRecord,
|
|
28
|
+
} from "./pr-environment";
|
|
29
|
+
|
|
30
|
+
const CONFIG: ChantConfig = {
|
|
31
|
+
environments: ["local", { name: "staging", endpoint: "http://localhost:4566" }, "prod", "pr-*"],
|
|
32
|
+
ownership: { stack: "shop", env: { param: "env" } },
|
|
33
|
+
buildParams: { env: { type: "string", default: "local" } },
|
|
34
|
+
} as ChantConfig;
|
|
35
|
+
|
|
36
|
+
function up(over: Partial<PrEnvRecord> = {}): PrEnvRecord {
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
kind: "up",
|
|
40
|
+
pr: 7,
|
|
41
|
+
env: "pr-7",
|
|
42
|
+
template: "staging",
|
|
43
|
+
marker: { stack: "shop", env: "pr-7" },
|
|
44
|
+
commit: "a".repeat(40),
|
|
45
|
+
components: ["app"],
|
|
46
|
+
planDigest: "sha256:aa",
|
|
47
|
+
result: "applied",
|
|
48
|
+
by: "octo",
|
|
49
|
+
timestamp: "2026-10-22T10:00:00.000Z",
|
|
50
|
+
expiresAt: "2026-10-25T10:00:00.000Z",
|
|
51
|
+
releaseTarget: false,
|
|
52
|
+
...over,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("deriving a pull request's environment", () => {
|
|
57
|
+
test("pr-<n> takes the template's endpoint and the marker env follows the build parameter", () => {
|
|
58
|
+
const d = derivePrEnvironment(CONFIG, "staging", 7);
|
|
59
|
+
expect(d).toMatchObject({ env: "pr-7", template: "staging", stack: "shop", param: "env", endpoint: "http://localhost:4566" });
|
|
60
|
+
if ("refusal" in d) throw new Error(d.refusal);
|
|
61
|
+
expect(d.environments?.[0]).toEqual({ name: "pr-7", endpoint: "http://localhost:4566" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a pr-* entry's own endpoint wins over the template's", () => {
|
|
65
|
+
const config = { ...CONFIG, environments: ["staging", { name: "pr-*", endpoint: "http://localhost:9000" }] } as ChantConfig;
|
|
66
|
+
expect(derivePrEnvironment(config, "staging", 7)).toMatchObject({ endpoint: "http://localhost:9000" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("refuses when the resources could not be told apart from the template's", () => {
|
|
70
|
+
const off = derivePrEnvironment({ ...CONFIG, ownership: undefined } as ChantConfig, "staging", 7);
|
|
71
|
+
expect(off).toMatchObject({ refusal: expect.stringContaining("no ownership.stack") });
|
|
72
|
+
const literal = derivePrEnvironment({ ...CONFIG, ownership: { stack: "shop", env: "staging" } } as ChantConfig, "staging", 7);
|
|
73
|
+
expect(literal).toMatchObject({ refusal: expect.stringContaining('the literal "staging"') });
|
|
74
|
+
const unset = derivePrEnvironment({ ...CONFIG, ownership: { stack: "shop" } } as ChantConfig, "staging", 7);
|
|
75
|
+
expect(unset).toMatchObject({ refusal: expect.stringContaining("ownership.env is unset") });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("refuses a template the config does not declare, and a name it would not take", () => {
|
|
79
|
+
expect(derivePrEnvironment(CONFIG, "qa", 7)).toMatchObject({ refusal: expect.stringContaining("do not declare") });
|
|
80
|
+
const noPattern = { ...CONFIG, environments: ["staging", "prod"] } as ChantConfig;
|
|
81
|
+
expect(derivePrEnvironment(noPattern, "staging", 7)).toMatchObject({ refusal: expect.stringContaining('add "pr-*"') });
|
|
82
|
+
const literal = { ...CONFIG, environments: ["staging", "pr-7", "pr-*"] } as ChantConfig;
|
|
83
|
+
expect(derivePrEnvironment(literal, "staging", 7)).toMatchObject({ refusal: expect.stringContaining("declared outright") });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("the records", () => {
|
|
88
|
+
test("an up makes an environment live, a destroying down ends it, an incomplete down leaves it", () => {
|
|
89
|
+
const records = [up(), up({ pr: 8, env: "pr-8", marker: { stack: "shop", env: "pr-8" } }), { ...up(), kind: "down" as const, result: "destroyed" as const }];
|
|
90
|
+
const live = livePrEnvironments(records);
|
|
91
|
+
expect([...live.keys()]).toEqual(["pr-8"]);
|
|
92
|
+
const incomplete = livePrEnvironments([up(), { ...up(), kind: "down", result: "incomplete" }]);
|
|
93
|
+
expect(incomplete.get("pr-7")?.teardownIncomplete).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a later up refreshes the expiry and keeps every component it ever applied", () => {
|
|
97
|
+
const live = livePrEnvironments([up(), up({ components: ["db"], expiresAt: "2026-10-26T10:00:00.000Z" })]).get("pr-7")!;
|
|
98
|
+
expect(live.components).toEqual(["app", "db"]);
|
|
99
|
+
expect(live.expiresAt).toBe("2026-10-26T10:00:00.000Z");
|
|
100
|
+
expect(expired(live, new Date("2026-10-26T10:00:00.000Z"))).toBe(true);
|
|
101
|
+
expect(expired(live, new Date("2026-10-25T10:00:00.000Z"))).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("malformed lines are skipped", () => {
|
|
105
|
+
expect(parsePrEnvRecords(`not json\n${JSON.stringify(up())}\n{"version":2}\n`)).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("the ownership check on teardown", () => {
|
|
110
|
+
const live = livePrEnvironments([up()]).get("pr-7")!;
|
|
111
|
+
|
|
112
|
+
test("passes for the pull request's own environment", () => {
|
|
113
|
+
expect(teardownRefusal(7, live, CONFIG.environments)).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("refuses another pull request's record, a marker of another environment, and a declared environment", () => {
|
|
117
|
+
expect(teardownRefusal(8, live, CONFIG.environments)).toContain("not 8");
|
|
118
|
+
expect(teardownRefusal(7, { ...live, marker: { stack: "shop", env: "staging" } }, CONFIG.environments)).toContain("another environment's resources");
|
|
119
|
+
expect(teardownRefusal(7, live, ["staging", "pr-7"])).toContain("declared outright");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("drops every planned entry whose marker is not exactly the environment's", () => {
|
|
123
|
+
const plan: TeardownPlan = {
|
|
124
|
+
environment: "pr-7",
|
|
125
|
+
stack: "shop",
|
|
126
|
+
entries: [
|
|
127
|
+
{ lexicon: "k8s", name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "pr-7" } },
|
|
128
|
+
{ lexicon: "k8s", name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "staging" } },
|
|
129
|
+
{ lexicon: "k8s", name: "other", type: "K8s::Core::Service", marker: { stack: "other", env: "pr-7" } },
|
|
130
|
+
],
|
|
131
|
+
holes: [],
|
|
132
|
+
skipped: [],
|
|
133
|
+
};
|
|
134
|
+
const owned = ownedEntries(plan, { stack: "shop", env: "pr-7" });
|
|
135
|
+
expect(owned.plan.entries.map((e) => e.marker)).toEqual([{ stack: "shop", env: "pr-7" }]);
|
|
136
|
+
expect(owned.dropped).toHaveLength(2);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a teardown with a failure, a skip or a hole is not complete", () => {
|
|
140
|
+
const plan: TeardownPlan = { environment: "pr-7", stack: "shop", entries: [], holes: [], skipped: [] };
|
|
141
|
+
const report = (outcome: TeardownReport["outcomes"][number]["outcome"]): TeardownReport => ({
|
|
142
|
+
environment: "pr-7",
|
|
143
|
+
stack: "shop",
|
|
144
|
+
plan,
|
|
145
|
+
outcomes: [{ lexicon: "k8s", name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "pr-7" }, outcome }],
|
|
146
|
+
unimplemented: [],
|
|
147
|
+
});
|
|
148
|
+
expect(teardownComplete(report("deleted"))).toBe(true);
|
|
149
|
+
expect(teardownComplete(report("retained"))).toBe(true);
|
|
150
|
+
expect(teardownComplete(report("failed"))).toBe(false);
|
|
151
|
+
expect(teardownComplete(report("skipped"))).toBe(false);
|
|
152
|
+
expect(teardownComplete({ ...report("deleted"), plan: { ...plan, holes: [{ lexicon: "k8s", name: "*", reason: "read-failed" }] } })).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("tearing down through the lexicons", () => {
|
|
157
|
+
const live = livePrEnvironments([up()]).get("pr-7")!;
|
|
158
|
+
|
|
159
|
+
/** A lexicon whose enumeration also returns another environment's resource, and records what it was asked to delete. */
|
|
160
|
+
function lexicon(): { plugin: ObservationLexicon; asked: { environment?: string; deleted: TeardownCandidate[] } } {
|
|
161
|
+
const asked: { environment?: string; deleted: TeardownCandidate[] } = { deleted: [] };
|
|
162
|
+
const plugin = {
|
|
163
|
+
name: "k8s",
|
|
164
|
+
async teardownOwned({ environment }: { environment: string }) {
|
|
165
|
+
asked.environment = environment;
|
|
166
|
+
return {
|
|
167
|
+
candidates: [
|
|
168
|
+
{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "pr-7" } },
|
|
169
|
+
{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "staging" } },
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
async executeTeardown({ candidates }: { candidates: TeardownCandidate[] }) {
|
|
174
|
+
asked.deleted.push(...candidates);
|
|
175
|
+
return { outcomes: candidates.map((c) => ({ name: c.name, outcome: "deleted" as const })) };
|
|
176
|
+
},
|
|
177
|
+
} as unknown as ObservationLexicon;
|
|
178
|
+
return { plugin, asked };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
test("deletes only what carries pr-<n>'s marker", async () => {
|
|
182
|
+
const { plugin, asked } = lexicon();
|
|
183
|
+
const torn = await tearDownPrEnvironment({ pr: 7, live, config: CONFIG, plugins: [plugin] });
|
|
184
|
+
if ("refusal" in torn) throw new Error(torn.refusal);
|
|
185
|
+
expect(asked.environment).toBe("pr-7");
|
|
186
|
+
expect(asked.deleted.map((c) => c.marker)).toEqual([{ stack: "shop", env: "pr-7" }]);
|
|
187
|
+
expect(teardownComplete(torn.report)).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("a refused teardown reads and deletes nothing", async () => {
|
|
191
|
+
const { plugin, asked } = lexicon();
|
|
192
|
+
const torn = await tearDownPrEnvironment({ pr: 7, live: { ...live, marker: { stack: "shop", env: "staging" } }, config: CONFIG, plugins: [plugin] });
|
|
193
|
+
expect(torn).toMatchObject({ refusal: expect.stringContaining("nothing was torn down") });
|
|
194
|
+
expect(asked.environment).toBeUndefined();
|
|
195
|
+
expect(asked.deleted).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("not a release target", () => {
|
|
200
|
+
test("a pr-<n> name is refused as a release's source or target, and nothing else is", () => {
|
|
201
|
+
expect(isPrEnvironmentName("pr-42")).toBe(true);
|
|
202
|
+
expect(isPrEnvironmentName("pr-")).toBe(false);
|
|
203
|
+
expect(isPrEnvironmentName("prod")).toBe(false);
|
|
204
|
+
expect(releaseTargetRefusal("pr-42")).toContain("never a release target");
|
|
205
|
+
expect(releaseTargetRefusal("staging")).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("the note says so", () => {
|
|
209
|
+
const note = renderPrEnvNote({ stage: "up", pr: 7, env: "pr-7", template: "staging", head: "a".repeat(40), op: "pr-7", gate: "pr-env", digest: "sha256:aa", status: "waiting", components: [] });
|
|
210
|
+
expect(note).toContain("<!-- chant-pr-env:pr-7 -->");
|
|
211
|
+
expect(note).toContain("chant approve pr-7 pr-env --plan sha256:aa");
|
|
212
|
+
expect(note).toContain("is not a release target");
|
|
213
|
+
expect(prEnvStatusState({ status: "waiting" })).toBe("pending");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("ttlMs reads minutes, hours and days", () => {
|
|
218
|
+
expect(ttlMs("90m")).toBe(5_400_000);
|
|
219
|
+
expect(ttlMs("72h")).toBe(259_200_000);
|
|
220
|
+
expect(ttlMs("7d")).toBe(604_800_000);
|
|
221
|
+
expect(ttlMs("1w")).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("what env-up hands on is checked as data (#4295)", () => {
|
|
225
|
+
const check = { pr: 7, head: "c".repeat(40), template: "staging", ttlMs: 3_600_000, now: new Date("2026-10-22T12:00:00.000Z"), by: "github-actions" };
|
|
226
|
+
|
|
227
|
+
test("a record for this pull request is kept with the post job's own commit, actor and expiry", () => {
|
|
228
|
+
const accepted = acceptHandedRecord(up({ expiresAt: "2099-01-01T00:00:00.000Z", commit: "d".repeat(40), by: "mallory" }), check);
|
|
229
|
+
if ("refusal" in accepted) throw new Error(accepted.refusal);
|
|
230
|
+
expect(accepted.record).toMatchObject({ pr: 7, env: "pr-7", commit: check.head, by: "github-actions", expiresAt: "2026-10-22T13:00:00.000Z", marker: { stack: "shop", env: "pr-7" }, releaseTarget: false });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a record for another environment, marker, template or member is refused", () => {
|
|
234
|
+
expect(acceptHandedRecord(up({ pr: 8, env: "pr-8" }), check)).toMatchObject({ refusal: expect.stringContaining("not pr-7") });
|
|
235
|
+
expect(acceptHandedRecord(up({ marker: { stack: "shop", env: "staging" } }), check)).toMatchObject({ refusal: expect.stringContaining("marker env") });
|
|
236
|
+
expect(acceptHandedRecord(up({ template: "prod" }), check)).toMatchObject({ refusal: expect.stringContaining("template") });
|
|
237
|
+
expect(acceptHandedRecord(up({ member: "api" }), check)).toMatchObject({ refusal: expect.stringContaining("member") });
|
|
238
|
+
expect(acceptHandedRecord({ ...up(), kind: "down", result: "destroyed" }, check)).toMatchObject({ refusal: expect.stringContaining("down record") });
|
|
239
|
+
expect(acceptHandedRecord("nonsense", check)).toMatchObject({ refusal: expect.stringContaining("does not read") });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a report for another environment is refused, and a kept one takes the post job's head and op", () => {
|
|
243
|
+
expect(handedPrEnvReport({ stage: "up", pr: 8, env: "pr-8", status: "applied" }, { pr: 7, head: "h" })).toMatchObject({ refusal: expect.any(String) });
|
|
244
|
+
expect(handedPrEnvReport(undefined, { pr: 7, head: "h" })).toMatchObject({ refusal: "The env-up job handed on no report." });
|
|
245
|
+
const kept = handedPrEnvReport({ stage: "up", pr: 7, env: "pr-7", status: "waiting", op: "pr-99", head: "x", digest: "sha256:aa", components: [{ component: "app", status: "planned", create: 1 }] }, { pr: 7, head: "h" });
|
|
246
|
+
if ("refusal" in kept) throw new Error(kept.refusal);
|
|
247
|
+
expect(kept.report).toMatchObject({ head: "h", op: "pr-7", status: "waiting", components: [{ component: "app", create: 1, delete: 0 }] });
|
|
248
|
+
});
|
|
249
|
+
});
|