@intentius/chant 0.105.1 → 0.106.3
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/change-set.d.ts +30 -8
- package/dist/change-set.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +11 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/pr.d.ts +16 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/changed-files.d.ts +8 -2
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/fan-out-record.d.ts +24 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +67 -4
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-run.d.ts +94 -3
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/gated-waves.d.ts +4 -1
- package/dist/gated-waves.d.ts.map +1 -1
- package/dist/lexicon.d.ts +13 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +7 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +2 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +10 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +2 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -2
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +29 -4
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +32 -6
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.d.ts +15 -1
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +1 -1
- package/src/__fixtures__/change-set/warden.change-set.golden.json +2 -2
- package/src/__fixtures__/change-set/warden.plan.json +47 -40
- package/src/__fixtures__/plan-summary/normalization-vectors.json +275 -2
- package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +2 -2
- package/src/__fixtures__/pr-loop/pr-plan.golden.json +2 -2
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -1
- package/src/change-set.test.ts +42 -12
- package/src/change-set.ts +50 -11
- package/src/cli/commands/build.ts +33 -14
- package/src/cli/commands/lint.ts +34 -1
- package/src/cli/handlers/build-lexicon-output.test.ts +102 -0
- package/src/cli/handlers/build.ts +68 -0
- package/src/cli/handlers/fan-out.test.ts +12 -1
- package/src/cli/handlers/fan-out.ts +7 -3
- package/src/cli/handlers/operator.ts +1 -2
- package/src/cli/handlers/pr.ts +223 -35
- package/src/cli/handlers/promote.test.ts +15 -1
- package/src/cli/handlers/promote.ts +5 -3
- package/src/cli/handlers/run.test.ts +215 -1
- package/src/cli/handlers/run.ts +36 -15
- package/src/cli/main.ts +13 -1
- package/src/cli/mcp/never-over-mcp.test.ts +120 -0
- package/src/cli/mcp/op-approve-origin.test.ts +1 -1
- package/src/cli/mcp/op-tools.ts +14 -2
- package/src/cli/mcp/server.test.ts +2 -1
- package/src/cli/registry.ts +7 -0
- package/src/components/changed-files.test.ts +58 -0
- package/src/components/changed-files.ts +19 -4
- package/src/components/fan-out-record.test.ts +47 -2
- package/src/components/fan-out-record.ts +53 -0
- package/src/components/pr-pipeline.ts +96 -6
- package/src/components/pr-run.test.ts +311 -4
- package/src/components/pr-run.ts +198 -5
- package/src/gated-waves.test.ts +11 -0
- package/src/gated-waves.ts +7 -2
- package/src/lexicon.ts +13 -1
- package/src/lifecycle/gate-ledger.ts +4 -3
- package/src/op/gate-name.ts +17 -0
- package/src/op/gate-summary.test.ts +12 -0
- package/src/op/gate-summary.ts +3 -1
- package/src/op/gate.test.ts +15 -1
- package/src/op/gate.ts +21 -4
- package/src/op/index.ts +2 -2
- package/src/op/local-executor.test.ts +35 -0
- package/src/op/local-executor.ts +2 -2
- package/src/op/local-output.test.ts +20 -0
- package/src/op/local-output.ts +4 -4
- package/src/op/runtime.ts +31 -4
- package/src/op/runtimes/local.test.ts +15 -0
- package/src/op/runtimes/local.ts +3 -1
- package/src/pr-loop.test.ts +41 -1
- package/src/pr-loop.ts +41 -10
- package/src/workspace/change-set.schema.json +1 -1
- package/src/workspace/lineage-upgrade-cli.ts +4 -3
- package/src/workspace/lineage-upgrade.test.ts +40 -0
- package/src/workspace/member-pipeline.test.ts +28 -0
- package/src/workspace/member-pipeline.ts +21 -3
- package/src/workspace/pr-report.schema.json +14 -1
package/src/lexicon.ts
CHANGED
|
@@ -41,6 +41,7 @@ export type {
|
|
|
41
41
|
OpRunHandle,
|
|
42
42
|
OpRunRecord,
|
|
43
43
|
OpRunStartOptions,
|
|
44
|
+
OpGateResolveOptions,
|
|
44
45
|
OpRunState,
|
|
45
46
|
OpRunStatus,
|
|
46
47
|
} from "./op/runtime";
|
|
@@ -562,6 +563,12 @@ export interface ComponentPipelineOptions {
|
|
|
562
563
|
* GitLab's job rules) are limited to the member's files and the pipeline
|
|
563
564
|
* file. Its jobs run in the member's directory, and its names carry the
|
|
564
565
|
* member's name so two members' pipelines never collide.
|
|
566
|
+
*
|
|
567
|
+
* A pull-request pipeline (`prLoop`, #3465) takes no path filter: a change
|
|
568
|
+
* outside the member can reach it (a module it calls in another member),
|
|
569
|
+
* so its plan runs on every pull request and selects from the whole
|
|
570
|
+
* change. Its commands pass `--member`, which gives the member its own
|
|
571
|
+
* gate op, note and statuses on the pull request.
|
|
565
572
|
*/
|
|
566
573
|
member?: PipelineMember;
|
|
567
574
|
}
|
|
@@ -913,7 +920,12 @@ export interface ChangedUnitsContext {
|
|
|
913
920
|
projectRoot: string;
|
|
914
921
|
/** The resolved project configuration, for the lexicon's own namespace. */
|
|
915
922
|
config: Record<string, unknown>;
|
|
916
|
-
/**
|
|
923
|
+
/**
|
|
924
|
+
* Files the change added, modified or deleted, relative to `projectRoot`,
|
|
925
|
+
* with `/` separators. Files outside `projectRoot` are listed too, as
|
|
926
|
+
* `../...`, so a unit that calls code outside the project (another
|
|
927
|
+
* workspace member's module, #3465) can claim them.
|
|
928
|
+
*/
|
|
917
929
|
changedFiles: string[];
|
|
918
930
|
}
|
|
919
931
|
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* (absent on the resolution lines written before #2119, which is why
|
|
42
42
|
* `"resolution"` is the default reading).
|
|
43
43
|
*/
|
|
44
|
-
import {
|
|
44
|
+
import { isNeverOverMcpGateOp } from "../op/gate-name";
|
|
45
45
|
import { samePlanDigest } from "./plan-digest";
|
|
46
46
|
import { sortedJsonReplacer } from "../utils";
|
|
47
47
|
import { currentGateOrigin, type GateOrigin } from "./gate-origin";
|
|
@@ -342,8 +342,9 @@ export async function appendPendingGate(
|
|
|
342
342
|
kind: "pending",
|
|
343
343
|
origin: currentGateOrigin(),
|
|
344
344
|
// chant#3485: an Op MCP cannot discover declares nothing, so the gate
|
|
345
|
-
// records the rule for it
|
|
346
|
-
|
|
345
|
+
// records the rule for it: fan-out, workspace-upgrade (chant#3513) and
|
|
346
|
+
// the pull-request gate's `pr-<number>` op.
|
|
347
|
+
...(isNeverOverMcpGateOp(input.op) ? { neverOverMcp: true as const } : {}),
|
|
347
348
|
...input,
|
|
348
349
|
};
|
|
349
350
|
const commit = await appendGateLine(record, "Pending gate record", opts);
|
package/src/op/gate-name.ts
CHANGED
|
@@ -37,6 +37,23 @@ export const FAN_OUT_GATE_OP = "fan-out";
|
|
|
37
37
|
*/
|
|
38
38
|
export const WORKSPACE_UPGRADE_GATE_OP = "workspace-upgrade";
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The pull-request loop records its gate under `pr-<number>`, or
|
|
42
|
+
* `pr-<number>-<member>` in a workspace member (`prOp` in `pr-loop.ts`), and
|
|
43
|
+
* no `*.op.ts` stands behind that name either.
|
|
44
|
+
*/
|
|
45
|
+
const PR_GATE_OP = /^pr-\d+(?:-[a-z0-9][a-z0-9-]*)?$/;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether gates recorded under this op name are never resolved over MCP or ACP
|
|
49
|
+
* (chant#3485, chant#3513). These are the gate-only names MCP cannot discover:
|
|
50
|
+
* `fan-out`, `workspace-upgrade` and the pull-request gate `pr-<number>`. With
|
|
51
|
+
* no Op config to read `neverOverMcp` from, the pending record carries it.
|
|
52
|
+
*/
|
|
53
|
+
export function isNeverOverMcpGateOp(op: string): boolean {
|
|
54
|
+
return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || PR_GATE_OP.test(op);
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
/** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
|
|
41
58
|
export interface GateNamed {
|
|
42
59
|
gate?: string;
|
|
@@ -107,3 +107,15 @@ describe("writeGatedRunSummary surfaces (#2243, #2256)", () => {
|
|
|
107
107
|
expect(writeGatedRunSummary(summary, { CHANT_GATE_SUMMARY: "/no/such/dir/gate.md" })).toBeUndefined();
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
describe("gatedRunSummaryMarkdown on a sealed gate (#3521)", () => {
|
|
112
|
+
test("the approve line carries --sign when sealed", () => {
|
|
113
|
+
const md = gatedRunSummaryMarkdown({ ...summary, sealed: true });
|
|
114
|
+
expect(md).toContain("chant approve app-apply approve-app-apply --sign --approver <you>");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("the approve line has no --sign when unsealed", () => {
|
|
118
|
+
expect(gatedRunSummaryMarkdown({ ...summary, sealed: false })).not.toContain("--sign");
|
|
119
|
+
expect(gatedRunSummaryMarkdown(summary)).not.toContain("--sign");
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/op/gate-summary.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface GatedRunSummary {
|
|
|
57
57
|
* Absent at level 0 and for the root member.
|
|
58
58
|
*/
|
|
59
59
|
ledgerPrefix?: string;
|
|
60
|
+
/** Whether the workspace seals this gate, so the `chant approve` line carries `--sign`. */
|
|
61
|
+
sealed?: boolean;
|
|
60
62
|
/** The environment a component gate's pending fact was recorded in (#2574). The `chant approve` line names it. */
|
|
61
63
|
environment?: string;
|
|
62
64
|
}
|
|
@@ -101,7 +103,7 @@ export function gatedRunSummaryMarkdown(summary: GatedRunSummary): string {
|
|
|
101
103
|
"Approve it, then re-run this workflow:",
|
|
102
104
|
"",
|
|
103
105
|
"```",
|
|
104
|
-
`${approveCommand(summary.op, summary.gate, summary.environment)} --approver <you>`,
|
|
106
|
+
`${approveCommand(summary.op, summary.gate, summary.environment, undefined, summary.sealed === true)} --approver <you>`,
|
|
105
107
|
"```",
|
|
106
108
|
"",
|
|
107
109
|
`Ledger: \`${gateLedgerPath(summary.op, summary.ledgerPrefix)}\` on the \`chant/lifecycle\` branch.`,
|
package/src/op/gate.test.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { writeFileSync } from "node:fs";
|
|
|
16
16
|
import { mkdir, rm } from "node:fs/promises";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
|
-
import { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "./gate";
|
|
19
|
+
import { approveCommand, describeGateMismatch, evaluateGate, gitGateLedgerPort, type GateDigestMismatch, type GateLedgerPort } from "./gate";
|
|
20
20
|
import { appendGateResolution, readGateLedger } from "../lifecycle/gate-ledger";
|
|
21
21
|
import {
|
|
22
22
|
requireLifecycleLedger,
|
|
@@ -612,3 +612,17 @@ describe("op/gate — a rejected ledger push is reported, not swallowed (#2310)"
|
|
|
612
612
|
expect(check.pending.gate).toBe("approve-live-apply");
|
|
613
613
|
});
|
|
614
614
|
});
|
|
615
|
+
|
|
616
|
+
describe("op/gate — the approve hint on a sealed gate (#3521)", () => {
|
|
617
|
+
test("approveCommand adds --sign last, only when sealed", () => {
|
|
618
|
+
expect(approveCommand("app", "ship")).toBe("chant approve app ship");
|
|
619
|
+
expect(approveCommand("app", "ship", "prod", "sha256:abc", true)).toBe("chant approve app ship --env prod --plan sha256:abc --sign");
|
|
620
|
+
expect(approveCommand("app", "ship", undefined, undefined, false)).toBe("chant approve app ship");
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("describeGateMismatch names --sign when sealed, and not when unsealed", () => {
|
|
624
|
+
const mismatch = { approved: "sha256:old", planned: "sha256:new", resolvedBy: "alex", timestamp: "2026-09-08T00:00:00Z" } as GateDigestMismatch;
|
|
625
|
+
expect(describeGateMismatch("app", "ship", mismatch, true)).toMatch(/chant approve app ship --sign$/);
|
|
626
|
+
expect(describeGateMismatch("app", "ship", mismatch)).not.toContain("--sign");
|
|
627
|
+
});
|
|
628
|
+
});
|
package/src/op/gate.ts
CHANGED
|
@@ -235,7 +235,7 @@ export interface GateDigestMismatch {
|
|
|
235
235
|
* have changed — the record simply never said what it approved, which is
|
|
236
236
|
* every record written before #2300.
|
|
237
237
|
*/
|
|
238
|
-
export function describeGateMismatch(op: string, gate: string, mismatch: GateDigestMismatch): string {
|
|
238
|
+
export function describeGateMismatch(op: string, gate: string, mismatch: GateDigestMismatch, sealed = false): string {
|
|
239
239
|
const why =
|
|
240
240
|
mismatch.approved === undefined
|
|
241
241
|
? mismatch.environment !== undefined
|
|
@@ -249,7 +249,7 @@ export function describeGateMismatch(op: string, gate: string, mismatch: GateDig
|
|
|
249
249
|
`Gate "${gate}" is approved, but not for this plan. ` +
|
|
250
250
|
`approved: ${describePlanDigest(mismatch.approved)} (by ${mismatch.resolvedBy} at ${mismatch.timestamp}); ` +
|
|
251
251
|
`planned: ${mismatch.planned}. ` +
|
|
252
|
-
`${why} ${approveCommand(op, gate, mismatch.environment)}`
|
|
252
|
+
`${why} ${approveCommand(op, gate, mismatch.environment, undefined, sealed)}`
|
|
253
253
|
);
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -512,8 +512,25 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
512
512
|
* plan the reader was shown and not whatever is pending by the time they run
|
|
513
513
|
* it.
|
|
514
514
|
*/
|
|
515
|
-
export function approveCommand(op: string, gate: string, environment?: string, planDigest?: string): string {
|
|
515
|
+
export function approveCommand(op: string, gate: string, environment?: string, planDigest?: string, sealed = false): string {
|
|
516
516
|
return `chant approve ${op} ${gate}` +
|
|
517
517
|
(environment === undefined ? "" : ` --env ${environment}`) +
|
|
518
|
-
(planDigest === undefined ? "" : ` --plan ${planDigest}`)
|
|
518
|
+
(planDigest === undefined ? "" : ` --plan ${planDigest}`) +
|
|
519
|
+
(sealed ? " --sign" : "");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Whether the workspace seals `gate` (`identity.gates`), so an approval
|
|
524
|
+
* without `--sign` doesn't count. Callers pass the answer to
|
|
525
|
+
* {@link approveCommand} so the hint names the flag. Loaded on demand, as the
|
|
526
|
+
* git ledger port does: the workspace modules import the Op modules. A read
|
|
527
|
+
* that throws counts as unsealed, since the hint is advice and not the check.
|
|
528
|
+
*/
|
|
529
|
+
export async function gateIsSealed(gate: string, cwd: string = process.cwd()): Promise<boolean> {
|
|
530
|
+
try {
|
|
531
|
+
const { gateAdmission } = await import("../workspace/identity");
|
|
532
|
+
return gateAdmission(cwd, gate) !== null;
|
|
533
|
+
} catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
519
536
|
}
|
package/src/op/index.ts
CHANGED
|
@@ -62,7 +62,7 @@ export { runOpLocally, parseDuration, OpRunFailure } from "./local-executor";
|
|
|
62
62
|
export type { StepRecord, OpRunResult, RunOpOptions } from "./local-executor";
|
|
63
63
|
export { currentOpRun, withOpRunContext } from "./run-context";
|
|
64
64
|
export type { OpRunContext, PassedGate } from "./run-context";
|
|
65
|
-
export { evaluateGate, gitGateLedgerPort, memoryGateLedgerPort, approveCommand, describeGateMismatch } from "./gate";
|
|
65
|
+
export { evaluateGate, gitGateLedgerPort, memoryGateLedgerPort, approveCommand, describeGateMismatch, gateIsSealed } from "./gate";
|
|
66
66
|
export type { GateLedgerPort, GateCheck, GateCheckInput, PendingGatePush, GateDigestMismatch, GateQuorumProgress, GateTally } from "./gate";
|
|
67
67
|
export { tallyGateApprovals, approverOf } from "./gate";
|
|
68
68
|
export {
|
|
@@ -80,7 +80,7 @@ export type { GateNamed } from "./gate-name";
|
|
|
80
80
|
export { createLocalOpRuntime } from "./runtimes/local";
|
|
81
81
|
export { runStateOf } from "./runtime";
|
|
82
82
|
export type {
|
|
83
|
-
OpRuntimeProvider, OpRunHandle, OpRunStartOptions, OpRunState, OpRunStatus,
|
|
83
|
+
OpRuntimeProvider, OpRunHandle, OpRunStartOptions, OpGateResolveOptions, OpRunState, OpRunStatus,
|
|
84
84
|
OpRunRecord, OpRunRecordInput, OpRunPhaseRecord, OpRunStepRecord,
|
|
85
85
|
} from "./runtime";
|
|
86
86
|
export { buildOpIR, serializeOpIR, opConfigFromIR, OP_IR_FORMAT_VERSION } from "./op-ir";
|
|
@@ -9,6 +9,14 @@ import {
|
|
|
9
9
|
import { memoryGateLedgerPort, type GateLedgerPort } from "./gate";
|
|
10
10
|
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
11
11
|
import { renderHuman } from "./local-output";
|
|
12
|
+
|
|
13
|
+
// #3521: whether the workspace seals a gate is read from identity.gates in the
|
|
14
|
+
// working directory; a test says which gates are sealed instead.
|
|
15
|
+
const sealedGates = new Set<string>();
|
|
16
|
+
vi.mock("./gate", async () => {
|
|
17
|
+
const actual = await vi.importActual<typeof import("./gate")>("./gate");
|
|
18
|
+
return { ...actual, gateIsSealed: async (gate: string) => sealedGates.has(gate) };
|
|
19
|
+
});
|
|
12
20
|
import type { GateResolutionRecord, PendingGateRecord, PendingGateInput } from "../lifecycle/gate-ledger";
|
|
13
21
|
import { stepOutput } from "./step-output-ref";
|
|
14
22
|
|
|
@@ -778,6 +786,33 @@ describe("runOpLocally — a gate approves a plan, not the next run (#2300)", ()
|
|
|
778
786
|
expect(lines.some((l) => l.includes("[refused]") && l.includes("not for this plan"))).toBe(true);
|
|
779
787
|
});
|
|
780
788
|
|
|
789
|
+
describe("the refusal's approve command (#3521)", () => {
|
|
790
|
+
async function refusalOnRename(): Promise<string | undefined> {
|
|
791
|
+
const { root, activities, config, digestOf } = harness();
|
|
792
|
+
const gates = approvableLedger();
|
|
793
|
+
await runOpLocally(config, activities, PROFILES, undefined, { gates, now: NOW });
|
|
794
|
+
gates.approve("approve-live-apply", digestOf("aws_s3_bucket.original"), "2026-09-09T12:05:00.000Z");
|
|
795
|
+
root.address = "aws_s3_bucket.renamed";
|
|
796
|
+
const second = await runOpLocally(config, activities, PROFILES, undefined, { gates, now: "2026-09-09T12:10:00.000Z" });
|
|
797
|
+
return second.records.find((r) => r.fn === "gate:approve-live-apply")?.refusal;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
test("unsealed gate: the refusal's approve command has no --sign", async () => {
|
|
801
|
+
const refusal = await refusalOnRename();
|
|
802
|
+
expect(refusal).toContain("chant approve live-apply approve-live-apply");
|
|
803
|
+
expect(refusal).not.toContain("--sign");
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("sealed gate: the refusal's approve command ends in --sign", async () => {
|
|
807
|
+
sealedGates.add("approve-live-apply");
|
|
808
|
+
try {
|
|
809
|
+
expect(await refusalOnRename()).toContain("chant approve live-apply approve-live-apply --sign");
|
|
810
|
+
} finally {
|
|
811
|
+
sealedGates.clear();
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
|
|
781
816
|
test("approve, re-run with the root unchanged — it applies", async () => {
|
|
782
817
|
const { applied, activities, config, digestOf } = harness();
|
|
783
818
|
const gates = approvableLedger();
|
package/src/op/local-executor.ts
CHANGED
|
@@ -26,7 +26,7 @@ import type { ReceiptReadResult } from "./receipt-store";
|
|
|
26
26
|
import { isStepOutputRef } from "./step-output-ref";
|
|
27
27
|
import { parseDuration } from "./duration";
|
|
28
28
|
import { stepTimeoutProblem } from "./activity-profiles";
|
|
29
|
-
import { describeGateMismatch, evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "./gate";
|
|
29
|
+
import { describeGateMismatch, evaluateGate, gateIsSealed, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "./gate";
|
|
30
30
|
import { gateName } from "./gate-name";
|
|
31
31
|
import type { ResolvedGateApproval } from "./gate-approval";
|
|
32
32
|
import { withOpRunContext, type OpRunContext, type PassedGate } from "./run-context";
|
|
@@ -672,7 +672,7 @@ async function runGateStep(
|
|
|
672
672
|
}
|
|
673
673
|
|
|
674
674
|
const refusal = check.mismatch
|
|
675
|
-
? { refusal: describeGateMismatch(gates.op, gateName(step), check.mismatch) }
|
|
675
|
+
? { refusal: describeGateMismatch(gates.op, gateName(step), check.mismatch, await gateIsSealed(gateName(step))) }
|
|
676
676
|
: {};
|
|
677
677
|
return {
|
|
678
678
|
record: {
|
|
@@ -207,3 +207,23 @@ describe("renderJson", () => {
|
|
|
207
207
|
expect(parsed.pushed).toBeUndefined();
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
describe("a sealed gate's approve line carries --sign (#3521)", () => {
|
|
212
|
+
test("renderHuman prints --sign when sealed, and not when unsealed", () => {
|
|
213
|
+
const sealed: string[] = [];
|
|
214
|
+
renderHuman(GATED_WITH_PLAN, (l) => sealed.push(l), true);
|
|
215
|
+
expect(sealed.join("\n")).toContain(`approve : chant approve prod-apply rollout-gate --plan ${GATED_WITH_PLAN.gate!.planDigest} --sign`);
|
|
216
|
+
const unsealed: string[] = [];
|
|
217
|
+
renderHuman(GATED_WITH_PLAN, (l) => unsealed.push(l), false);
|
|
218
|
+
expect(unsealed.join("\n")).not.toContain("--sign");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("renderJson's approve field carries --sign when sealed, and not when unsealed", () => {
|
|
222
|
+
const sealed: string[] = [];
|
|
223
|
+
renderJson(GATED, (l) => sealed.push(l), true);
|
|
224
|
+
expect(JSON.parse(sealed[0]).approve).toBe("chant approve prod-apply rollout-gate --sign");
|
|
225
|
+
const unsealed: string[] = [];
|
|
226
|
+
renderJson(GATED, (l) => unsealed.push(l), false);
|
|
227
|
+
expect(JSON.parse(unsealed[0]).approve).toBe("chant approve prod-apply rollout-gate");
|
|
228
|
+
});
|
|
229
|
+
});
|
package/src/op/local-output.ts
CHANGED
|
@@ -26,7 +26,7 @@ function formatDuration(ms: number): string {
|
|
|
26
26
|
* Render a run result as human-readable progress. Defaults to stderr so stdout
|
|
27
27
|
* stays clean for piping (the `--json` renderer owns stdout).
|
|
28
28
|
*/
|
|
29
|
-
export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
|
|
29
|
+
export function renderHuman(result: OpRunResult, write: Writer = stderr, sealed = false): void {
|
|
30
30
|
let currentPhase: string | undefined;
|
|
31
31
|
for (const record of result.records) {
|
|
32
32
|
if (record.phase !== currentPhase) {
|
|
@@ -100,7 +100,7 @@ export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
|
|
|
100
100
|
// #2300: the plan the approval will be bound to. Printed before the
|
|
101
101
|
// command, because it is what the command approves.
|
|
102
102
|
if (gate.planDigest) write(` plan : ${gate.planDigest}`);
|
|
103
|
-
write(` approve : ${approveCommand(gate.op, gate.gate, undefined, gate.planDigest)}`);
|
|
103
|
+
write(` approve : ${approveCommand(gate.op, gate.gate, undefined, gate.planDigest, sealed)}`);
|
|
104
104
|
if (gate.url) write(` approve at: ${gate.url}`);
|
|
105
105
|
write(` expires : ${gate.expiresAt}`);
|
|
106
106
|
// #2310: this run's own append reached only the local chant/lifecycle
|
|
@@ -127,8 +127,8 @@ export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
|
|
|
127
127
|
* stopped on; the `approve` command line rides alongside so a CI job reporting
|
|
128
128
|
* the gate doesn't have to reassemble it from the op and gate names.
|
|
129
129
|
*/
|
|
130
|
-
export function renderJson(result: OpRunResult, write: Writer = stdout): void {
|
|
131
|
-
const approve = result.gate ? { approve: approveCommand(result.gate.op, result.gate.gate, undefined, result.gate.planDigest) } : {};
|
|
130
|
+
export function renderJson(result: OpRunResult, write: Writer = stdout, sealed = false): void {
|
|
131
|
+
const approve = result.gate ? { approve: approveCommand(result.gate.op, result.gate.gate, undefined, result.gate.planDigest, sealed) } : {};
|
|
132
132
|
// #2310: whether this run's own append reached the remote — not part of
|
|
133
133
|
// the persisted ledger record (a replay has nothing new to report), but a
|
|
134
134
|
// live run's JSON consumer needs it exactly where the human render shows it.
|
package/src/op/runtime.ts
CHANGED
|
@@ -183,10 +183,29 @@ export interface OpRunStartOptions {
|
|
|
183
183
|
signal?: AbortSignal;
|
|
184
184
|
/**
|
|
185
185
|
* For an Op that declares `workLease` (#2748): the work item this run is
|
|
186
|
-
* for (`--work <id>`) and who holds its lease (`--holder`).
|
|
187
|
-
* runtime takes
|
|
186
|
+
* for (`--work <id>`) and who holds its lease (`--holder`). The local
|
|
187
|
+
* runtime takes the lease itself; a runtime that declares
|
|
188
|
+
* {@link OpRuntimeProvider.carriesWork} hands both to where the run
|
|
189
|
+
* executes, which takes it there.
|
|
188
190
|
*/
|
|
189
191
|
work?: { item?: string; holder?: string };
|
|
192
|
+
/**
|
|
193
|
+
* The id to name this run by, in place of one the runtime mints (#3539).
|
|
194
|
+
* The local runtime uses it as the run record's id, the gate ledger's
|
|
195
|
+
* `runId` and `currentOpRun().runId`. `chant acp` passes fountain's
|
|
196
|
+
* conversation id here, so the record a hosted run writes in the sandbox
|
|
197
|
+
* and the run `chant run status --on fountain` reports carry the same id.
|
|
198
|
+
*/
|
|
199
|
+
runId?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** What a caller hands {@link OpRuntimeProvider.resolveGate} besides the resolution. */
|
|
203
|
+
export interface OpGateResolveOptions {
|
|
204
|
+
/**
|
|
205
|
+
* `--env` on `chant run approve` (#3539): the environment the woken run
|
|
206
|
+
* runs in. Omitted, a runtime keeps the environment the gated run had.
|
|
207
|
+
*/
|
|
208
|
+
env?: string;
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
/**
|
|
@@ -202,6 +221,13 @@ export interface OpRuntimeProvider {
|
|
|
202
221
|
/** How the CLI names this runtime in messages, and what `--on` matches. */
|
|
203
222
|
readonly name: string;
|
|
204
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether `start` carries `opts.work` to where the run executes (#3555),
|
|
226
|
+
* so `chant run <op> --work <id> --on <name>` is allowed. Omitted, a
|
|
227
|
+
* non-local runtime is refused `--work` and `--holder` by name.
|
|
228
|
+
*/
|
|
229
|
+
readonly carriesWork?: boolean;
|
|
230
|
+
|
|
205
231
|
/** Start a run. Rejects with an actionable Error when it cannot. */
|
|
206
232
|
start(op: OpConfig, opts: OpRunStartOptions): Promise<OpRunHandle>;
|
|
207
233
|
|
|
@@ -220,9 +246,10 @@ export interface OpRuntimeProvider {
|
|
|
220
246
|
/**
|
|
221
247
|
* Wake a run whose gate has just been resolved. The ledger write is the
|
|
222
248
|
* fact and `chant approve` owns it; this is the runtime's chance to act on
|
|
223
|
-
* it. Omit when a run re-reads the ledger on its own.
|
|
249
|
+
* it. Omit when a run re-reads the ledger on its own. `opts.env` is the
|
|
250
|
+
* `--env` the approver named, when they named one.
|
|
224
251
|
*/
|
|
225
|
-
resolveGate?(op: string, gate: string, resolution: GateResolutionRecord): Promise<void>;
|
|
252
|
+
resolveGate?(op: string, gate: string, resolution: GateResolutionRecord, opts?: OpGateResolveOptions): Promise<void>;
|
|
226
253
|
|
|
227
254
|
/**
|
|
228
255
|
* Host `chant run --components`. Optional: a runtime without it refuses the
|
|
@@ -95,6 +95,21 @@ describe("the local op runtime", () => {
|
|
|
95
95
|
expect(status.result?.record?.id).toBe(handle.runId);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test("a run named by its caller carries that id on the handle, the run context and the record (#3539)", async () => {
|
|
99
|
+
const { currentOpRun } = await import("../run-context");
|
|
100
|
+
let seen: ReturnType<typeof currentOpRun>;
|
|
101
|
+
loadActivitiesMock.mockResolvedValue(new Map([["ok", vi.fn(async () => { seen = currentOpRun(); })]]));
|
|
102
|
+
const runtime = createLocalOpRuntime();
|
|
103
|
+
|
|
104
|
+
const handle = await runtime.start(op("hello", [{ kind: "activity", fn: "ok", args: {} }]), { runId: "conv-42" });
|
|
105
|
+
const status = await handle.result();
|
|
106
|
+
|
|
107
|
+
expect(handle.runId).toBe("conv-42");
|
|
108
|
+
expect(status.runId).toBe("conv-42");
|
|
109
|
+
expect(seen?.runId).toBe("conv-42");
|
|
110
|
+
expect(status.result?.record?.id).toBe("conv-42");
|
|
111
|
+
});
|
|
112
|
+
|
|
98
113
|
test("the project's configured lexicons decide which appliers load", async () => {
|
|
99
114
|
loadActivitiesMock.mockResolvedValue(new Map([["ok", vi.fn(async () => ({}))]]));
|
|
100
115
|
const runtime = createLocalOpRuntime();
|
package/src/op/runtimes/local.ts
CHANGED
|
@@ -139,7 +139,9 @@ export function createLocalOpRuntime(opts: { projectPath?: string } = {}): OpRun
|
|
|
139
139
|
|
|
140
140
|
const [activities, profiles] = await Promise.all([loadActivities(lexicons), loadProfiles()]);
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
// A caller that names the run (#3539: `chant acp` on fountain passes the
|
|
143
|
+
// conversation id) gets its id on the record; otherwise one is minted.
|
|
144
|
+
const runId = startOpts.runId ?? `local-${Date.now()}`;
|
|
143
145
|
const startedAt = new Date().toISOString();
|
|
144
146
|
noteEnv(op);
|
|
145
147
|
|
package/src/pr-loop.test.ts
CHANGED
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
PR_REPORT_SCHEMA_ID,
|
|
17
17
|
prApproveCommand,
|
|
18
18
|
prNoteMarker,
|
|
19
|
+
PR_GATE_OP,
|
|
19
20
|
prOp,
|
|
21
|
+
prStatusContext,
|
|
20
22
|
prStatusDescription,
|
|
21
23
|
prStatusState,
|
|
22
24
|
renderPrNote,
|
|
@@ -101,6 +103,12 @@ const appliedReport: PrReport = {
|
|
|
101
103
|
],
|
|
102
104
|
};
|
|
103
105
|
|
|
106
|
+
const resumedReport: PrReport = {
|
|
107
|
+
...appliedReport,
|
|
108
|
+
members: [{ ...planReport.members[0], status: "applied" }, { ...planReport.members[1], status: "applied" }],
|
|
109
|
+
resumed: { applied: ["a"] },
|
|
110
|
+
};
|
|
111
|
+
|
|
104
112
|
describe("the pr-report schema", () => {
|
|
105
113
|
test("is a valid schema and names this module's id", () => {
|
|
106
114
|
expect(validSchema(schema)).toBe(true);
|
|
@@ -108,7 +116,7 @@ describe("the pr-report schema", () => {
|
|
|
108
116
|
});
|
|
109
117
|
|
|
110
118
|
test("every stage's report validates, and its change set validates against the change-set schema", () => {
|
|
111
|
-
for (const report of [planReport, refusedReport, appliedReport]) {
|
|
119
|
+
for (const report of [planReport, refusedReport, appliedReport, resumedReport]) {
|
|
112
120
|
expectValid(report);
|
|
113
121
|
expectValidChangeSet(report.changeSet);
|
|
114
122
|
expect(report.changeSet.digest).toBe(report.digest);
|
|
@@ -144,6 +152,12 @@ describe("the note", () => {
|
|
|
144
152
|
golden("pr-apply-applied.note.golden.md", note);
|
|
145
153
|
});
|
|
146
154
|
|
|
155
|
+
test("a resumed apply names what the earlier attempt applied (#3464)", () => {
|
|
156
|
+
const note = renderPrNote(resumedReport);
|
|
157
|
+
expect(note).toContain("Resumed under the same approval: `a` applied in an earlier attempt and did not run again.");
|
|
158
|
+
expect(renderPrNote(appliedReport)).not.toContain("Resumed");
|
|
159
|
+
});
|
|
160
|
+
|
|
147
161
|
test("a small limit leaves the summary out rather than cutting the table", () => {
|
|
148
162
|
const note = renderPrNote(planReport, { limit: 800 });
|
|
149
163
|
expect(note).toContain("| `app` |");
|
|
@@ -169,3 +183,29 @@ describe("the status", () => {
|
|
|
169
183
|
expect(prApproveCommand(planReport, "gitlab:ana")).toBe(`chant approve pr-12 pr-apply --plan ${doc.digest} --approver gitlab:ana --sign`);
|
|
170
184
|
});
|
|
171
185
|
});
|
|
186
|
+
|
|
187
|
+
describe("a workspace member's loop (#3465)", () => {
|
|
188
|
+
const memberReport: PrReport = { ...planReport, member: "network", op: prOp(12, "network") };
|
|
189
|
+
|
|
190
|
+
test("the op, the note marker and the status contexts carry the member's name", () => {
|
|
191
|
+
expect(prOp(12)).toBe("pr-12");
|
|
192
|
+
expect(prOp(12, "network")).toBe("pr-12-network");
|
|
193
|
+
expect(prNoteMarker("prod", "network")).toBe("<!-- chant-pr:prod:network -->");
|
|
194
|
+
expect(prNoteMarker("prod", "network")).not.toBe(prNoteMarker("prod"));
|
|
195
|
+
expect(prStatusContext("plan")).toBe("chant/plan");
|
|
196
|
+
expect(prStatusContext("apply", "network")).toBe("chant/apply/network");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("the gate op pattern takes both forms and nothing else", () => {
|
|
200
|
+
for (const op of ["pr-12", "pr-12-network", "pr-3-a1-b2"]) expect(PR_GATE_OP.test(op)).toBe(true);
|
|
201
|
+
for (const op of ["pr-", "pr-x", "pr-12-", "pr-12-Net", "fan-out"]) expect(PR_GATE_OP.test(op)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("the report with a member validates, and its note names the member and approves its own op", () => {
|
|
205
|
+
expectValid(memberReport);
|
|
206
|
+
const note = renderPrNote(memberReport);
|
|
207
|
+
expect(note.startsWith("<!-- chant-pr:" + memberReport.env + ":network -->")).toBe(true);
|
|
208
|
+
expect(note).toContain("in member `network`");
|
|
209
|
+
expect(note).toContain("chant approve pr-12-network pr-apply");
|
|
210
|
+
});
|
|
211
|
+
});
|
package/src/pr-loop.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* One gate per pull request: op `pr-<number>`, gate `pr-apply` unless the
|
|
18
18
|
* pipeline names another. It binds the digest of the change-set document over
|
|
19
|
-
* every planned member (`
|
|
20
|
-
* wave binds over its members (#3049)
|
|
19
|
+
* every planned member (`changeSetDocumentDigest`, #3181): the set digest a
|
|
20
|
+
* gated wave binds over its members (#3049), and the document's entries, holes
|
|
21
|
+
* and side effects with it (#3555). A reviewer approves it with `chant
|
|
21
22
|
* approve pr-<number> pr-apply --plan <digest>`. The op carries the number so
|
|
22
23
|
* that one pull request's approval never answers another's, and the gate name
|
|
23
24
|
* stays fixed so a workspace can require signed approvals for it under
|
|
@@ -26,6 +27,14 @@
|
|
|
26
27
|
* On merge the apply plans the same members again, before anything applies,
|
|
27
28
|
* and decides the gate against the new digest. The same plan applies; a moved
|
|
28
29
|
* plan stops with both digests named and nothing applied.
|
|
30
|
+
*
|
|
31
|
+
* ## Inside a workspace member
|
|
32
|
+
*
|
|
33
|
+
* Each workspace member that generates the pipeline (#3465) runs its own
|
|
34
|
+
* plan and apply on the same pull request, so each keeps its own gate, note
|
|
35
|
+
* and statuses: op `pr-<number>-<member>`, the note marker and the status
|
|
36
|
+
* contexts carry the member's name. The gate name stays the same, so one
|
|
37
|
+
* `identity.gates` entry covers every member.
|
|
29
38
|
*/
|
|
30
39
|
|
|
31
40
|
import type { ChangeSetAction, ChangeSetDocument } from "./change-set";
|
|
@@ -40,19 +49,30 @@ export const PR_REPORT_CONTRACT = 1;
|
|
|
40
49
|
/** The gate a pull request's apply waits on, unless the pipeline names another. */
|
|
41
50
|
export const PR_APPLY_GATE = "pr-apply";
|
|
42
51
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The op a pull request's gate is recorded under: `pr-<number>`, or
|
|
54
|
+
* `pr-<number>-<member>` for a workspace member's pipeline (#3465).
|
|
55
|
+
*/
|
|
56
|
+
export function prOp(pr: number, member?: string): string {
|
|
57
|
+
return member ? `pr-${pr}-${member}` : `pr-${pr}`;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
/**
|
|
49
|
-
export
|
|
50
|
-
|
|
60
|
+
/** Whether `op` is a pull request's gate op, with or without a member. Member names follow the declaration's name pattern. */
|
|
61
|
+
export const PR_GATE_OP = /^pr-\d+(?:-[a-z0-9][a-z0-9-]*)?$/;
|
|
62
|
+
|
|
63
|
+
/** The hidden line that marks the pull request's note, one per environment and, in a workspace, per member. */
|
|
64
|
+
export function prNoteMarker(env: string, member?: string): string {
|
|
65
|
+
return member ? `<!-- chant-pr:${env}:${member} -->` : `<!-- chant-pr:${env} -->`;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
/** The status contexts the two stages set on the commit. */
|
|
54
69
|
export const PR_STATUS_CONTEXTS = { plan: "chant/plan", apply: "chant/apply" } as const;
|
|
55
70
|
|
|
71
|
+
/** A stage's status context: `chant/plan`, or `chant/plan/<member>` for a workspace member's pipeline. */
|
|
72
|
+
export function prStatusContext(stage: "plan" | "apply", member?: string): string {
|
|
73
|
+
return member ? `${PR_STATUS_CONTEXTS[stage]}/${member}` : PR_STATUS_CONTEXTS[stage];
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
/** Changes a member's plan proposes, by the actions a reviewer reads. */
|
|
57
77
|
export interface PrMemberCounts {
|
|
58
78
|
create: number;
|
|
@@ -143,6 +163,8 @@ export interface PrReport {
|
|
|
143
163
|
/** The pull or merge request number, when known. */
|
|
144
164
|
pr: number | null;
|
|
145
165
|
env: string;
|
|
166
|
+
/** The workspace member whose pipeline ran the stage (#3465). Absent outside a workspace member. */
|
|
167
|
+
member?: string;
|
|
146
168
|
/** The commit the change is measured from: the merge base of the given base and `head`. */
|
|
147
169
|
base: string;
|
|
148
170
|
head: string;
|
|
@@ -155,6 +177,12 @@ export interface PrReport {
|
|
|
155
177
|
refusal?: PrRefusal;
|
|
156
178
|
/** The refusal or failure, as printed. */
|
|
157
179
|
message?: string;
|
|
180
|
+
/**
|
|
181
|
+
* Apply stage, `--resume` (#3464): an earlier attempt at the approved set
|
|
182
|
+
* failed partway, and these components applied in it. This run applied
|
|
183
|
+
* the rest under the same approval and did not run them again.
|
|
184
|
+
*/
|
|
185
|
+
resumed?: { applied: string[] };
|
|
158
186
|
approval: PrApproval;
|
|
159
187
|
/** Component names: which changed, which run because they depend on one, and what the change touched that no component deploys. */
|
|
160
188
|
selection: {
|
|
@@ -255,12 +283,15 @@ function approvalLines(report: PrReport, approver: string): string[] {
|
|
|
255
283
|
export function renderPrNote(report: PrReport, options: { limit?: number; approver?: string } = {}): string {
|
|
256
284
|
const limit = options.limit ?? GITHUB_COMMENT_LIMIT;
|
|
257
285
|
const out: string[] = [
|
|
258
|
-
prNoteMarker(report.env),
|
|
259
|
-
`### chant ${report.stage === "plan" ? "plan" : "apply"} for ${code(report.env)}`,
|
|
286
|
+
prNoteMarker(report.env, report.member),
|
|
287
|
+
`### chant ${report.stage === "plan" ? "plan" : "apply"} for ${code(report.env)}${report.member ? ` in member ${code(report.member)}` : ""}`,
|
|
260
288
|
"",
|
|
261
289
|
`${headline(report)} Head ${code(short(report.head))}, measured from ${code(short(report.base))}.`,
|
|
262
290
|
];
|
|
263
291
|
if (report.message) out.push("", `> ${report.message.split("\n").join("\n> ")}`);
|
|
292
|
+
if (report.resumed && report.resumed.applied.length > 0) {
|
|
293
|
+
out.push("", `Resumed under the same approval: ${report.resumed.applied.map(code).join(", ")} applied in an earlier attempt and did not run again.`);
|
|
294
|
+
}
|
|
264
295
|
if (report.members.length > 0) {
|
|
265
296
|
out.push("", "| Member | Component | Status | Create | Update | Replace | Delete |", "|---|---|---|---:|---:|---:|---:|");
|
|
266
297
|
for (const m of report.members) {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"digest": {
|
|
27
27
|
"type": "string",
|
|
28
28
|
"pattern": "^(?:jcs1-)?sha256:[0-9a-f]{64}$",
|
|
29
|
-
"description": "computePlanDigest(\"change-set\", \u2026) over the members' { member, planDigest } pairs sorted by member, provisional members left out. What a gate over the whole run binds."
|
|
29
|
+
"description": "computePlanDigest(\"change-set-document\", \u2026) over the set digest (computePlanDigest(\"change-set\", \u2026) over the members' { member, planDigest } pairs sorted by member), each member's member, status and holes, the entries and the side effects, provisional members and their entries left out. What a gate over the whole run binds."
|
|
30
30
|
},
|
|
31
31
|
"members": {
|
|
32
32
|
"type": "array",
|
|
@@ -15,7 +15,7 @@ import { writeFileSync } from "node:fs";
|
|
|
15
15
|
import { resolve } from "node:path";
|
|
16
16
|
import { formatError, formatInfo, formatSuccess, formatWarning } from "../cli/format";
|
|
17
17
|
import type { CommandContext } from "../cli/registry";
|
|
18
|
-
import { approveCommand, describeGateMismatch, evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "../op/gate";
|
|
18
|
+
import { approveCommand, describeGateMismatch, evaluateGate, gateIsSealed, gitGateLedgerPort, type GateLedgerPort } from "../op/gate";
|
|
19
19
|
import { WORKSPACE_UPGRADE_GATE_OP } from "../op/gate-name";
|
|
20
20
|
import { LockError } from "./lineage-lock";
|
|
21
21
|
import { applyStagedUpgrade, describeStaged, stageUpgrade, type ChantRunner, type StagedUpgrade } from "./lineage-upgrade";
|
|
@@ -104,12 +104,13 @@ export async function upgradeCommand(opts: UpgradeCommandOptions): Promise<Upgra
|
|
|
104
104
|
});
|
|
105
105
|
if (!check.satisfied) {
|
|
106
106
|
if (!opts.json) {
|
|
107
|
-
|
|
107
|
+
const sealed = await gateIsSealed(gate);
|
|
108
|
+
if (check.mismatch) console.error(formatWarning({ message: describeGateMismatch(WORKSPACE_UPGRADE_GATE_OP, gate, check.mismatch, sealed) }));
|
|
108
109
|
if (check.pushWarning) console.error(formatWarning({ message: check.pushWarning }));
|
|
109
110
|
if (check.quorum) console.error(formatInfo(`approvals so far: ${check.quorum.approvers.length} of ${check.quorum.need}`));
|
|
110
111
|
console.error(
|
|
111
112
|
formatInfo(
|
|
112
|
-
`Gated: the patch ${staged.digest} waits for approval. Review it (--output <file> writes it), then run \`${approveCommand(WORKSPACE_UPGRADE_GATE_OP, gate)}\` and repeat this command to apply it.`,
|
|
113
|
+
`Gated: the patch ${staged.digest} waits for approval. Review it (--output <file> writes it), then run \`${approveCommand(WORKSPACE_UPGRADE_GATE_OP, gate, undefined, undefined, sealed)}\` and repeat this command to apply it.`,
|
|
113
114
|
),
|
|
114
115
|
);
|
|
115
116
|
}
|