@intentius/chant 0.124.40 → 0.124.42
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/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 +18 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +42 -5
- 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 +76 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +615 -55
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +9 -0
- 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.map +1 -1
- package/dist/cli/handlers/run.js +11 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +49 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +46 -2
- package/dist/cli/registry.d.ts +31 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability.d.ts +47 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +44 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +8 -1
- package/dist/components/driver.d.ts +10 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +52 -2
- package/dist/components/fan-out-run.d.ts +37 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +44 -4
- package/dist/components/gate-plan.d.ts +2 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +1 -0
- 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 +92 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +150 -21
- 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 +6 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +8 -3
- package/dist/components/wave-plan.d.ts +14 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +34 -2
- 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/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/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- 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/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/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/pr-refs.d.ts +10 -0
- package/dist/op/activities/pr-refs.d.ts.map +1 -0
- package/dist/op/activities/pr-refs.js +26 -0
- package/dist/op/activities/predict-behaviour.d.ts +2 -4
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.js +2 -21
- 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 +28 -43
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +170 -120
- 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/op/stage-hooks.d.ts +132 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +142 -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 +64 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +331 -44
- package/dist/pr-loop.d.ts +107 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +84 -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/pr-requires.d.ts +38 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +48 -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 +243 -243
- 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/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- 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-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- 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 +17 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +66 -4
- package/dist/workspace/ci-workflow-at.d.ts +14 -0
- package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
- package/dist/workspace/ci-workflow-at.js +17 -0
- package/dist/workspace/ci-workflow.d.ts +11 -3
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +10 -2
- package/dist/workspace/declaration.d.ts +69 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +149 -2
- 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 +94 -3
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +1 -1
- 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.map +1 -1
- package/dist/workspace/status.js +4 -1
- 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/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +18 -1
- package/src/cli/handlers/fan-out.ts +43 -6
- 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-before-merge.test.ts +276 -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 +605 -61
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +49 -4
- package/src/cli/parse-args.ts +34 -2
- package/src/cli/registry.ts +31 -2
- package/src/components/capability-targets.test.ts +162 -0
- package/src/components/capability.ts +71 -0
- package/src/components/cli-support.ts +14 -1
- package/src/components/driver.ts +54 -3
- package/src/components/fan-out-run.ts +58 -3
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/gate-plan.ts +3 -0
- 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 +194 -21
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +14 -2
- package/src/components/wave-plan.ts +38 -3
- 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/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- 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 +197 -183
- 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/op/stage-hooks.ts +251 -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 +347 -35
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +170 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/pr-requires.test.ts +184 -0
- package/src/pr-requires.ts +79 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -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/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- 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-init.ts +5 -4
- 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 +107 -8
- package/src/workspace/ci-run.ts +74 -5
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +149 -2
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +140 -5
- 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 +94 -3
- package/src/workspace/reason-codes.ts +1 -1
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.ts +4 -1
- 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
|
@@ -72,6 +72,14 @@ export interface DeployContext {
|
|
|
72
72
|
* against missing values.
|
|
73
73
|
*/
|
|
74
74
|
unresolvedInputs?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* The addresses this component's steps narrow to (#4300), from
|
|
77
|
+
* `--target <component>:<address>`, sorted. A step's `plan` plans only
|
|
78
|
+
* these, and its `run` applies only these. Every capability step of the
|
|
79
|
+
* component must accept targets ({@link Capability.targets}), or the
|
|
80
|
+
* component refuses before anything plans or runs. Absent: the whole step.
|
|
81
|
+
*/
|
|
82
|
+
targets?: string[];
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -101,6 +109,14 @@ export interface CapabilityPlan {
|
|
|
101
109
|
* applies.
|
|
102
110
|
*/
|
|
103
111
|
outputs?: PlannedOutputs;
|
|
112
|
+
/**
|
|
113
|
+
* The targets this plan narrowed to (#4300), sorted. A plan made with
|
|
114
|
+
* `DeployContext.targets` gives them back, which says it planned only
|
|
115
|
+
* those. Planning binds them into the member's digest
|
|
116
|
+
* ({@link targetedPlanDigest} in ./wave-plan.ts), so an approval of a
|
|
117
|
+
* targeted plan covers that plan and no other.
|
|
118
|
+
*/
|
|
119
|
+
targets?: string[];
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
/**
|
|
@@ -176,11 +192,44 @@ export interface Capability<In = unknown, Out = unknown> {
|
|
|
176
192
|
* compensation gap. See ../lint/rules/comp/comp003-mutating-no-rollback.ts.
|
|
177
193
|
*/
|
|
178
194
|
readonly rollbackPolicy?: RollbackPolicy;
|
|
195
|
+
/**
|
|
196
|
+
* Whether this verb narrows to `DeployContext.targets` (#4300). `"accept"`:
|
|
197
|
+
* its `plan` and `run` act on the targets alone, and its plan gives them
|
|
198
|
+
* back in {@link CapabilityPlan.targets}. `{ refuse }`: it can't, and says
|
|
199
|
+
* why. Unset refuses too, with a reason that names the verb.
|
|
200
|
+
*/
|
|
201
|
+
readonly targets?: TargetSupport;
|
|
179
202
|
}
|
|
180
203
|
|
|
181
204
|
/** A capability's relationship to rollback — see `Capability.rollbackPolicy`. */
|
|
182
205
|
export type RollbackPolicy = "native" | "none-by-design" | "needs-opt-out";
|
|
183
206
|
|
|
207
|
+
/** Whether a capability accepts `DeployContext.targets` (#4300). See `Capability.targets`. */
|
|
208
|
+
export type TargetSupport = "accept" | { refuse: string };
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A component was asked to narrow to targets (#4300) and one of its steps'
|
|
212
|
+
* capabilities refuses them. Nothing in the component plans or runs.
|
|
213
|
+
*/
|
|
214
|
+
export class TargetsRefusedError extends Error {
|
|
215
|
+
constructor(
|
|
216
|
+
readonly component: string,
|
|
217
|
+
readonly kind: string,
|
|
218
|
+
readonly reason: string,
|
|
219
|
+
) {
|
|
220
|
+
super(`component "${component}": ${kind} refuses targets: ${reason}`);
|
|
221
|
+
this.name = "TargetsRefusedError";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Why `capability` refuses targets, or `undefined` when it accepts them (#4300). */
|
|
226
|
+
export function targetRefusal(capability: Pick<Capability<never, unknown>, "kind" | "targets">): string | undefined {
|
|
227
|
+
const t = capability.targets;
|
|
228
|
+
if (t === "accept") return undefined;
|
|
229
|
+
if (t && typeof t === "object" && typeof t.refuse === "string") return t.refuse;
|
|
230
|
+
return `${capability.kind} does not say it can narrow to targets`;
|
|
231
|
+
}
|
|
232
|
+
|
|
184
233
|
/** Extract a capability's `In` type. */
|
|
185
234
|
export type CapabilityInput<C> = C extends Capability<infer In, unknown> ? In : never;
|
|
186
235
|
/** Extract a capability's `Out` type. */
|
|
@@ -257,3 +306,25 @@ export class CapabilityRegistry {
|
|
|
257
306
|
return [...this.capabilities.keys()].sort();
|
|
258
307
|
}
|
|
259
308
|
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* `--target <component>:<address>` values (#4300), grouped by component with
|
|
312
|
+
* each component's addresses sorted and unique. The address is everything
|
|
313
|
+
* after the first `:`, so it may hold colons of its own. Throws for a value
|
|
314
|
+
* with no component or no address, and, given `known`, for a component not
|
|
315
|
+
* among them.
|
|
316
|
+
*/
|
|
317
|
+
export function parseComponentTargets(values: readonly string[], known?: ReadonlySet<string>): Record<string, string[]> {
|
|
318
|
+
const out: Record<string, Set<string>> = {};
|
|
319
|
+
for (const value of values) {
|
|
320
|
+
const at = value.indexOf(":");
|
|
321
|
+
const component = at > 0 ? value.slice(0, at).trim() : "";
|
|
322
|
+
const address = at > 0 ? value.slice(at + 1).trim() : "";
|
|
323
|
+
if (component === "" || address === "") throw new Error(`--target needs a component and an address: --target <component>:<address>, got "${value}"`);
|
|
324
|
+
if (known && !known.has(component)) {
|
|
325
|
+
throw new Error(`--target ${value}: no component is named ${component} (components: ${[...known].sort().join(", ") || "none"})`);
|
|
326
|
+
}
|
|
327
|
+
(out[component] ??= new Set()).add(address);
|
|
328
|
+
}
|
|
329
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)).map(([c, s]) => [c, [...s].sort()]));
|
|
330
|
+
}
|
|
@@ -390,6 +390,12 @@ export function findComponentGate(component: DriverComponent): { gate: string }
|
|
|
390
390
|
export interface RunComponentsOptions {
|
|
391
391
|
/** Target environment name, threaded into every capability's `DeployContext.env` (default: "local"). */
|
|
392
392
|
env?: string;
|
|
393
|
+
/**
|
|
394
|
+
* The addresses each component's steps narrow to (#4300), by component
|
|
395
|
+
* (`--target <component>:<address>`). A name the run does not run refuses
|
|
396
|
+
* the run before anything starts.
|
|
397
|
+
*/
|
|
398
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
393
399
|
/**
|
|
394
400
|
* chant #1051 — opt-in: discover `*.component.ts` files in a sandboxed
|
|
395
401
|
* child process (`chant run --components <name|all> --sandbox`) instead of
|
|
@@ -664,6 +670,12 @@ export async function runComponents(
|
|
|
664
670
|
return { success: false, selected: [], error: resolved.error };
|
|
665
671
|
}
|
|
666
672
|
const targets = resolved.targets;
|
|
673
|
+
const narrowed = options.targets ?? {};
|
|
674
|
+
const outside = Object.keys(narrowed).filter((name) => !targets.some((c) => c.name === name));
|
|
675
|
+
if (outside.length > 0) {
|
|
676
|
+
return { success: false, selected: [], error: `--target names ${outside.join(", ")}, which this run does not run` };
|
|
677
|
+
}
|
|
678
|
+
const targeted = Object.keys(narrowed).length > 0 ? { targets: narrowed } : {};
|
|
667
679
|
|
|
668
680
|
// (#629) Resolve `chant.config.ts`'s `sbom`/`signing`/`vulnPolicy` sections
|
|
669
681
|
// and fill their defaults into every recognized step (`generate-sbom`,
|
|
@@ -712,6 +724,7 @@ export async function runComponents(
|
|
|
712
724
|
const run = await runInterpretDriver(resolvedTargets, registry, {
|
|
713
725
|
env,
|
|
714
726
|
releaseIdentity,
|
|
727
|
+
...targeted,
|
|
715
728
|
componentOutputs: seedOutputs,
|
|
716
729
|
...(emit ? { onRunEvent: emit } : {}),
|
|
717
730
|
...(gates ? { gates } : {}),
|
|
@@ -755,7 +768,7 @@ export async function runComponents(
|
|
|
755
768
|
emit?.({ type: "component-start", wave: 1, component: componentName });
|
|
756
769
|
const componentResult = await runComponentDeploy(
|
|
757
770
|
resolvedTargets[0],
|
|
758
|
-
deployContext({ env, releaseIdentity }, componentName),
|
|
771
|
+
deployContext({ env, releaseIdentity, ...targeted }, componentName),
|
|
759
772
|
registry,
|
|
760
773
|
componentOutputs,
|
|
761
774
|
undefined,
|
package/src/components/driver.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { componentPlanDigest } from "./gate-plan";
|
|
|
55
55
|
import { componentDeployInputs } from "./deploy-inputs";
|
|
56
56
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
57
57
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
58
|
-
import { isPlanMovedError, PLAN_MOVED_EXIT_CODE } from "./capability";
|
|
58
|
+
import { isPlanMovedError, PLAN_MOVED_EXIT_CODE, TargetsRefusedError, targetRefusal } from "./capability";
|
|
59
59
|
import { isPromotedArtifact } from "./auto-release";
|
|
60
60
|
import type { ReleaseIdentity } from "../telemetry-attribution";
|
|
61
61
|
import type { RunProgressEvent } from "./run-progress";
|
|
@@ -889,6 +889,18 @@ export async function runComponentDeploy(
|
|
|
889
889
|
onRunEvent?: RunEventSink,
|
|
890
890
|
): Promise<DriverComponentResult> {
|
|
891
891
|
const emit = joinRunEventSinks(onRunEvent, progressSinkOf(onProgress));
|
|
892
|
+
// #4300: a component narrowed to targets runs only when every step can narrow.
|
|
893
|
+
try {
|
|
894
|
+
assertTargetsAccepted(component, registry, ctx);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
if (!(err instanceof TargetsRefusedError)) throw err;
|
|
897
|
+
return {
|
|
898
|
+
component: component.name,
|
|
899
|
+
ok: false,
|
|
900
|
+
status: "fail",
|
|
901
|
+
records: [{ component: component.name, phase: "Targets", kind: err.kind, status: "fail", durationMs: 0, error: err.message }],
|
|
902
|
+
};
|
|
903
|
+
}
|
|
892
904
|
// Each component tracks its own release: a digest one component promoted never stamps another's deploy.
|
|
893
905
|
if (ctx.release) ctx = { ...ctx, release: { ...ctx.release } };
|
|
894
906
|
const phaseOutputs: Record<string, Record<string, unknown>> = {};
|
|
@@ -908,6 +920,7 @@ export async function runComponentDeploy(
|
|
|
908
920
|
component,
|
|
909
921
|
...(ctx.vars ? { vars: ctx.vars } : {}),
|
|
910
922
|
...(release !== undefined ? { release } : {}),
|
|
923
|
+
...(ctx.targets ? { targets: ctx.targets } : {}),
|
|
911
924
|
// #4155: the files the steps deploy from, read where the steps read
|
|
912
925
|
// them. Only a gate reads the digest, so a run with none skips the hashing.
|
|
913
926
|
...(hasGateStep(component) ? { inputs: componentDeployInputs(component.deploy, process.cwd()).inputs } : {}),
|
|
@@ -1121,6 +1134,8 @@ export interface InterpretRunOptions {
|
|
|
1121
1134
|
* step promotes an artifact. Absent: no deploy step sees a release.
|
|
1122
1135
|
*/
|
|
1123
1136
|
releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
|
|
1137
|
+
/** The addresses each component narrows to (#4300), by component: its `DeployContext.targets`. */
|
|
1138
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
1124
1139
|
}
|
|
1125
1140
|
|
|
1126
1141
|
/**
|
|
@@ -1144,11 +1159,47 @@ export interface InterpretRunOptions {
|
|
|
1144
1159
|
*/
|
|
1145
1160
|
/** A component's `DeployContext` for a run: its env, vars and, when the caller knows it, its release. */
|
|
1146
1161
|
export function deployContext(
|
|
1147
|
-
options: {
|
|
1162
|
+
options: {
|
|
1163
|
+
env: string;
|
|
1164
|
+
vars?: Record<string, unknown>;
|
|
1165
|
+
releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
|
|
1166
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
1167
|
+
},
|
|
1148
1168
|
component: string,
|
|
1149
1169
|
): DeployContext {
|
|
1150
1170
|
const release = options.releaseIdentity?.(component);
|
|
1151
|
-
|
|
1171
|
+
const targets = options.targets?.[component];
|
|
1172
|
+
return {
|
|
1173
|
+
env: options.env,
|
|
1174
|
+
component,
|
|
1175
|
+
...(options.vars ? { vars: options.vars } : {}),
|
|
1176
|
+
...(release ? { release: { ...release } } : {}),
|
|
1177
|
+
...(targets && targets.length > 0 ? { targets: [...new Set(targets)].sort() } : {}),
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Refuse a component asked to narrow to `ctx.targets` (#4300) when one of its
|
|
1183
|
+
* capability steps can't: throws {@link TargetsRefusedError} naming the first
|
|
1184
|
+
* such verb, in composition order. A step whose kind is not registered
|
|
1185
|
+
* refuses too. Does nothing when `ctx.targets` is absent.
|
|
1186
|
+
*/
|
|
1187
|
+
export function assertTargetsAccepted(component: DriverComponent, registry: CapabilityRegistry, ctx: DeployContext): void {
|
|
1188
|
+
if (!ctx.targets || ctx.targets.length === 0) return;
|
|
1189
|
+
const walk = (steps: DriverPhase["steps"]): void => {
|
|
1190
|
+
for (const step of steps) {
|
|
1191
|
+
if (typeof (step as DriverPhase).phase === "string" && Array.isArray((step as DriverPhase).steps)) {
|
|
1192
|
+
walk((step as DriverPhase).steps);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const kind = (step as { kind?: unknown }).kind;
|
|
1196
|
+
if (kind === "gate" || typeof kind !== "string") continue;
|
|
1197
|
+
if (!registry.has(kind)) throw new TargetsRefusedError(component.name, kind, `no capability is registered for ${kind}`);
|
|
1198
|
+
const reason = targetRefusal(registry.resolve(kind));
|
|
1199
|
+
if (reason !== undefined) throw new TargetsRefusedError(component.name, kind, reason);
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
for (const phase of component.deploy) walk(phase.steps);
|
|
1152
1203
|
}
|
|
1153
1204
|
|
|
1154
1205
|
export async function runInterpretDriver(
|
|
@@ -54,6 +54,7 @@ import type { CapabilityRegistry } from "./capability";
|
|
|
54
54
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
55
55
|
import type { DeployContext } from "./capability";
|
|
56
56
|
import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
|
|
57
|
+
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
57
58
|
import { decideWaveGate, runWaveLoop, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
|
|
58
59
|
|
|
59
60
|
/** The sink a fan-out emits to: run events, and the older stream through them. */
|
|
@@ -129,6 +130,19 @@ export interface FanOutWaveGate {
|
|
|
129
130
|
* do not exist yet. A later wave is left for its own job.
|
|
130
131
|
*/
|
|
131
132
|
only?: number;
|
|
133
|
+
/**
|
|
134
|
+
* With `only` (#4291): members of earlier waves the record says failed to
|
|
135
|
+
* plan or to apply. Each blocks its dependents in this wave and names
|
|
136
|
+
* itself as what blocked them; the rest of the wave runs. An earlier
|
|
137
|
+
* member that neither applied nor failed still refuses the run.
|
|
138
|
+
*/
|
|
139
|
+
failedEarlier?: readonly string[];
|
|
140
|
+
/**
|
|
141
|
+
* With `only` (#4291): earlier waves the record says stopped at their gate.
|
|
142
|
+
* A member left in one of them stops this wave too ({@link EarlierWaveWaitingError}),
|
|
143
|
+
* since a gate nobody approved stops every wave after it.
|
|
144
|
+
*/
|
|
145
|
+
waitingEarlier?: ReadonlyArray<{ wave: number; gate: string; components: readonly string[] }>;
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
/** Thrown when `FanOutWaveGate.only` names a wave whose predecessors have not all applied. */
|
|
@@ -146,6 +160,22 @@ export class EarlierWaveNotAppliedError extends Error {
|
|
|
146
160
|
}
|
|
147
161
|
}
|
|
148
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Thrown when `FanOutWaveGate.only` names a wave after one that waits at its
|
|
165
|
+
* gate (#4291): nothing in this wave runs until that gate is approved and
|
|
166
|
+
* its job runs again. The CLI exits 3, as the waiting wave's job does.
|
|
167
|
+
*/
|
|
168
|
+
export class EarlierWaveWaitingError extends Error {
|
|
169
|
+
constructor(
|
|
170
|
+
readonly wave: number,
|
|
171
|
+
readonly earlier: number,
|
|
172
|
+
readonly gate: string,
|
|
173
|
+
) {
|
|
174
|
+
super(`wave ${wave} does not run yet: wave ${earlier} waits at its gate ${gate}. Approve it and run wave ${earlier} again, then this wave.`);
|
|
175
|
+
this.name = "EarlierWaveWaitingError";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
149
179
|
/** The single gate over a whole fan-out. Omit it to run without one. */
|
|
150
180
|
export interface FanOutGate {
|
|
151
181
|
/** The name `chant approve <op> <gate>` takes — the fan-out's name, not a component's. */
|
|
@@ -158,6 +188,8 @@ export interface FanOutGate {
|
|
|
158
188
|
export interface FanOutRunOptions {
|
|
159
189
|
env: string;
|
|
160
190
|
vars?: Record<string, unknown>;
|
|
191
|
+
/** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
|
|
192
|
+
targets?: Readonly<Record<string, readonly string[]>>;
|
|
161
193
|
/** Outputs of components this run is not running: the ones the plan lists in `seeds`. */
|
|
162
194
|
componentOutputs?: Record<string, Record<string, unknown>>;
|
|
163
195
|
onProgress?: (event: RunProgressEvent) => void;
|
|
@@ -290,7 +322,7 @@ export async function runFanOut(
|
|
|
290
322
|
evaluateGate(gates.port, {
|
|
291
323
|
op: gate.op,
|
|
292
324
|
gate: gate.gate,
|
|
293
|
-
planDigest: active.digest,
|
|
325
|
+
planDigest: fanOutGateDigest(active.digest, options.targets),
|
|
294
326
|
...(gate.description ? { description: gate.description } : {}),
|
|
295
327
|
...(gate.timeout ? { timeout: gate.timeout } : {}),
|
|
296
328
|
...(options.now ? { now: options.now } : {}),
|
|
@@ -426,6 +458,21 @@ export async function runFanOut(
|
|
|
426
458
|
};
|
|
427
459
|
}
|
|
428
460
|
|
|
461
|
+
/**
|
|
462
|
+
* The digest a gate over the whole set binds: the derivation's digest, and,
|
|
463
|
+
* when the run narrows components to targets (#4300), the digest over it and
|
|
464
|
+
* every component's sorted targets. An approval of the whole set then covers
|
|
465
|
+
* no narrowed run, and one narrowed run's approval covers no other.
|
|
466
|
+
*/
|
|
467
|
+
export function fanOutGateDigest(digest: string, targets: FanOutRunOptions["targets"]): string {
|
|
468
|
+
const narrowed = Object.entries(targets ?? {})
|
|
469
|
+
.filter(([, t]) => t.length > 0)
|
|
470
|
+
.map(([component, t]) => [component, [...new Set(t)].sort()] as const)
|
|
471
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
472
|
+
if (narrowed.length === 0) return digest;
|
|
473
|
+
return computePlanDigest("fan-out-targets", { plan: digest, targets: Object.fromEntries(narrowed) });
|
|
474
|
+
}
|
|
475
|
+
|
|
429
476
|
/** The run's `deployContext`, with the carried state and the carry callback (#3459) when the caller gave them. */
|
|
430
477
|
function fanOutContext(options: FanOutRunOptions, component: string): DeployContext {
|
|
431
478
|
return {
|
|
@@ -518,7 +565,13 @@ async function runGatedWaves(
|
|
|
518
565
|
if (!wave) return { end: "stop" };
|
|
519
566
|
const runnable = wave.filter((name) => left.has(name) && !blockedBy.has(name));
|
|
520
567
|
if (waveGate.only !== undefined && waveNum < waveGate.only) {
|
|
521
|
-
|
|
568
|
+
// #4291: an earlier member that failed blocks only its dependents; one that waits at its gate stops this wave.
|
|
569
|
+
const failedBefore = new Set(waveGate.failedEarlier ?? []);
|
|
570
|
+
const open = runnable.filter((name) => !failedBefore.has(name));
|
|
571
|
+
const waiting = waveGate.waitingEarlier?.find((w) => w.wave === waveNum && open.some((name) => w.components.includes(name)));
|
|
572
|
+
if (waiting) throw new EarlierWaveWaitingError(waveGate.only, waveNum, waiting.gate);
|
|
573
|
+
if (open.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, open);
|
|
574
|
+
for (const name of runnable) markBlocked(name);
|
|
522
575
|
return { end: "next" };
|
|
523
576
|
}
|
|
524
577
|
if (waveGate.only !== undefined && waveNum > waveGate.only) return { end: "stop" };
|
|
@@ -664,7 +717,9 @@ async function runGatedWaves(
|
|
|
664
717
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
665
718
|
.map(([component, by]) => ({ component, reason: "blocked" as const, blockedBy: by }));
|
|
666
719
|
|
|
667
|
-
|
|
720
|
+
// #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.
|
|
721
|
+
const blockedHere = waveGate.only !== undefined && (plan.waves[waveGate.only - 1] ?? []).some((name) => blockedBy.has(name));
|
|
722
|
+
const status: FanOutRunResult["status"] = failed.length > 0 || blockedHere ? "fail" : gatedFact ? "gated" : "ok";
|
|
668
723
|
emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
|
|
669
724
|
|
|
670
725
|
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();
|
|
@@ -45,6 +45,8 @@ export interface ComponentGatePlan {
|
|
|
45
45
|
release?: string;
|
|
46
46
|
/** What the steps deploy from (`componentDeployInputs`), when the caller read it. */
|
|
47
47
|
inputs?: readonly DeployInputDigest[];
|
|
48
|
+
/** The addresses the component narrows to (#4300), sorted. A gate's approval then covers that narrowing alone. */
|
|
49
|
+
targets?: readonly string[];
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** The plan digest a component gate records and approves. */
|
|
@@ -58,5 +60,6 @@ export function componentPlanDigest(plan: ComponentGatePlan): string {
|
|
|
58
60
|
vars: plan.vars ?? null,
|
|
59
61
|
release: plan.release ?? null,
|
|
60
62
|
...(plan.inputs && plan.inputs.length > 0 ? { inputs: plan.inputs } : {}),
|
|
63
|
+
...(plan.targets && plan.targets.length > 0 ? { targets: [...plan.targets].sort() } : {}),
|
|
61
64
|
});
|
|
62
65
|
}
|