@intentius/chant 0.58.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/core.d.ts +17 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +15 -4
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +13 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts +0 -16
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +22 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +4 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +19 -4
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +1 -7
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.d.ts.map +1 -1
- package/dist/lexicon.d.ts +170 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +9 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +62 -2
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/builders.d.ts +2 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/change-signal.d.ts +91 -0
- package/dist/op/change-signal.d.ts.map +1 -0
- package/dist/op/composites/apply-op.d.ts +7 -2
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/reconcile-op.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +40 -0
- package/dist/op/gate-name.d.ts.map +1 -0
- package/dist/op/gate-summary.d.ts +43 -0
- package/dist/op/gate-summary.d.ts.map +1 -0
- package/dist/op/index.d.ts +6 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/operator.d.ts +63 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/types.d.ts +18 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts +26 -7
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +28 -2
- package/dist/terraform/carve-provider.d.ts.map +1 -1
- package/dist/terraform/data-source-shape.d.ts +77 -0
- package/dist/terraform/data-source-shape.d.ts.map +1 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/providers/kubernetes.d.ts +5 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +16 -4
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/core.ts +25 -3
- package/src/audit/discover.ts +122 -14
- package/src/cli/commands/audit.test.ts +5 -3
- package/src/cli/commands/audit.ts +5 -1
- package/src/cli/commands/build.test.ts +39 -1
- package/src/cli/commands/build.ts +14 -0
- package/src/cli/commands/carve-bridge.test.ts +237 -14
- package/src/cli/commands/carve-bridge.ts +21 -17
- package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
- package/src/cli/commands/lint.test.ts +146 -1
- package/src/cli/commands/lint.ts +82 -7
- package/src/cli/handlers/operator.ts +81 -0
- package/src/cli/handlers/run.test.ts +132 -3
- package/src/cli/handlers/run.ts +87 -3
- package/src/cli/main.test.ts +9 -11
- package/src/cli/main.ts +5 -27
- package/src/cli/plugins.test.ts +68 -2
- package/src/cli/plugins.ts +39 -0
- package/src/cli/registry.ts +14 -0
- package/src/components/README.md +2 -2
- package/src/components/__fixtures__/neo4j-fanout.json +1 -1
- package/src/components/cli-support.test.ts +18 -7
- package/src/components/cli-support.ts +8 -3
- package/src/components/component-schema.test.ts +18 -2
- package/src/components/component.schema.json +17 -4
- package/src/components/component.test.ts +2 -2
- package/src/components/component.ts +25 -5
- package/src/components/config-defaults.test.ts +2 -2
- package/src/components/driver.test.ts +20 -1
- package/src/components/driver.ts +12 -5
- package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
- package/src/components/verbs/run-agent.test.ts +19 -0
- package/src/components/verbs/run-agent.ts +1 -7
- package/src/detectLexicon.ts +18 -1
- package/src/discovery/fold-import.test.ts +1 -1
- package/src/fold/foldable-helpers.ts +1 -1
- package/src/graph-ops.test.ts +1 -1
- package/src/lexicon.ts +177 -3
- package/src/lifecycle/gate-ledger.ts +12 -1
- package/src/lint/pipeline-change-gate.test.ts +2 -2
- package/src/lint/rules/comp/comp.test.ts +26 -0
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +4 -2
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +1 -1
- package/src/op/activities/index.ts +2 -2
- package/src/op/activities/reconcile.test.ts +82 -2
- package/src/op/activities/reconcile.ts +169 -2
- package/src/op/builders.ts +3 -3
- package/src/op/change-signal.test.ts +117 -0
- package/src/op/change-signal.ts +169 -0
- package/src/op/composites/apply-op.ts +14 -4
- package/src/op/composites/composites.test.ts +17 -4
- package/src/op/composites/reconcile-op.ts +7 -4
- package/src/op/effect-step.test.ts +3 -3
- package/src/op/gate-name.test.ts +65 -0
- package/src/op/gate-name.ts +60 -0
- package/src/op/gate-summary.ts +84 -0
- package/src/op/index.ts +9 -2
- package/src/op/local-executor.test.ts +16 -1
- package/src/op/local-executor.ts +4 -3
- package/src/op/op-ir.test.ts +12 -1
- package/src/op/op-ir.ts +5 -3
- package/src/op/op-verb-class.test.ts +2 -2
- package/src/op/op.test.ts +2 -2
- package/src/op/operator.test.ts +368 -0
- package/src/op/operator.ts +141 -5
- package/src/op/runtimes/local.test.ts +1 -1
- package/src/op/types.ts +23 -3
- package/src/terraform/aws-resources.test.ts +13 -4
- package/src/terraform/bridge.test.ts +22 -9
- package/src/terraform/bridge.ts +89 -28
- package/src/terraform/carve-provider.ts +38 -2
- package/src/terraform/data-source-shape.ts +95 -0
- package/src/terraform/graph.ts +35 -7
- package/src/terraform/providers/kubernetes.ts +48 -2
- package/src/terraform/tier-map.ts +21 -6
- package/src/terraform/types.ts +8 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `signalName` -> `gate` rename (#2202) and the one-version bridge that
|
|
3
|
+
* keeps the old key readable through 0.59.0.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { gateName, usesDeprecatedGateKey, opUsesDeprecatedGateKey } from "./gate-name";
|
|
7
|
+
import type { OpConfig } from "./types";
|
|
8
|
+
|
|
9
|
+
describe("gateName", () => {
|
|
10
|
+
it("reads the new key", () => {
|
|
11
|
+
expect(gateName({ gate: "approve-prod" })).toBe("approve-prod");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("falls back to the deprecated key", () => {
|
|
15
|
+
expect(gateName({ signalName: "approve-prod" })).toBe("approve-prod");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("prefers the new key when a step somehow carries both", () => {
|
|
19
|
+
expect(gateName({ gate: "new", signalName: "old" })).toBe("new");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("usesDeprecatedGateKey", () => {
|
|
24
|
+
it("is true only when the new key is absent", () => {
|
|
25
|
+
expect(usesDeprecatedGateKey({ signalName: "old" })).toBe(true);
|
|
26
|
+
expect(usesDeprecatedGateKey({ gate: "new" })).toBe(false);
|
|
27
|
+
expect(usesDeprecatedGateKey({ gate: "new", signalName: "old" })).toBe(false);
|
|
28
|
+
expect(usesDeprecatedGateKey({})).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("opUsesDeprecatedGateKey", () => {
|
|
33
|
+
const op = (phases: OpConfig["phases"], onFailure?: OpConfig["phases"]): OpConfig =>
|
|
34
|
+
({ name: "x", overview: "x", phases, ...(onFailure ? { onFailure } : {}) }) as OpConfig;
|
|
35
|
+
|
|
36
|
+
it("finds a gate on the old key in a plain phase", () => {
|
|
37
|
+
expect(opUsesDeprecatedGateKey(op([{ name: "Approve", steps: [{ kind: "gate", signalName: "go" }] }]))).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("finds one nested inside an effect step", () => {
|
|
41
|
+
const config = op([
|
|
42
|
+
{
|
|
43
|
+
name: "Seed",
|
|
44
|
+
steps: [
|
|
45
|
+
{
|
|
46
|
+
kind: "effect",
|
|
47
|
+
receipt: { name: "seeded", effect: "db-seed", flavor: "hash", inputs: { file: "seed.sql" } },
|
|
48
|
+
steps: [{ kind: "gate", signalName: "go" }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
expect(opUsesDeprecatedGateKey(config)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("finds one in an onFailure phase", () => {
|
|
57
|
+
expect(
|
|
58
|
+
opUsesDeprecatedGateKey(op([{ name: "Apply", steps: [] }], [{ name: "Undo", steps: [{ kind: "gate", signalName: "go" }] }])),
|
|
59
|
+
).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("is false for an Op on the new key", () => {
|
|
63
|
+
expect(opUsesDeprecatedGateKey(op([{ name: "Approve", steps: [{ kind: "gate", gate: "go" }] }]))).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate step's name field, and the one-version bridge for the key it used
|
|
3
|
+
* to carry (chant #2202).
|
|
4
|
+
*
|
|
5
|
+
* A gate is a fact on the gate ledger, not a channel: since #2119 nothing is
|
|
6
|
+
* running and nothing is signalled, so the field that names a gate is called
|
|
7
|
+
* `gate` — the same word `chant approve <op> <gate>` takes on the command
|
|
8
|
+
* line. It was called `signalName` through 0.58.0.
|
|
9
|
+
*
|
|
10
|
+
* `signalName` is still read, on `GateStep`, on the component `Gate` step and
|
|
11
|
+
* on the composites' gate options, and a build that sees only the old key
|
|
12
|
+
* warns once. It goes away in 0.60.0: delete this module's
|
|
13
|
+
* {@link DEPRECATED_GATE_KEY_WARNING}, {@link usesDeprecatedGateKey}, the
|
|
14
|
+
* `signalName` branch of {@link gateName}, the optional `signalName` fields
|
|
15
|
+
* that call it, and the schema's `oneOf` once that release is out, leaving
|
|
16
|
+
* `gate` as the only key.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { OpConfig, PhaseDefinition, StepDefinition } from "./types";
|
|
20
|
+
|
|
21
|
+
/** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
|
|
22
|
+
export interface GateNamed {
|
|
23
|
+
gate?: string;
|
|
24
|
+
/** @deprecated Renamed to `gate` in #2202. Accepted through 0.59.0, removed in 0.60.0. */
|
|
25
|
+
signalName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The gate's name, from whichever key carries it. One place reads the old key,
|
|
30
|
+
* so removing the bridge is a one-file edit rather than a sweep.
|
|
31
|
+
*/
|
|
32
|
+
export function gateName(step: GateNamed): string {
|
|
33
|
+
return step.gate ?? step.signalName ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when a step names its gate only with the deprecated key. */
|
|
37
|
+
export function usesDeprecatedGateKey(step: GateNamed): boolean {
|
|
38
|
+
return step.gate === undefined && typeof step.signalName === "string";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What a build prints once when it finds a gate step still using the old key. */
|
|
42
|
+
export const DEPRECATED_GATE_KEY_WARNING =
|
|
43
|
+
'Gate steps still using "signalName": the key is now "gate" (chant #2202). ' +
|
|
44
|
+
'"signalName" is accepted through 0.59.0 and removed in 0.60.0.';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* True when any gate step in an Op's phases (main or `onFailure`, at any
|
|
48
|
+
* nesting depth inside an `effect()`) names itself only with the deprecated
|
|
49
|
+
* key. `chant build` calls this to warn once per build.
|
|
50
|
+
*/
|
|
51
|
+
export function opUsesDeprecatedGateKey(config: OpConfig): boolean {
|
|
52
|
+
const inStep = (step: StepDefinition): boolean => {
|
|
53
|
+
if (step.kind === "gate") return usesDeprecatedGateKey(step);
|
|
54
|
+
if (step.kind === "effect") return step.steps.some(inStep);
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
const inPhases = (phases: PhaseDefinition[] | undefined): boolean =>
|
|
58
|
+
(phases ?? []).some((p) => p.steps.some(inStep));
|
|
59
|
+
return inPhases(config.phases) || inPhases(config.onFailure);
|
|
60
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pending-gate block a CI run leaves where people actually look (#2243).
|
|
3
|
+
*
|
|
4
|
+
* A gated run already prints the gate, the approve command and the expiry to
|
|
5
|
+
* stderr (`./local-output.ts`'s `renderHuman`). In CI that lands in a log
|
|
6
|
+
* nobody opens unless the run is red, which is exactly the run this stops
|
|
7
|
+
* being red. GitHub Actions, Forgejo Actions and Gitea all give a step a
|
|
8
|
+
* markdown scratchpad through `GITHUB_STEP_SUMMARY`, rendered at the top of
|
|
9
|
+
* the run page; anything else that sets the variable gets the same block.
|
|
10
|
+
*
|
|
11
|
+
* That is the whole forge coupling: one environment variable, appended to
|
|
12
|
+
* when it is set, and nothing at all when it is not. No API call, no token,
|
|
13
|
+
* no flag to turn it on.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { appendFileSync } from "node:fs";
|
|
17
|
+
import { gateLedgerPath } from "../lifecycle/gate-ledger";
|
|
18
|
+
import { approveCommand } from "./gate";
|
|
19
|
+
|
|
20
|
+
/** What a gated run knows about the gate it stopped on. */
|
|
21
|
+
export interface GatedRunSummary {
|
|
22
|
+
/** The Op (or component) the gate belongs to. */
|
|
23
|
+
op: string;
|
|
24
|
+
/** The gate's name — the second argument to `chant approve`. */
|
|
25
|
+
gate: string;
|
|
26
|
+
/** The gate's authored description, when it has one. */
|
|
27
|
+
description?: string;
|
|
28
|
+
/** ISO-8601 instant the pending fact ages out at. */
|
|
29
|
+
expiresAt?: string;
|
|
30
|
+
/** The approval surface the run resolved, when it knew one. */
|
|
31
|
+
url?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The markdown block itself. Pure, so a test reads what CI would render
|
|
36
|
+
* without touching a file: op, gate, the exact `chant approve` line, and the
|
|
37
|
+
* ledger path the pending fact was appended to.
|
|
38
|
+
*/
|
|
39
|
+
export function gatedRunSummaryMarkdown(summary: GatedRunSummary): string {
|
|
40
|
+
const lines = [
|
|
41
|
+
`### Waiting on gate \`${summary.gate}\``,
|
|
42
|
+
"",
|
|
43
|
+
`Op \`${summary.op}\` reached a gate nobody has approved, recorded the pending fact, and stopped. Nothing failed.`,
|
|
44
|
+
"",
|
|
45
|
+
];
|
|
46
|
+
if (summary.description) {
|
|
47
|
+
lines.push(summary.description, "");
|
|
48
|
+
}
|
|
49
|
+
lines.push(
|
|
50
|
+
"Approve it, then re-run this workflow:",
|
|
51
|
+
"",
|
|
52
|
+
"```",
|
|
53
|
+
`${approveCommand(summary.op, summary.gate)} --approver <you>`,
|
|
54
|
+
"```",
|
|
55
|
+
"",
|
|
56
|
+
`Ledger: \`${gateLedgerPath(summary.op)}\` on the \`chant/lifecycle\` branch.`,
|
|
57
|
+
);
|
|
58
|
+
if (summary.expiresAt) lines.push(`Expires: ${summary.expiresAt}`);
|
|
59
|
+
if (summary.url) lines.push(`Approve at: ${summary.url}`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Append {@link gatedRunSummaryMarkdown} to the file `GITHUB_STEP_SUMMARY`
|
|
66
|
+
* names, when the variable is set and the file can be written.
|
|
67
|
+
*
|
|
68
|
+
* Returns the path written, or `undefined` when there was nothing to write to.
|
|
69
|
+
* A write that fails is swallowed: a run that already decided its outcome must
|
|
70
|
+
* not turn into a different one because a CI scratchpad was read-only.
|
|
71
|
+
*/
|
|
72
|
+
export function writeGatedRunSummary(
|
|
73
|
+
summary: GatedRunSummary,
|
|
74
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
const path = env.GITHUB_STEP_SUMMARY;
|
|
77
|
+
if (!path) return undefined;
|
|
78
|
+
try {
|
|
79
|
+
appendFileSync(path, gatedRunSummaryMarkdown(summary));
|
|
80
|
+
return path;
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/op/index.ts
CHANGED
|
@@ -52,6 +52,8 @@ export { runOpLocally, parseDuration, OpRunFailure } from "./local-executor";
|
|
|
52
52
|
export type { StepRecord, OpRunResult, RunOpOptions } from "./local-executor";
|
|
53
53
|
export { evaluateGate, gitGateLedgerPort, memoryGateLedgerPort, approveCommand } from "./gate";
|
|
54
54
|
export type { GateLedgerPort, GateCheck, GateCheckInput } from "./gate";
|
|
55
|
+
export { gateName, usesDeprecatedGateKey, DEPRECATED_GATE_KEY_WARNING } from "./gate-name";
|
|
56
|
+
export type { GateNamed } from "./gate-name";
|
|
55
57
|
export { createLocalOpRuntime } from "./runtimes/local";
|
|
56
58
|
export { runStateOf } from "./runtime";
|
|
57
59
|
export type {
|
|
@@ -85,9 +87,14 @@ export type {
|
|
|
85
87
|
} from "./converge-rule";
|
|
86
88
|
export {
|
|
87
89
|
discoverConvergeOps, runOperatorRound, runOperatorForever, formatRoundLine,
|
|
88
|
-
DEFAULT_OPERATOR_INTERVAL_MS,
|
|
90
|
+
formatSignalLine, DEFAULT_OPERATOR_INTERVAL_MS,
|
|
89
91
|
} from "./operator";
|
|
90
|
-
export type {
|
|
92
|
+
export type {
|
|
93
|
+
OperatorTickEvent, OperatorRoundOptions, OperatorLoopOptions,
|
|
94
|
+
ChangeSubscriber, OperatorSignalEvent,
|
|
95
|
+
} from "./operator";
|
|
96
|
+
export { createChangeSignalGate, DEFAULT_SIGNAL_FLOOR_MS } from "./change-signal";
|
|
97
|
+
export type { ChangeSignalGate, ChangeSignalGateOptions, WakeReason } from "./change-signal";
|
|
91
98
|
export { classifyOpVerbClass, isGated } from "./op-verb-class";
|
|
92
99
|
export type { OpVerbClass } from "./op-verb-class";
|
|
93
100
|
export { takeProfileAndId } from "./builders";
|
|
@@ -284,7 +284,7 @@ describe("runOpLocally — gate as fact (#2119)", () => {
|
|
|
284
284
|
phases: [
|
|
285
285
|
{ name: "P1", steps: [
|
|
286
286
|
{ kind: "activity", fn: "before" },
|
|
287
|
-
{ kind: "gate",
|
|
287
|
+
{ kind: "gate", gate: "approve-prod", description: "release manager signs off", timeout: "24h" },
|
|
288
288
|
{ kind: "activity", fn: "after" },
|
|
289
289
|
] },
|
|
290
290
|
{ name: "P2", steps: [{ kind: "activity", fn: "later" }] },
|
|
@@ -324,6 +324,21 @@ describe("runOpLocally — gate as fact (#2119)", () => {
|
|
|
324
324
|
]);
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
+
// #2202: the gate step's name key is `gate`; `signalName` is read through
|
|
328
|
+
// 0.59.0, so a step still spelling it that way reaches the same ledger entry.
|
|
329
|
+
test("a gate step still using the deprecated `signalName` key names the same gate", async () => {
|
|
330
|
+
const { activities } = tracked();
|
|
331
|
+
const legacy = op({
|
|
332
|
+
phases: [{ name: "P1", steps: [{ kind: "gate", signalName: "approve-prod" }] }],
|
|
333
|
+
});
|
|
334
|
+
const port = memoryGateLedgerPort();
|
|
335
|
+
const result = await runOpLocally(legacy, activities, PROFILES, undefined, { gates: port, now: NOW });
|
|
336
|
+
|
|
337
|
+
expect(result.status).toBe("gated");
|
|
338
|
+
expect(result.gate).toMatchObject({ op: "test-op", gate: "approve-prod" });
|
|
339
|
+
expect(result.records.map((r) => r.fn)).toEqual(["gate:approve-prod"]);
|
|
340
|
+
});
|
|
341
|
+
|
|
327
342
|
test("onFailure phases do not run on a gated run", async () => {
|
|
328
343
|
const { calls, activities } = tracked();
|
|
329
344
|
await runOpLocally(gatedOp(), activities, PROFILES, undefined, { gates: memoryGateLedgerPort(), now: NOW });
|
package/src/op/local-executor.ts
CHANGED
|
@@ -26,6 +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 { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "./gate";
|
|
29
|
+
import { gateName } from "./gate-name";
|
|
29
30
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
30
31
|
import { appendRunRecord, buildRunRecord } from "../lifecycle/run-ledger";
|
|
31
32
|
import type { OpRunRecord } from "./runtime";
|
|
@@ -342,7 +343,7 @@ function pushRecord(sink: StepRecord[], ctx: GateContext, ...recs: StepRecord[])
|
|
|
342
343
|
|
|
343
344
|
/** The record name a gate step lands under, so a reader (and a JSON consumer) can pick it out of `records`. */
|
|
344
345
|
function gateFn(step: GateStep): string {
|
|
345
|
-
return `gate:${step
|
|
346
|
+
return `gate:${gateName(step)}`;
|
|
346
347
|
}
|
|
347
348
|
|
|
348
349
|
/**
|
|
@@ -359,7 +360,7 @@ async function runGateStep(
|
|
|
359
360
|
const start = Date.now();
|
|
360
361
|
const check = await evaluateGate(gates.port, {
|
|
361
362
|
op: gates.op,
|
|
362
|
-
gate: step
|
|
363
|
+
gate: gateName(step),
|
|
363
364
|
...(step.description ? { description: step.description } : {}),
|
|
364
365
|
...(step.timeout ? { timeout: step.timeout } : {}),
|
|
365
366
|
...(gates.runId ? { runId: gates.runId } : {}),
|
|
@@ -376,7 +377,7 @@ async function runGateStep(
|
|
|
376
377
|
status: "ok",
|
|
377
378
|
durationMs: Date.now() - start,
|
|
378
379
|
approval: {
|
|
379
|
-
gate: step
|
|
380
|
+
gate: gateName(step),
|
|
380
381
|
resolvedBy: resolution.resolvedBy,
|
|
381
382
|
timestamp: resolution.timestamp,
|
|
382
383
|
...(resolution.url ? { url: resolution.url } : {}),
|
package/src/op/op-ir.test.ts
CHANGED
|
@@ -126,7 +126,7 @@ describe("op.json IR", () => {
|
|
|
126
126
|
expect(build.steps[0]).toMatchObject({ kind: "activity", fn: "shellCmd", profile: "fastIdempotent" });
|
|
127
127
|
expect(approve.steps[0]).toMatchObject({
|
|
128
128
|
kind: "gate",
|
|
129
|
-
|
|
129
|
+
gate: "approve-deploy",
|
|
130
130
|
timeout: "24h",
|
|
131
131
|
description: "Release manager sign-off",
|
|
132
132
|
});
|
|
@@ -141,6 +141,17 @@ describe("op.json IR", () => {
|
|
|
141
141
|
expect(ir.onFailure[0].name).toBe("Rollback");
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
// #2202: whichever key an authored gate step used, the IR writes `gate`, so
|
|
145
|
+
// a foreign consumer of op.json never sees the deprecated spelling.
|
|
146
|
+
it("normalizes a gate step's deprecated `signalName` key onto `gate`", () => {
|
|
147
|
+
const config = representativeOp();
|
|
148
|
+
const approve = config.phases.find((p) => p.name === "Approve")!;
|
|
149
|
+
approve.steps[0] = { kind: "gate", signalName: "approve-deploy", timeout: "24h" };
|
|
150
|
+
const ir = buildOpIR(config);
|
|
151
|
+
expect(ir.phases[1].steps[0]).toMatchObject({ kind: "gate", gate: "approve-deploy", timeout: "24h" });
|
|
152
|
+
expect(serializeOpIR(config)).not.toContain("signalName");
|
|
153
|
+
});
|
|
154
|
+
|
|
144
155
|
it("contracts are the injected registry's: empty until a caller supplies one", () => {
|
|
145
156
|
const bare = buildOpIR(representativeOp());
|
|
146
157
|
expect(bare.activityContracts).toEqual({});
|
package/src/op/op-ir.ts
CHANGED
|
@@ -95,6 +95,7 @@ import type {
|
|
|
95
95
|
import type { EffectReceiptRef } from "./receipt-store";
|
|
96
96
|
import type { ActivityContract } from "./activity-contract";
|
|
97
97
|
import { ACTIVITY_PROFILES, type ActivityProfile } from "./activity-profiles";
|
|
98
|
+
import { gateName } from "./gate-name";
|
|
98
99
|
|
|
99
100
|
/**
|
|
100
101
|
* The op.json IR schema version. Bumped on a breaking change to this
|
|
@@ -131,7 +132,8 @@ export interface OpIRActivityStep {
|
|
|
131
132
|
|
|
132
133
|
export interface OpIRGateStep {
|
|
133
134
|
kind: "gate";
|
|
134
|
-
signalName
|
|
135
|
+
/** The gate's name — `GateStep.gate`, or its deprecated `signalName` spelling (#2202). */
|
|
136
|
+
gate: string;
|
|
135
137
|
/** Resolved to its effective value — `GateStep.timeout ?? "48h"`. */
|
|
136
138
|
timeout: string;
|
|
137
139
|
description?: string;
|
|
@@ -227,7 +229,7 @@ function irActivityStep(step: ActivityStep, contracts: ReadonlyMap<string, Activ
|
|
|
227
229
|
function irGateStep(step: GateStep): OpIRGateStep {
|
|
228
230
|
return {
|
|
229
231
|
kind: "gate",
|
|
230
|
-
|
|
232
|
+
gate: gateName(step),
|
|
231
233
|
timeout: step.timeout ?? "48h",
|
|
232
234
|
...(step.description ? { description: step.description } : {}),
|
|
233
235
|
};
|
|
@@ -375,7 +377,7 @@ function opStepFromIR(step: OpIRStep): StepDefinition {
|
|
|
375
377
|
if (step.kind === "gate") {
|
|
376
378
|
return {
|
|
377
379
|
kind: "gate",
|
|
378
|
-
|
|
380
|
+
gate: step.gate,
|
|
379
381
|
timeout: step.timeout,
|
|
380
382
|
...(step.description ? { description: step.description } : {}),
|
|
381
383
|
};
|
|
@@ -106,7 +106,7 @@ describe("isGated", () => {
|
|
|
106
106
|
expect(
|
|
107
107
|
isGated(
|
|
108
108
|
cfg([
|
|
109
|
-
{ name: "Approve", steps: [{ kind: "gate",
|
|
109
|
+
{ name: "Approve", steps: [{ kind: "gate", gate: "approve-x" }] },
|
|
110
110
|
{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] },
|
|
111
111
|
]),
|
|
112
112
|
),
|
|
@@ -118,7 +118,7 @@ describe("isGated", () => {
|
|
|
118
118
|
isGated(
|
|
119
119
|
cfg(
|
|
120
120
|
[{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] }],
|
|
121
|
-
[{ name: "Rollback", steps: [{ kind: "gate",
|
|
121
|
+
[{ name: "Rollback", steps: [{ kind: "gate", gate: "approve-rollback" }] }],
|
|
122
122
|
),
|
|
123
123
|
),
|
|
124
124
|
).toBe(true);
|
package/src/op/op.test.ts
CHANGED
|
@@ -131,10 +131,10 @@ describe("activity()", () => {
|
|
|
131
131
|
// ── gate() ────────────────────────────────────────────────────────────────────
|
|
132
132
|
|
|
133
133
|
describe("gate()", () => {
|
|
134
|
-
it("returns GateStep with kind 'gate' and
|
|
134
|
+
it("returns GateStep with kind 'gate' and the gate name", () => {
|
|
135
135
|
const g = gate("dns-delegation");
|
|
136
136
|
expect(g.kind).toBe("gate");
|
|
137
|
-
expect(g.
|
|
137
|
+
expect(g.gate).toBe("dns-delegation");
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
it("includes timeout when provided", () => {
|