@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,6 +26,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { loadChantConfig, type ChantConfig } from "../../config";
|
|
28
28
|
import { promotionOrder } from "../../workspace/promotion-order";
|
|
29
|
+
import { releaseTargetRefusal } from "../../components/pr-environment";
|
|
29
30
|
import { resolvePromotionSource } from "../../components/promote-order";
|
|
30
31
|
import { fetchLifecycle, getHeadCommit, pushLifecycle } from "../../lifecycle/git";
|
|
31
32
|
import {
|
|
@@ -68,6 +69,7 @@ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
|
68
69
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
69
70
|
import { GATED_EXIT_CODE } from "./run";
|
|
70
71
|
import type { CommandContext } from "../registry";
|
|
72
|
+
import { withComponentEndpoint } from "../component-endpoint";
|
|
71
73
|
import { redeployLine, releaseRollbackLine, rollbackTarget, unsettledComponents } from "../../lifecycle/mixed-environment";
|
|
72
74
|
|
|
73
75
|
const PROMOTE_USAGE =
|
|
@@ -390,7 +392,20 @@ function approvalLine(r: import("../../lifecycle/merge-record").MergeRecord): st
|
|
|
390
392
|
* which differ only in how the plan was chosen, whether the publish step runs,
|
|
391
393
|
* and which field names the earlier release on the new record.
|
|
392
394
|
*/
|
|
393
|
-
|
|
395
|
+
/** {@link deployPlanIn}, with `plan.to`'s declared endpoint applied (#4281). */
|
|
396
|
+
function deployPlan(
|
|
397
|
+
ctx: CommandContext,
|
|
398
|
+
verb: Verb,
|
|
399
|
+
plan: PromotionPlan,
|
|
400
|
+
project: { projectPath: string; config: ChantConfig; targets: DriverComponent[] },
|
|
401
|
+
actor: string | undefined,
|
|
402
|
+
record: (item: PromotionItem, env: string, run: DeployRunInfo) => ReleaseRecordInput,
|
|
403
|
+
under?: UnderRelease,
|
|
404
|
+
): Promise<number> {
|
|
405
|
+
return withComponentEndpoint(ctx, () => deployPlanIn(ctx, verb, plan, project, actor, record, under), { config: project.config, environment: plan.to });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function deployPlanIn(
|
|
394
409
|
ctx: CommandContext,
|
|
395
410
|
verb: Verb,
|
|
396
411
|
plan: PromotionPlan,
|
|
@@ -661,7 +676,7 @@ async function deployPlan(
|
|
|
661
676
|
const admission = gateAdmission(result);
|
|
662
677
|
const input = record(item, plan.to, { runId, ...(runOrigin ? { runOrigin } : {}), actor: actor!, timestamp, ...(approver ? { approver } : {}), ...(admission ? { admission } : {}) });
|
|
663
678
|
const templates = templateDigests.get(item.component);
|
|
664
|
-
const { record: written } = await appendReleaseRecord(under ? { ...input, ...(templates ? { digest: templates } : {}), releaseDigest: under.release.releaseDigest } : input);
|
|
679
|
+
const { record: written } = await appendReleaseRecord(under ? { ...input, ...(templates ? { digest: templates, digestKind: "templates" as const } : {}), releaseDigest: under.release.releaseDigest } : input);
|
|
665
680
|
recorded.push(written);
|
|
666
681
|
}
|
|
667
682
|
if (outcome) await appendDeployOutcome(outcome);
|
|
@@ -883,6 +898,14 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
883
898
|
console.error(formatError({ message: "--to <env> is required", hint: PROMOTE_USAGE }));
|
|
884
899
|
return 1;
|
|
885
900
|
}
|
|
901
|
+
// #4298: a pull request's environment is neither a release's target nor its source.
|
|
902
|
+
for (const env of [to, args.migrateFrom]) {
|
|
903
|
+
const refusal = env ? releaseTargetRefusal(env) : undefined;
|
|
904
|
+
if (refusal) {
|
|
905
|
+
console.error(formatError({ message: refusal }));
|
|
906
|
+
return 1;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
886
909
|
const who = actorOrRefuse(ctx);
|
|
887
910
|
if (typeof who === "number") return who;
|
|
888
911
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant query "<sql>" [dir] [--member <name>] [--json]` (#4297): one
|
|
3
|
+
* read-only SQL statement over the snapshots, Op runs, gates, deploys and
|
|
4
|
+
* releases on the local `chant/lifecycle` branch. The tables and the engine
|
|
5
|
+
* live in `../../lifecycle/query.ts`; this reads the ledgers and prints.
|
|
6
|
+
*
|
|
7
|
+
* Inside a workspace every member's ledger is read, each row naming its
|
|
8
|
+
* member; `--member` keeps the members it names. Outside one the project's
|
|
9
|
+
* own ledger is read and `member` is NULL. It reads the local branch and
|
|
10
|
+
* never fetches.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { formatError, formatWarning } from "../format";
|
|
15
|
+
import type { CommandContext } from "../registry";
|
|
16
|
+
import type { QueryLedger } from "../../lifecycle/query";
|
|
17
|
+
import { findWorkspaceRoot } from "../../project-root";
|
|
18
|
+
|
|
19
|
+
const USAGE = 'chant query "<sql>" [dir] [--member <name>] [--json]';
|
|
20
|
+
|
|
21
|
+
/** The ledgers `chant query` reads from `cwd`: every member's in a workspace, else the project's own. */
|
|
22
|
+
export async function queryLedgers(cwd: string, members?: readonly string[]): Promise<QueryLedger[]> {
|
|
23
|
+
const { gitLedgerReadPort } = await import("../../lifecycle/git");
|
|
24
|
+
const found = findWorkspaceRoot(cwd);
|
|
25
|
+
if (!found) return [{ member: null, port: gitLedgerReadPort({ cwd }) }];
|
|
26
|
+
// Workspace code loads only when there is a workspace, so a level-0 project never pays for it.
|
|
27
|
+
const { readDeclaration, WorkspaceReadError } = await import("../../workspace/declaration");
|
|
28
|
+
const { workingTree } = await import("../../workspace/tree");
|
|
29
|
+
const { QueryError } = await import("../../lifecycle/query");
|
|
30
|
+
let declaration;
|
|
31
|
+
try {
|
|
32
|
+
declaration = readDeclaration(workingTree(found.dir), "");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err instanceof WorkspaceReadError) throw new QueryError(`${err.code}: ${err.message}`);
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
const wanted = members && members.length > 0 ? new Set(members) : null;
|
|
38
|
+
if (wanted) {
|
|
39
|
+
const unknown = [...wanted].filter((n) => !declaration.members.some((m) => m.name === n));
|
|
40
|
+
if (unknown.length > 0) throw new QueryError(`--member ${unknown.join(", ")} names no member of the workspace`);
|
|
41
|
+
}
|
|
42
|
+
return declaration.members
|
|
43
|
+
.filter((m) => !wanted || wanted.has(m.name))
|
|
44
|
+
.map((m) => ({ member: m.name, port: gitLedgerReadPort({ cwd: found.dir, member: m.dir === "." ? "." : m.name }) }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runQueryCommand(ctx: CommandContext): Promise<number> {
|
|
48
|
+
const { args } = ctx;
|
|
49
|
+
const sql = args.positionals?.[0];
|
|
50
|
+
if (sql === undefined || sql.trim() === "") {
|
|
51
|
+
console.error(formatError({ message: "chant query needs a statement", hint: `${USAGE}, such as chant query "SELECT env, count(*) FROM resources GROUP BY env"` }));
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
const cwd = resolve(args.positionals?.[1] ?? ".");
|
|
55
|
+
if (!existsSync(cwd)) {
|
|
56
|
+
console.error(formatError({ message: `${cwd} does not exist`, hint: USAGE }));
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
const { loadQueryData, runQuery, formatQueryResult, QueryError } = await import("../../lifecycle/query");
|
|
60
|
+
try {
|
|
61
|
+
const load = await loadQueryData(await queryLedgers(cwd, args.members));
|
|
62
|
+
const result = await runQuery(sql, load);
|
|
63
|
+
if (args.json) {
|
|
64
|
+
console.log(JSON.stringify(result, null, 2));
|
|
65
|
+
} else {
|
|
66
|
+
console.log(formatQueryResult(result));
|
|
67
|
+
const bad = Object.entries(result.malformed).filter(([, n]) => n > 0);
|
|
68
|
+
if (bad.length > 0) console.error(formatWarning({ message: `left out what could not be read: ${bad.map(([t, n]) => `${n} in ${t}`).join(", ")}` }));
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err instanceof QueryError) {
|
|
73
|
+
console.error(formatError({ message: err.message, hint: USAGE }));
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -46,7 +46,8 @@ vi.mock("../../config", async () => {
|
|
|
46
46
|
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
47
47
|
return { ...actual, loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) };
|
|
48
48
|
});
|
|
49
|
-
vi.mock("../../components/auto-release", () => ({
|
|
49
|
+
vi.mock("../../components/auto-release", async () => ({
|
|
50
|
+
...(await vi.importActual<typeof import("../../components/auto-release")>("../../components/auto-release")),
|
|
50
51
|
maybeRecordAutoRelease: (...args: unknown[]) => maybeRecordAutoReleaseMock(...args),
|
|
51
52
|
}));
|
|
52
53
|
vi.mock("../../components/manifest-persistence", () => ({
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { recordGateApproval } from "./operator";
|
|
|
17
17
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
18
18
|
import { parseParamFlags } from "../build-params-cli";
|
|
19
19
|
import type { CommandContext } from "../registry";
|
|
20
|
+
import { withComponentEndpoint } from "../component-endpoint";
|
|
20
21
|
import { stewardBesideFor, stewardFormFor, stewardTurnLeaseName, DEFAULT_STEWARD_ENV, type StewardDeclaration } from "../../op/steward";
|
|
21
22
|
import { holdBesideLease } from "../../op/steward-beside";
|
|
22
23
|
import { acquireStewardTurn, STEWARD_TURN_WAIT_MS } from "../../op/operator";
|
|
@@ -29,11 +30,11 @@ import { ndjsonProgressSink } from "../../components/run-progress";
|
|
|
29
30
|
import { findStoredRun, readRunEvents } from "../../run-event-store";
|
|
30
31
|
import type { RunEvent } from "../../run-event";
|
|
31
32
|
import { openCliRunEvents, refusesBothStreams, stdoutLine } from "../run-events";
|
|
32
|
-
import { maybeRecordAutoRelease } from "../../components/auto-release";
|
|
33
|
+
import { appliedDigestOf, extractRunDigest, maybeRecordAutoRelease, type AppliedDigest } from "../../components/auto-release";
|
|
33
34
|
import { maybePersistBuildManifest } from "../../components/manifest-persistence";
|
|
34
35
|
import { failedRunExitCode, type DriverComponentResult } from "../../components/driver";
|
|
35
36
|
import { isComponentList, type ComponentRunPlan, type RunComponentsResult } from "../../components/cli-support";
|
|
36
|
-
import { resolveCiRun, type ReleaseRecord, type RunOrigin } from "../../lifecycle/release-ledger";
|
|
37
|
+
import { isApplyRecord, resolveCiRun, type ReleaseRecord, type RunOrigin } from "../../lifecycle/release-ledger";
|
|
37
38
|
import { gateAdmission, gateApprover } from "../../components/promote";
|
|
38
39
|
import { runOpGenerate } from "./run-generate";
|
|
39
40
|
|
|
@@ -935,10 +936,14 @@ export async function recordAutoReleasesForRun(
|
|
|
935
936
|
releaseDigest?: string,
|
|
936
937
|
/** #4222: the digest of the templates the release records for a component in `env`, for one that publishes no artifact. */
|
|
937
938
|
templatesDigestOf?: (component: string) => string | null,
|
|
939
|
+
/** #4274: what a component that publishes no artifact applied (`appliedDigestOf`), so it is recorded too. */
|
|
940
|
+
appliedDigestOf?: (component: string) => AppliedDigest | null,
|
|
938
941
|
): Promise<ReleaseRecord[]> {
|
|
939
942
|
const recorded: ReleaseRecord[] = [];
|
|
940
943
|
for (const componentResult of results) {
|
|
941
944
|
if (!componentResult.ok) continue;
|
|
945
|
+
// Hashed only for a component that promoted no artifact, the only one it is recorded for.
|
|
946
|
+
const applied = appliedDigestOf && !disabled && extractRunDigest(componentResult.records) === undefined ? appliedDigestOf(componentResult.component) : null;
|
|
942
947
|
const outcome = await maybeRecordAutoRelease(
|
|
943
948
|
{
|
|
944
949
|
component: componentResult.component,
|
|
@@ -954,6 +959,7 @@ export async function recordAutoReleasesForRun(
|
|
|
954
959
|
// #4043: the release the deploy was made under, when `--release` named one.
|
|
955
960
|
...(releaseDigest !== undefined ? { releaseDigest } : {}),
|
|
956
961
|
...(releaseDigest !== undefined && templatesDigestOf?.(componentResult.component) ? { templatesDigest: templatesDigestOf(componentResult.component)! } : {}),
|
|
962
|
+
...(applied ? { appliedDigest: applied } : {}),
|
|
957
963
|
},
|
|
958
964
|
{ disabled },
|
|
959
965
|
);
|
|
@@ -1023,7 +1029,8 @@ export async function recordAutoReleasesForRun(
|
|
|
1023
1029
|
* rather than loaded a second time.
|
|
1024
1030
|
*/
|
|
1025
1031
|
export function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
1026
|
-
|
|
1032
|
+
// #4281: the components run against the environment's declared endpoint.
|
|
1033
|
+
return withRecordingChoice(ctx, () => withComponentEndpoint(ctx, () => runComponentSelection(ctx)));
|
|
1027
1034
|
}
|
|
1028
1035
|
|
|
1029
1036
|
/**
|
|
@@ -1281,7 +1288,11 @@ async function runComponentSelection(ctx: CommandContext): Promise<number> {
|
|
|
1281
1288
|
// #3955: a run that records nothing has no release to report either.
|
|
1282
1289
|
const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord) || ledgerRecordingOff();
|
|
1283
1290
|
const templatesDigestOf = underRelease ? await releaseTemplatesDigestOf(underRelease.release, env) : undefined;
|
|
1284
|
-
|
|
1291
|
+
// #4274: a component that publishes nothing is recorded by its deploy-input digest.
|
|
1292
|
+
const appliedOf = result.components ? appliedDigestOf({ components: result.components, env, projectRoot: projectPath }) : undefined;
|
|
1293
|
+
const recorded = await recordAutoReleasesForRun(result.run.results, env, release.runId, disabled, release.runOrigin, underRelease?.releaseDigest, templatesDigestOf, appliedOf);
|
|
1294
|
+
// A generated promote job promotes artifacts; an apply has none to name.
|
|
1295
|
+
const promotable = recorded.filter((r) => !isApplyRecord(r));
|
|
1285
1296
|
if (underRelease) await warnOnArtifactMismatch(underRelease.release, recorded);
|
|
1286
1297
|
// `--digest-file` (#2602): the releases this run recorded, one
|
|
1287
1298
|
// `<component>=<digest>` line each, the form `chant components promote
|
|
@@ -1290,7 +1301,7 @@ async function runComponentSelection(ctx: CommandContext): Promise<number> {
|
|
|
1290
1301
|
if (ctx.args.digestFile) {
|
|
1291
1302
|
const digestPath = resolve(ctx.args.digestFile);
|
|
1292
1303
|
mkdirSync(dirname(digestPath), { recursive: true });
|
|
1293
|
-
writeFileSync(digestPath,
|
|
1304
|
+
writeFileSync(digestPath, promotable.map((r) => `${r.component}=${r.digest}\n`).join(""));
|
|
1294
1305
|
}
|
|
1295
1306
|
}
|
|
1296
1307
|
|
package/src/cli/main.ts
CHANGED
|
@@ -173,6 +173,11 @@ Commands:
|
|
|
173
173
|
terms; --live or --at <ref> with --env <name> reads the
|
|
174
174
|
estate or a recorded snapshot; --show <attr>,
|
|
175
175
|
--check-live, --check-snapshot, --fail-on-drift)
|
|
176
|
+
query "<sql>" [dir] Run one read-only SQL statement over chant/lifecycle:
|
|
177
|
+
the resources and snapshots each lexicon observed, Op
|
|
178
|
+
runs, gates, deploys, releases and cuts, loaded into
|
|
179
|
+
node:sqlite (schema v1, listed in chant_schema).
|
|
180
|
+
--member <name> keeps members; --json prints rows
|
|
176
181
|
generate Generate typed code from sources chant.config declares
|
|
177
182
|
(k8s.crds, helm.charts) into src/generated/<lexicon>/
|
|
178
183
|
(codegen.outDir); --check exits 1 when the committed
|
|
@@ -558,6 +563,15 @@ Ops:
|
|
|
558
563
|
applies it once, if .chant/overriders at base lists
|
|
559
564
|
--actor
|
|
560
565
|
|
|
566
|
+
lock <target>... Lock targets for a pull request on chant/lifecycle
|
|
567
|
+
(--pr <n>, --stage plan|apply|lock, --head <sha>,
|
|
568
|
+
--by <who>, --gate <name>, --json). --from-report
|
|
569
|
+
<pr-plan.json> locks the components a plan selected.
|
|
570
|
+
--forge <kind> reads which holders are still open and
|
|
571
|
+
posts chant/lock on --head. Exits 3 when another open
|
|
572
|
+
pull request holds a target. --list prints the locks
|
|
573
|
+
unlock [<target>...] Release the locks a pull request holds (--pr <n>,
|
|
574
|
+
--gate <name>, --json)
|
|
561
575
|
graph Show Op dependency graph (--stacks for cross-stack order,
|
|
562
576
|
--format ir|mermaid|dot|layout for the lint-gated graph IR,
|
|
563
577
|
a Mermaid flowchart, Graphviz DOT, or node positions;
|
|
@@ -1003,11 +1017,26 @@ Component release ledger + status:
|
|
|
1003
1017
|
report and note (--base <ref> --pr <n>
|
|
1004
1018
|
[--forge github|gitlab|forgejo] posts them);
|
|
1005
1019
|
applies nothing
|
|
1020
|
+
components pr-post Post the plan note and the chant/plan status
|
|
1021
|
+
from the report pr-plan --no-status wrote
|
|
1022
|
+
(--pr <n> --head <sha> --env <env> --forge
|
|
1023
|
+
<kind>), in a job that runs none of the
|
|
1024
|
+
change's code (#4295)
|
|
1006
1025
|
components pr-record On merge (#4168): plan the same members again,
|
|
1007
1026
|
store the change set and write the merge record
|
|
1008
1027
|
_merges/<commit>.json; applies and refuses
|
|
1009
1028
|
nothing, and a plan that differs from the
|
|
1010
1029
|
approved one sets a failing chant/record status
|
|
1030
|
+
components pr-env An environment per open pull request (#4298): up
|
|
1031
|
+
--base <ref> --pr <n> applies the change to pr-<n>,
|
|
1032
|
+
derived from ci.prEnvironment.template, once an
|
|
1033
|
+
approval of its plan stands (chant approve pr-<n>
|
|
1034
|
+
pr-env --plan <digest>); post --pr <n> --head
|
|
1035
|
+
<sha> keeps the record up --no-push handed on and
|
|
1036
|
+
posts the note; down --pr <n> tears down
|
|
1037
|
+
only what carries pr-<n>'s ownership marker; sweep
|
|
1038
|
+
tears down each one whose TTL passed or whose pull
|
|
1039
|
+
request closed. Never a release target
|
|
1011
1040
|
components pr-apply With --apply-on-merge: plan the same members again and apply
|
|
1012
1041
|
them only if an approval stands for that digest
|
|
1013
1042
|
(chant approve pr-<n> pr-apply --plan <digest>);
|
|
@@ -1201,7 +1230,8 @@ Options:
|
|
|
1201
1230
|
chant.config.ts's build.sandbox: true; #1045)
|
|
1202
1231
|
--param <name=value> (build, graph, run --components, components fan-out,
|
|
1203
1232
|
components promote, components rollback,
|
|
1204
|
-
components redeploy,
|
|
1233
|
+
components redeploy, components pr-env up,
|
|
1234
|
+
release cut) Bind a
|
|
1205
1235
|
declared build-time parameter (chant.config.ts's buildParams)
|
|
1206
1236
|
to a value, for source to read as params.<name>
|
|
1207
1237
|
(#1064) instead of process.env — repeatable.
|
|
@@ -1213,7 +1243,8 @@ Options:
|
|
|
1213
1243
|
chant.template.json declares instead (#2627).
|
|
1214
1244
|
--params-file <path> (build, graph, run --components, components fan-out,
|
|
1215
1245
|
components promote, components rollback,
|
|
1216
|
-
components redeploy,
|
|
1246
|
+
components redeploy, components pr-env up,
|
|
1247
|
+
release cut) JSON file
|
|
1217
1248
|
of { "name": value } build-time parameter values
|
|
1218
1249
|
(#1064). Second precedence, after --param.
|
|
1219
1250
|
init --from refuses it: pass --param instead.
|
|
@@ -1441,6 +1472,8 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1441
1472
|
{ name: "describe", handler: runDescribe },
|
|
1442
1473
|
{ name: "explain", handler: runExplain },
|
|
1443
1474
|
{ name: "search", handler: runSearch },
|
|
1475
|
+
// #4297 — SQL over the snapshots, runs, gates, deploys and releases on chant/lifecycle. Imported on first use.
|
|
1476
|
+
{ name: "query", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/query")).runQueryCommand(ctx) },
|
|
1444
1477
|
// chant#2591 — `import --agents` and `audit` read chant.config statically
|
|
1445
1478
|
// and never evaluate it (chant#2589 for audit, which audits code it must not run).
|
|
1446
1479
|
{ name: "import", handler: runImport, runsNoConfig: (args) => args.agents === true },
|
|
@@ -1524,6 +1557,9 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1524
1557
|
{ name: "operator log", handler: runOperatorLog },
|
|
1525
1558
|
{ name: "operator", handler: runOperator },
|
|
1526
1559
|
{ name: "approve", handler: runApprove },
|
|
1560
|
+
// #4293 — target locks a pull request holds on chant/lifecycle. Imported on first use.
|
|
1561
|
+
{ name: "lock", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/lock")).runLock(ctx) },
|
|
1562
|
+
{ name: "unlock", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/lock")).runUnlock(ctx) },
|
|
1527
1563
|
|
|
1528
1564
|
{ name: "graph", handler: runGraph },
|
|
1529
1565
|
{ name: "vendor", handler: runVendor },
|
|
@@ -1613,7 +1649,10 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1613
1649
|
// #3183 — the pull-request loop the generated CI runs. Imported on first use.
|
|
1614
1650
|
{ name: "components pr-plan", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPlan(ctx) },
|
|
1615
1651
|
{ name: "components pr-apply", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrApply(ctx) },
|
|
1652
|
+
// #4295: posts from the plan report in a job that runs none of the change's code, so it loads no plugins.
|
|
1653
|
+
{ name: "components pr-post", handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPost(ctx) },
|
|
1616
1654
|
{ name: "components pr-record", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrRecord(ctx) },
|
|
1655
|
+
{ name: "components pr-env", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr-env")).runComponentsPrEnv(ctx) },
|
|
1617
1656
|
{ name: "components status", requiresPlugins: true, handler: runComponentsStatus },
|
|
1618
1657
|
{ name: "components release", handler: runComponentsReleaseRecord },
|
|
1619
1658
|
{ name: "components export", handler: runComponentsExport },
|
package/src/cli/parse-args.ts
CHANGED
|
@@ -43,6 +43,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
43
43
|
"--stacks",
|
|
44
44
|
"--components",
|
|
45
45
|
"--pr-build",
|
|
46
|
+
"--pr-env",
|
|
46
47
|
"--up",
|
|
47
48
|
"--down",
|
|
48
49
|
"--include-dependents",
|
|
@@ -72,6 +73,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
72
73
|
"--in-place",
|
|
73
74
|
"--require-runner-start",
|
|
74
75
|
"--no-push",
|
|
76
|
+
"--list",
|
|
75
77
|
"--no-require-review",
|
|
76
78
|
"--expire",
|
|
77
79
|
"--check-live",
|
|
@@ -396,11 +398,26 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
396
398
|
if (!result.forge || result.forge.startsWith("-")) throw new Error("--forge needs a forge: --forge github|gitlab|forgejo");
|
|
397
399
|
} else if (arg === "--require-review") {
|
|
398
400
|
result.requireReview = true;
|
|
401
|
+
} else if (arg === "--list") {
|
|
402
|
+
// `chant lock --list` (#4293): print the target locks instead of taking any.
|
|
403
|
+
result.list = true;
|
|
404
|
+
} else if (arg === "--stage") {
|
|
405
|
+
// `chant lock --stage plan|apply|lock` (#4293): how the lock was taken.
|
|
406
|
+
result.lockStage = args[++i];
|
|
407
|
+
if (!result.lockStage || result.lockStage.startsWith("-")) throw new Error("--stage needs plan, apply or lock: --stage <stage>");
|
|
408
|
+
} else if (arg === "--from-report") {
|
|
409
|
+
// `chant lock --from-report <pr-plan.json>` (#4293): lock the components a pull-request report lists.
|
|
410
|
+
result.fromReport = args[++i];
|
|
411
|
+
if (!result.fromReport || result.fromReport.startsWith("-")) throw new Error("--from-report needs a report file: --from-report <pr-plan.json>");
|
|
399
412
|
} else if (arg === "--report-url") {
|
|
400
413
|
// `chant components pr-plan|pr-apply --report-url <url>` (#4007).
|
|
401
414
|
const url = args[++i];
|
|
402
415
|
if (!url || url.startsWith("-")) throw new Error("--report-url needs a URL: --report-url s3://<bucket>/<prefix> or file://<directory>");
|
|
403
416
|
result.reportUrl = url;
|
|
417
|
+
} else if (arg === "--result") {
|
|
418
|
+
// `chant components pr-post --result <result>` (#4295): the plan job's own result, which fails the status when it failed.
|
|
419
|
+
result.planResult = args[++i];
|
|
420
|
+
if (!result.planResult || result.planResult.startsWith("-")) throw new Error("--result needs the plan job's result: --result success|failure|cancelled");
|
|
404
421
|
} else if (arg === "--no-status") {
|
|
405
422
|
// `chant ci run|serve --no-status` (#4097): post no commit status for the job.
|
|
406
423
|
result.noStatus = true;
|
|
@@ -422,6 +439,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
422
439
|
} else if (arg === "--apply-on-merge") {
|
|
423
440
|
// `chant build --pr-loop --apply-on-merge` and `chant ci serve --apply-on-merge` (#4168): opt into applying on merge.
|
|
424
441
|
result.applyOnMerge = true;
|
|
442
|
+
} else if (arg === "--pr-env") {
|
|
443
|
+
// `chant build --pr-loop --pr-env` (#4298): add the pull request environment's jobs.
|
|
444
|
+
result.prEnv = true;
|
|
425
445
|
} else if (arg === "--pr-build") {
|
|
426
446
|
// `chant build --pr-loop --pr-build` (#4088), and `chant ci run --pr-build` (#4094).
|
|
427
447
|
result.prBuild = true;
|
|
@@ -864,8 +884,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
864
884
|
} else if (arg === "--expire") {
|
|
865
885
|
result.expire = true;
|
|
866
886
|
} else if (arg === "--role") {
|
|
867
|
-
// #2508 — a role the approver
|
|
868
|
-
// roles
|
|
887
|
+
// #2508 — a role the approver claims, which a gate's policy sees. A
|
|
888
|
+
// quorum reads roles from .chant/trust.json at base instead (#4290).
|
|
889
|
+
// Repeatable, and a comma list works too.
|
|
869
890
|
const value = args[++i] ?? "";
|
|
870
891
|
result.roles = [...(result.roles ?? []), ...value.split(",").map((r) => r.trim()).filter(Boolean)];
|
|
871
892
|
} else if (arg === "--agent") {
|
package/src/cli/registry.ts
CHANGED
|
@@ -302,6 +302,12 @@ export interface ParsedArgs {
|
|
|
302
302
|
* pr-apply finds it from the commit through `--forge` when omitted.
|
|
303
303
|
*/
|
|
304
304
|
pr?: number;
|
|
305
|
+
/** `chant lock --list` (#4293): print the target locks instead of taking any. */
|
|
306
|
+
list?: boolean;
|
|
307
|
+
/** `chant lock --stage plan|apply|lock` (#4293): how the lock is taken. Default `lock`. */
|
|
308
|
+
lockStage?: string;
|
|
309
|
+
/** `chant lock --from-report <file>` (#4293): lock the components a `pr-plan.json` lists. */
|
|
310
|
+
fromReport?: string;
|
|
305
311
|
/** Every `--pr <n>`, in order: `chant release compose` (#4168 4.3) takes several. */
|
|
306
312
|
prs?: number[];
|
|
307
313
|
/** `chant release cut --channel <tag>` (#4168 4.3): the tag a publish of the release moves in place of `latest`. */
|
|
@@ -345,6 +351,8 @@ export interface ParsedArgs {
|
|
|
345
351
|
* approved plans of the merges between or a gate of its own.
|
|
346
352
|
*/
|
|
347
353
|
green?: boolean;
|
|
354
|
+
/** `chant build --pr-loop --pr-env` (#4298) — add the env-up, env-down and env-sweep jobs of a pull request's environment. */
|
|
355
|
+
prEnv?: boolean;
|
|
348
356
|
/** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
|
|
349
357
|
prBuild?: boolean;
|
|
350
358
|
/** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
|
|
@@ -477,7 +485,7 @@ export interface ParsedArgs {
|
|
|
477
485
|
records?: boolean;
|
|
478
486
|
/** `chant workspace wip save --label <text>` (#3172): what the caller calls the snapshot, such as a turn id. */
|
|
479
487
|
label?: string;
|
|
480
|
-
/** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. */
|
|
488
|
+
/** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. `chant components pr-apply|pr-record --branch <branch>` (#4292): the branch pushed, which `ci.branches` routes members by; default the forge's GITHUB_REF_NAME or CI_COMMIT_BRANCH. */
|
|
481
489
|
branch?: string;
|
|
482
490
|
/** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. Also `chant ci init --workflow <name>` (#4168 step 6), the prescribed workflow to set up. */
|
|
483
491
|
ciWorkflows?: string[];
|
|
@@ -491,6 +499,8 @@ export interface ParsedArgs {
|
|
|
491
499
|
ciSecrets?: string;
|
|
492
500
|
/** `chant ci workflow --chant <command>` (#3573): the command the tick step runs chant with, in place of the npx pin. Also `chant ci run --chant` (#4094): what runs chant in the job. */
|
|
493
501
|
ciChant?: string;
|
|
502
|
+
/** `chant components pr-post --result <result>` (#4295): the plan job's own result. `failure` or `cancelled` fails the `chant/plan` status. */
|
|
503
|
+
planResult?: string;
|
|
494
504
|
/** `chant ci run|serve --no-status` (#4097): post no commit status for the job. `chant components pr-plan|pr-apply --no-status` (#4161): post neither the status nor the note. */
|
|
495
505
|
noStatus?: boolean;
|
|
496
506
|
/** `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries. `{key}` is `<pr>/<commit>[/<member>]`, as `--report-url` keys it. */
|
|
@@ -737,7 +747,7 @@ export interface ParsedArgs {
|
|
|
737
747
|
* on a model-authored channel.
|
|
738
748
|
*/
|
|
739
749
|
allowSameOrigin?: boolean;
|
|
740
|
-
/** `chant approve --role <name>` (#2508) — roles the approver claims,
|
|
750
|
+
/** `chant approve --role <name>` (#2508) — roles the approver claims, which a gate's policy sees. A quorum never counts them (#4290): its roles come from `.chant/trust.json` at base. Repeatable; a comma list also works. */
|
|
741
751
|
roles?: string[];
|
|
742
752
|
/** `chant approve --agent` (#2508) — record the approval as an agent's. An agent never counts toward a quorum; it passes a gate only through a policy permit in `enforce` mode. */
|
|
743
753
|
agent?: boolean;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4274: a component that only deploys infrastructure gets a release record
|
|
3
|
+
* from `pr-apply` and `chant run --components`. Its digest is the plan digest
|
|
4
|
+
* its approval bound, or its deploy-input digest when nothing planned, and
|
|
5
|
+
* `digestKind` says which. Written into a workspace member's ledger, under
|
|
6
|
+
* the member's prefix on chant/lifecycle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { appliedDigestOf } from "./auto-release";
|
|
15
|
+
import { recordAutoReleasesForRun } from "../cli/handlers/run";
|
|
16
|
+
import { clearMemberLedgerCache } from "../lifecycle/member-ledger";
|
|
17
|
+
import { readReleaseLedger } from "../lifecycle/release-ledger";
|
|
18
|
+
import type { DriverComponent, DriverComponentResult } from "./driver";
|
|
19
|
+
|
|
20
|
+
const scratch: string[] = [];
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function git(args: string[], cwd: string): string {
|
|
26
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
27
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
28
|
+
return r.stdout;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A workspace whose member `infra` deploys one CloudFormation template and publishes nothing. */
|
|
32
|
+
function workspace(): { root: string; member: string } {
|
|
33
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-infra-record-")));
|
|
34
|
+
scratch.push(root);
|
|
35
|
+
const files: Record<string, string> = {
|
|
36
|
+
"chant.workspace.json": JSON.stringify({ name: "acme", schema: 1, members: [{ name: "infra", dir: "services/infra", kind: "chant" }] }),
|
|
37
|
+
"services/infra/chant.config.json": JSON.stringify({ lexicons: ["aws"] }),
|
|
38
|
+
"services/infra/dist/infra.template.json": JSON.stringify({ Resources: {} }),
|
|
39
|
+
};
|
|
40
|
+
for (const [path, text] of Object.entries(files)) {
|
|
41
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
42
|
+
writeFileSync(join(root, path), text);
|
|
43
|
+
}
|
|
44
|
+
git(["init", "-q", "-b", "main"], root);
|
|
45
|
+
git(["config", "user.email", "test@chant.dev"], root);
|
|
46
|
+
git(["config", "user.name", "Test"], root);
|
|
47
|
+
git(["add", "-A"], root);
|
|
48
|
+
git(["commit", "-q", "-m", "init"], root);
|
|
49
|
+
return { root, member: join(root, "services/infra") };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const infra: DriverComponent = {
|
|
53
|
+
name: "infra",
|
|
54
|
+
deploy: [{ name: "Deploy", steps: [{ kind: "cfn-deploy", stackName: "infra", template: "dist/infra.template.json" }] }],
|
|
55
|
+
} as unknown as DriverComponent;
|
|
56
|
+
|
|
57
|
+
const applied = {
|
|
58
|
+
component: "infra",
|
|
59
|
+
ok: true,
|
|
60
|
+
status: "ok",
|
|
61
|
+
records: [{ component: "infra", phase: "Deploy", kind: "cfn-deploy", status: "ok", durationMs: 1, output: { stackId: "arn:aws:cloudformation:infra" } }],
|
|
62
|
+
} as unknown as DriverComponentResult;
|
|
63
|
+
|
|
64
|
+
const planDigest = `jcs1-sha256:${"a".repeat(64)}`;
|
|
65
|
+
|
|
66
|
+
let cwd: string;
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
cwd = process.cwd();
|
|
69
|
+
clearMemberLedgerCache();
|
|
70
|
+
process.env.GITHUB_ACTOR = "alice";
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
process.chdir(cwd);
|
|
74
|
+
delete process.env.GITHUB_ACTOR;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("appliedDigestOf", () => {
|
|
78
|
+
test("a component with one planned member gets that member's plan digest", () => {
|
|
79
|
+
const of = appliedDigestOf({ components: [infra], env: "staging", projectRoot: "/nowhere", members: [{ member: "infra", component: "infra", planDigest }] });
|
|
80
|
+
expect(of("infra")).toEqual({ digest: planDigest, kind: "plan" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a component with several planned members gets the set digest over them, the same whatever their order", () => {
|
|
84
|
+
const members = [
|
|
85
|
+
{ member: "infra/a", component: "infra", planDigest },
|
|
86
|
+
{ member: "infra/b", component: "infra", planDigest: `jcs1-sha256:${"b".repeat(64)}` },
|
|
87
|
+
];
|
|
88
|
+
const one = appliedDigestOf({ components: [infra], env: "staging", projectRoot: "/nowhere", members })("infra");
|
|
89
|
+
const two = appliedDigestOf({ components: [infra], env: "staging", projectRoot: "/nowhere", members: [...members].reverse() })("infra");
|
|
90
|
+
expect(one?.kind).toBe("plan");
|
|
91
|
+
expect(one?.digest).not.toBe(planDigest);
|
|
92
|
+
expect(two).toEqual(one);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a component that planned nothing gets its deploy-input digest, which moves with the template it deploys", () => {
|
|
96
|
+
const { member } = workspace();
|
|
97
|
+
const before = appliedDigestOf({ components: [infra], env: "staging", projectRoot: member })("infra");
|
|
98
|
+
expect(before?.kind).toBe("deploy-input");
|
|
99
|
+
writeFileSync(join(member, "dist/infra.template.json"), JSON.stringify({ Resources: { Bucket: {} } }));
|
|
100
|
+
const after = appliedDigestOf({ components: [infra], env: "staging", projectRoot: member })("infra");
|
|
101
|
+
expect(after?.kind).toBe("deploy-input");
|
|
102
|
+
expect(after?.digest).not.toBe(before?.digest);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a component the run doesn't know has none", () => {
|
|
106
|
+
expect(appliedDigestOf({ components: [infra], env: "staging", projectRoot: "/nowhere" })("other")).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("recordAutoReleasesForRun for an infrastructure-only component", () => {
|
|
111
|
+
test("a pr-apply records the plan digest under the member's prefix", async () => {
|
|
112
|
+
const { root, member } = workspace();
|
|
113
|
+
process.chdir(member);
|
|
114
|
+
const of = appliedDigestOf({ components: [infra], env: "staging", projectRoot: member, members: [{ member: "infra", component: "infra", planDigest }] });
|
|
115
|
+
const recorded = await recordAutoReleasesForRun([applied], "staging", "run-1", false, { forge: "local" }, undefined, undefined, of);
|
|
116
|
+
expect(recorded).toHaveLength(1);
|
|
117
|
+
expect(recorded[0]).toMatchObject({ component: "infra", env: "staging", digest: planDigest, digestKind: "plan", actor: "alice" });
|
|
118
|
+
|
|
119
|
+
const files = git(["ls-tree", "-r", "--name-only", "chant/lifecycle"], root).split("\n").filter(Boolean);
|
|
120
|
+
expect(files).toContain("_members/infra/staging/releases.jsonl");
|
|
121
|
+
const { records } = await readReleaseLedger("staging", { cwd: member });
|
|
122
|
+
expect(records).toEqual([expect.objectContaining({ digest: planDigest, digestKind: "plan" })]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("without an applied digest it records nothing, as before", async () => {
|
|
126
|
+
const { member } = workspace();
|
|
127
|
+
process.chdir(member);
|
|
128
|
+
const recorded = await recordAutoReleasesForRun([applied], "staging", "run-1", false);
|
|
129
|
+
expect(recorded).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -181,7 +181,46 @@ describe("maybeRecordAutoRelease", () => {
|
|
|
181
181
|
);
|
|
182
182
|
expect(outcome).toMatchObject({ recorded: true });
|
|
183
183
|
const [input] = appendReleaseRecordMock.mock.calls[0];
|
|
184
|
-
expect(input).toMatchObject({ digest: "sha256:tpl", releaseDigest: "sha256:rel" });
|
|
184
|
+
expect(input).toMatchObject({ digest: "sha256:tpl", releaseDigest: "sha256:rel", digestKind: "templates" });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("#4274: a run that published nothing records what it applied, and says which digest that is", async () => {
|
|
188
|
+
appendReleaseRecordMock.mockResolvedValue({
|
|
189
|
+
commit: "c".repeat(40),
|
|
190
|
+
record: { version: 1, component: "infra", env: "staging", digest: "sha256:plan", gitSha: "x", runId: "r", timestamp: "t", actor: "alice", digestKind: "plan" },
|
|
191
|
+
});
|
|
192
|
+
const outcome = await maybeRecordAutoRelease(
|
|
193
|
+
{ component: "infra", env: "staging", success: true, records: [applyRecord()], runId: "run-1", appliedDigest: { digest: "sha256:plan", kind: "plan" } },
|
|
194
|
+
{ actor: "alice", gitSha: "a".repeat(40) },
|
|
195
|
+
);
|
|
196
|
+
expect(outcome).toMatchObject({ recorded: true });
|
|
197
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ digest: "sha256:plan", digestKind: "plan" });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("#4274: an artifact digest wins over what the run applied, and carries no digestKind", async () => {
|
|
201
|
+
appendReleaseRecordMock.mockResolvedValue({
|
|
202
|
+
commit: "c".repeat(40),
|
|
203
|
+
record: { version: 1, component: "svc", env: "prod", digest: "sha256:abc", gitSha: "x", runId: "r", timestamp: "t", actor: "alice" },
|
|
204
|
+
});
|
|
205
|
+
await maybeRecordAutoRelease(
|
|
206
|
+
{ component: "svc", env: "prod", success: true, records: [publishRecord("sha256:abc")], runId: "run-1", appliedDigest: { digest: "sha256:in", kind: "deploy-input" } },
|
|
207
|
+
{ actor: "alice", gitSha: "a".repeat(40) },
|
|
208
|
+
);
|
|
209
|
+
const [input] = appendReleaseRecordMock.mock.calls[0];
|
|
210
|
+
expect(input).toMatchObject({ digest: "sha256:abc" });
|
|
211
|
+
expect(input).not.toHaveProperty("digestKind");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("#4274: a templates digest under a release wins over what the run applied", async () => {
|
|
215
|
+
appendReleaseRecordMock.mockResolvedValue({
|
|
216
|
+
commit: "c".repeat(40),
|
|
217
|
+
record: { version: 1, component: "infra", env: "staging", digest: "sha256:tpl", gitSha: "x", runId: "r", timestamp: "t", actor: "alice" },
|
|
218
|
+
});
|
|
219
|
+
await maybeRecordAutoRelease(
|
|
220
|
+
{ component: "infra", env: "staging", success: true, records: [applyRecord()], runId: "run-1", releaseDigest: "sha256:rel", templatesDigest: "sha256:tpl", appliedDigest: { digest: "sha256:in", kind: "deploy-input" } },
|
|
221
|
+
{ actor: "alice", gitSha: "a".repeat(40) },
|
|
222
|
+
);
|
|
223
|
+
expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ digest: "sha256:tpl", digestKind: "templates" });
|
|
185
224
|
});
|
|
186
225
|
|
|
187
226
|
test("#4222: a templates digest without a release is ignored", async () => {
|