@intentius/chant 0.61.0 → 0.63.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/cli/handlers/operator.d.ts +13 -18
- 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.map +1 -1
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +3 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/driver-output.d.ts.map +1 -1
- package/dist/components/driver.d.ts +12 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +10 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon.d.ts +51 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +61 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +117 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/plan-digest.d.ts +33 -0
- package/dist/lifecycle/plan-digest.d.ts.map +1 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/op/activities/lexicon-upgrade.d.ts +33 -3
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activities/lifecycle.d.ts +27 -0
- package/dist/op/activities/lifecycle.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +394 -14
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/builders.d.ts +6 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/apply-op.d.ts +6 -0
- 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-summary.d.ts +16 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +104 -13
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +3 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +34 -2
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +8 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/types.d.ts +19 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +8 -0
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/terraform/__fixtures__/build-graph.d.ts +8 -0
- package/dist/terraform/__fixtures__/build-graph.d.ts.map +1 -1
- package/dist/terraform/graph.d.ts +18 -2
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +7 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/operator.test.ts +232 -1
- package/src/cli/handlers/operator.ts +130 -6
- package/src/cli/handlers/run.ts +19 -0
- package/src/cli/main.ts +2 -0
- package/src/cli/registry.ts +2 -0
- package/src/components/cli-support.ts +29 -4
- package/src/components/driver-output.ts +10 -0
- package/src/components/driver.test.ts +31 -0
- package/src/components/driver.ts +54 -8
- package/src/discovery/fold-import.test.ts +55 -0
- package/src/fold/fold.test.ts +152 -0
- package/src/fold/fold.ts +102 -2
- package/src/fold/subset-doc-parity.test.ts +35 -1
- package/src/fold/subset.ts +10 -0
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/gate-ledger.test.ts +133 -1
- package/src/lifecycle/gate-ledger.ts +108 -0
- package/src/lifecycle/git.test.ts +49 -5
- package/src/lifecycle/git.ts +312 -11
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/plan-digest.test.ts +49 -0
- package/src/lifecycle/plan-digest.ts +86 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/op/activities/lexicon-upgrade.test.ts +134 -39
- package/src/op/activities/lexicon-upgrade.ts +74 -12
- package/src/op/activities/lifecycle.ts +51 -2
- package/src/op/activities/reconcile.test.ts +1013 -1
- package/src/op/activities/reconcile.ts +721 -25
- package/src/op/builders.ts +7 -1
- package/src/op/composites/apply-op.ts +16 -0
- package/src/op/composites/composites.test.ts +15 -2
- package/src/op/composites/reconcile-op.test.ts +18 -0
- package/src/op/composites/reconcile-op.ts +7 -1
- package/src/op/gate-summary.test.ts +33 -0
- package/src/op/gate-summary.ts +31 -0
- package/src/op/gate.test.ts +614 -0
- package/src/op/gate.ts +190 -24
- package/src/op/index.ts +5 -2
- package/src/op/local-executor.test.ts +340 -2
- package/src/op/local-executor.ts +190 -32
- package/src/op/local-output.test.ts +38 -0
- package/src/op/local-output.ts +24 -1
- package/src/op/op-ir.test.ts +22 -0
- package/src/op/op-ir.ts +9 -0
- package/src/op/operator.test.ts +20 -0
- package/src/op/operator.ts +39 -1
- package/src/op/runtime.ts +2 -0
- package/src/op/runtimes/local.ts +11 -0
- package/src/op/types.ts +19 -0
- package/src/runtime-adapter.ts +17 -3
- package/src/terraform/__fixtures__/build-graph.ts +42 -0
- package/src/terraform/__fixtures__/carve-locals-data.test.ts +138 -0
- package/src/terraform/__fixtures__/depth-estate/main.tf +141 -0
- package/src/terraform/__fixtures__/depth-estate/terraform.tfstate +17 -0
- package/src/terraform/__fixtures__/depth-estate.test.ts +162 -0
- package/src/terraform/graph.test.ts +148 -1
- package/src/terraform/graph.ts +144 -6
- package/src/terraform/parse.ts +4 -1
- package/src/terraform/types.ts +7 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
buildUpgradeSummary,
|
|
10
10
|
computeBumpedVersion,
|
|
11
11
|
bumpPackageJsonVersion,
|
|
12
|
+
lexiconUpgradeIssueMarker,
|
|
12
13
|
type CheckPinnedFn,
|
|
13
14
|
type CheckRollingFn,
|
|
14
15
|
type BumpPackageVersionFn,
|
|
@@ -223,21 +224,105 @@ describe("lexiconUpgrade report mode", () => {
|
|
|
223
224
|
|
|
224
225
|
// ── issue mode ────────────────────────────────────────────────────────
|
|
225
226
|
|
|
227
|
+
describe("lexiconUpgradeIssueMarker (#2297)", () => {
|
|
228
|
+
test("deterministic per lexicon, and distinct across lexicons", () => {
|
|
229
|
+
expect(lexiconUpgradeIssueMarker("k8s")).toBe("<!-- chant-lexicon-upgrade:k8s -->");
|
|
230
|
+
expect(lexiconUpgradeIssueMarker("k8s")).toBe(lexiconUpgradeIssueMarker("k8s"));
|
|
231
|
+
expect(lexiconUpgradeIssueMarker("k8s")).not.toBe(lexiconUpgradeIssueMarker("aws"));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
226
235
|
describe("lexiconUpgrade issue mode", () => {
|
|
227
|
-
test("
|
|
236
|
+
test("outside any known CI job, falls back to gh's own ambient gh issue create, marker-prefixed", async () => {
|
|
228
237
|
const checkPinned: CheckPinnedFn = vi.fn(async () => pinnedResult());
|
|
229
238
|
const { gh, calls } = recordingGh();
|
|
230
239
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
vi.stubEnv("GITHUB_REPOSITORY", "");
|
|
241
|
+
try {
|
|
242
|
+
const r = await lexiconUpgrade({
|
|
243
|
+
lexicon: "gcp",
|
|
244
|
+
mode: "issue",
|
|
245
|
+
_checkPinned: checkPinned,
|
|
246
|
+
_gh: gh,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(r.issueUrl).toBe("https://gh/issue/1");
|
|
250
|
+
expect(calls.some((c) => c.includes("gh issue create"))).toBe(true);
|
|
251
|
+
expect(calls.some((c) => c.includes("gh pr create"))).toBe(false);
|
|
252
|
+
expect(calls.some((c) => c.includes("<!-- chant-lexicon-upgrade:gcp -->"))).toBe(true);
|
|
253
|
+
} finally {
|
|
254
|
+
vi.unstubAllEnvs();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("on a GitHub Actions job (GITHUB_REPOSITORY set), opens the sticky issue via gh api (#2297)", async () => {
|
|
259
|
+
const checkPinned: CheckPinnedFn = vi.fn(async () => pinnedResult());
|
|
260
|
+
const calls: Array<{ cmd: string; env?: NodeJS.ProcessEnv }> = [];
|
|
261
|
+
const gh = vi.fn(async (cmd: string, opts?: { env?: NodeJS.ProcessEnv }) => {
|
|
262
|
+
calls.push({ cmd, env: opts?.env });
|
|
263
|
+
if (cmd.includes("--paginate")) return { stdout: "", stderr: "" }; // no owned issue yet
|
|
264
|
+
if (cmd.includes("--method POST")) return { stdout: "https://github.com/acme/infra/issues/11\n", stderr: "" };
|
|
265
|
+
return { stdout: "", stderr: "" };
|
|
236
266
|
});
|
|
237
267
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
268
|
+
vi.stubEnv("GITHUB_REPOSITORY", "acme/infra");
|
|
269
|
+
vi.stubEnv("GITHUB_API_URL", "");
|
|
270
|
+
vi.stubEnv("CHANT_FORGEJO_TOKEN", "");
|
|
271
|
+
vi.stubEnv("GH_TOKEN", "ghs-workflow");
|
|
272
|
+
try {
|
|
273
|
+
const r = await lexiconUpgrade({
|
|
274
|
+
lexicon: "gcp",
|
|
275
|
+
mode: "issue",
|
|
276
|
+
_checkPinned: checkPinned,
|
|
277
|
+
_gh: gh,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(r.issueUrl).toBe("https://github.com/acme/infra/issues/11");
|
|
281
|
+
expect(calls.some((c) => c.cmd.includes("gh issue create"))).toBe(false);
|
|
282
|
+
const post = calls.find((c) => c.cmd.includes("--method POST"));
|
|
283
|
+
expect(post?.cmd).toContain("https://api.github.com/repos/acme/infra/issues");
|
|
284
|
+
expect(post?.cmd).toContain("<!-- chant-lexicon-upgrade:gcp -->");
|
|
285
|
+
// #2320: the shared `postOrUpdateGithubIssue` resolves the credential
|
|
286
|
+
// and hands it to the runner, so this caller gets the same forwarding
|
|
287
|
+
// reconcilePr's issue mode does rather than leaving `gh` to guess.
|
|
288
|
+
for (const call of calls) expect(call.env?.GH_TOKEN).toBe("ghs-workflow");
|
|
289
|
+
} finally {
|
|
290
|
+
vi.unstubAllEnvs();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a re-run on a GitHub Actions job PATCHes the issue it already owns (#2297)", async () => {
|
|
295
|
+
const checkPinned: CheckPinnedFn = vi.fn(async () => pinnedResult());
|
|
296
|
+
const calls: Array<{ cmd: string; env?: NodeJS.ProcessEnv }> = [];
|
|
297
|
+
const gh = vi.fn(async (cmd: string, opts?: { env?: NodeJS.ProcessEnv }) => {
|
|
298
|
+
calls.push({ cmd, env: opts?.env });
|
|
299
|
+
if (cmd.includes("--paginate")) return { stdout: "11\n", stderr: "" }; // marker search found issue 11
|
|
300
|
+
if (cmd.includes("--method PATCH")) return { stdout: "https://github.com/acme/infra/issues/11\n", stderr: "" };
|
|
301
|
+
return { stdout: "", stderr: "" };
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
vi.stubEnv("GITHUB_REPOSITORY", "acme/infra");
|
|
305
|
+
vi.stubEnv("GITHUB_API_URL", "");
|
|
306
|
+
// The cross-instance case (#2320): a Forgejo token that must outrank the
|
|
307
|
+
// job's own `github.token`, on the path that used to forward neither.
|
|
308
|
+
vi.stubEnv("CHANT_FORGEJO_TOKEN", "forgejo-cross-instance");
|
|
309
|
+
vi.stubEnv("GH_TOKEN", "ghs-this-instance-only");
|
|
310
|
+
try {
|
|
311
|
+
const r = await lexiconUpgrade({
|
|
312
|
+
lexicon: "gcp",
|
|
313
|
+
mode: "issue",
|
|
314
|
+
_checkPinned: checkPinned,
|
|
315
|
+
_gh: gh,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(r.issueUrl).toBe("https://github.com/acme/infra/issues/11");
|
|
319
|
+
expect(calls.some((c) => c.cmd.includes("--method POST"))).toBe(false);
|
|
320
|
+
expect(calls.some((c) => c.cmd.includes("gh issue create"))).toBe(false);
|
|
321
|
+
const patch = calls.find((c) => c.cmd.includes("--method PATCH"));
|
|
322
|
+
expect(patch?.env?.GH_TOKEN).toBe("forgejo-cross-instance");
|
|
323
|
+
} finally {
|
|
324
|
+
vi.unstubAllEnvs();
|
|
325
|
+
}
|
|
241
326
|
});
|
|
242
327
|
});
|
|
243
328
|
|
|
@@ -260,21 +345,26 @@ describe("lexiconUpgrade breakage handling", () => {
|
|
|
260
345
|
);
|
|
261
346
|
const { gh, calls } = recordingGh();
|
|
262
347
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
348
|
+
vi.stubEnv("GITHUB_REPOSITORY", "");
|
|
349
|
+
try {
|
|
350
|
+
const r = await lexiconUpgrade({
|
|
351
|
+
lexicon: "k8s",
|
|
352
|
+
mode: "pull-request",
|
|
353
|
+
_checkPinned: checkPinned,
|
|
354
|
+
_gh: gh,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(r.validationOk).toBe(false);
|
|
358
|
+
expect(r.issueUrl).toBe("https://gh/issue/1");
|
|
359
|
+
expect(r.prUrl).toBeUndefined();
|
|
360
|
+
// must NOT open a PR, must NOT push
|
|
361
|
+
expect(calls.some((c) => c.includes("gh pr create"))).toBe(false);
|
|
362
|
+
expect(calls.some((c) => c.includes("gh issue create"))).toBe(true);
|
|
363
|
+
// the issue body carries the failure diff
|
|
364
|
+
expect(r.summary).toContain("type error");
|
|
365
|
+
} finally {
|
|
366
|
+
vi.unstubAllEnvs();
|
|
367
|
+
}
|
|
278
368
|
});
|
|
279
369
|
|
|
280
370
|
test("upstream fetch error surfaces as a non-upgrade report, never a PR", async () => {
|
|
@@ -473,20 +563,25 @@ describe("lexiconUpgrade pull-request mode: package.json version bump", () => {
|
|
|
473
563
|
return { stdout: "", stderr: "" };
|
|
474
564
|
});
|
|
475
565
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
566
|
+
vi.stubEnv("GITHUB_REPOSITORY", "");
|
|
567
|
+
try {
|
|
568
|
+
const r = await lexiconUpgrade({
|
|
569
|
+
lexicon: "aws",
|
|
570
|
+
mode: "pull-request",
|
|
571
|
+
_checkRolling: checkRolling,
|
|
572
|
+
_bumpPackageVersion: bumpFn,
|
|
573
|
+
_gh: gh,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Rolling drift → issue, never a baseline-only PR, a branch push, or a bump.
|
|
577
|
+
expect(r.mode).toBe("issue");
|
|
578
|
+
expect(r.issueUrl).toBe("https://gh/issue/9");
|
|
579
|
+
expect(calls.some((c) => c.includes("gh issue create"))).toBe(true);
|
|
580
|
+
expect(calls.some((c) => c.includes("gh pr create"))).toBe(false);
|
|
581
|
+
expect(calls.some((c) => c.startsWith("git "))).toBe(false);
|
|
582
|
+
expect(bumpFn).not.toHaveBeenCalled();
|
|
583
|
+
} finally {
|
|
584
|
+
vi.unstubAllEnvs();
|
|
585
|
+
}
|
|
491
586
|
});
|
|
492
587
|
});
|
|
@@ -28,6 +28,7 @@ import { join } from "node:path";
|
|
|
28
28
|
import type { UpgradeCheckResult, LexiconId } from "../../codegen/pinned-upgrade";
|
|
29
29
|
import type { RollingUpgradeResult, RollingLexicon } from "../../codegen/rolling-upgrade";
|
|
30
30
|
import { bumpForSeverity, bumpPackageJsonVersion } from "../../codegen/version-bump";
|
|
31
|
+
import { postOrUpdateGithubIssue } from "./reconcile";
|
|
31
32
|
|
|
32
33
|
export { bumpPackageJsonVersion };
|
|
33
34
|
|
|
@@ -94,7 +95,7 @@ export interface LexiconUpgradeResult {
|
|
|
94
95
|
semverLabel: SemverLabel | null;
|
|
95
96
|
/** URL of the opened/updated PR (pull-request mode). */
|
|
96
97
|
prUrl?: string;
|
|
97
|
-
/** URL of the opened issue (issue mode). */
|
|
98
|
+
/** URL of the opened or edited issue (issue mode; sticky per lexicon, #2297). */
|
|
98
99
|
issueUrl?: string;
|
|
99
100
|
/** Full markdown summary, used as PR/issue body or printed in report mode. */
|
|
100
101
|
summary: string;
|
|
@@ -119,8 +120,21 @@ export type CheckRollingFn = (opts: {
|
|
|
119
120
|
verbose?: boolean;
|
|
120
121
|
}) => Promise<RollingUpgradeResult>;
|
|
121
122
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Minimal shell-exec interface for gh/git invocations.
|
|
125
|
+
*
|
|
126
|
+
* `opts` carries the environment `postOrUpdateGithubIssue` resolves the issue
|
|
127
|
+
* credential into (chant #2320) — the same `GhExec` shape reconcile.ts
|
|
128
|
+
* declares, so the two stay one signature and this can keep being passed
|
|
129
|
+
* straight through. Every other call site here forwards nothing and passes
|
|
130
|
+
* nothing. A mock runner is free to ignore the argument; the default one
|
|
131
|
+
* hands it to `execAsync`, which is what makes CHANT_FORGEJO_TOKEN actually
|
|
132
|
+
* reach `gh` on a cross-instance Forgejo run.
|
|
133
|
+
*/
|
|
134
|
+
export type GhRunner = (
|
|
135
|
+
cmd: string,
|
|
136
|
+
opts?: { env?: NodeJS.ProcessEnv },
|
|
137
|
+
) => Promise<{ stdout: string; stderr: string }>;
|
|
124
138
|
|
|
125
139
|
/** Applies a pinned version bump permanently (no revert). Async (core loads the lexicon's pin descriptor); a sync mock is also accepted. */
|
|
126
140
|
export type ApplyBumpFn = (
|
|
@@ -282,7 +296,53 @@ function shellQuote(s: string): string {
|
|
|
282
296
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
283
297
|
}
|
|
284
298
|
|
|
285
|
-
const defaultGh: GhRunner = async (cmd) => execAsync(cmd);
|
|
299
|
+
const defaultGh: GhRunner = async (cmd, opts) => execAsync(cmd, { ...opts });
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The hidden marker that makes this lexicon's upgrade-status issue findable
|
|
303
|
+
* across re-runs (#2297): written as the issue body's first line, matched by
|
|
304
|
+
* `startswith` — the same recipe reconcile.ts's `issueMarker` names for a
|
|
305
|
+
* reconcile finding, kept as its own function because the two activities own
|
|
306
|
+
* different issues. Keyed on `lexicon`, not on the validation outcome: a
|
|
307
|
+
* lexicon has one upgrade-status issue that flips between "validation
|
|
308
|
+
* failed" and "upgrade ready" as reality changes, rather than a second issue
|
|
309
|
+
* for the other outcome. `lexicon` is a closed set of known identifiers
|
|
310
|
+
* (see `SupportedLexicon`), so unlike `issueMarker` this needs no slugifying.
|
|
311
|
+
*/
|
|
312
|
+
export function lexiconUpgradeIssueMarker(lexicon: SupportedLexicon): string {
|
|
313
|
+
return `<!-- chant-lexicon-upgrade:${lexicon} -->`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Open or update this lexicon's upgrade-status issue (#2297). On a GitHub
|
|
318
|
+
* Actions or Forgejo Actions job (`GITHUB_REPOSITORY` set) this is
|
|
319
|
+
* `postOrUpdateGithubIssue`'s sticky recipe from reconcile.ts: find the OPEN
|
|
320
|
+
* issue already carrying this lexicon's marker, edit it, open one only when
|
|
321
|
+
* there is none — see that function's own doc comment for the search
|
|
322
|
+
* mechanism and the close decision (never closed here either). Outside any
|
|
323
|
+
* known CI job, falls back to `gh`'s own ambient `gh issue create`, still
|
|
324
|
+
* marker-prefixed so a later CI run finds and edits it instead of opening a
|
|
325
|
+
* second one.
|
|
326
|
+
*/
|
|
327
|
+
async function postLexiconIssue(
|
|
328
|
+
lexicon: SupportedLexicon,
|
|
329
|
+
title: string,
|
|
330
|
+
body: string,
|
|
331
|
+
gh: GhRunner,
|
|
332
|
+
): Promise<string> {
|
|
333
|
+
const marker = lexiconUpgradeIssueMarker(lexicon);
|
|
334
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
335
|
+
if (repo) {
|
|
336
|
+
// `postOrUpdateGithubIssue` resolves the token itself and hands it back
|
|
337
|
+
// through `gh`'s second argument (#2320), so this runner has to forward
|
|
338
|
+
// what it is given rather than dropping it — see `GhRunner`.
|
|
339
|
+
return postOrUpdateGithubIssue(repo, marker, title, body, gh);
|
|
340
|
+
}
|
|
341
|
+
const { stdout } = await gh(
|
|
342
|
+
`gh issue create --title ${shellQuote(title)} --body ${shellQuote(`${marker}\n\n${body}`)}`,
|
|
343
|
+
);
|
|
344
|
+
return stdout.trim();
|
|
345
|
+
}
|
|
286
346
|
|
|
287
347
|
// ── PR idempotency helpers ────────────────────────────────────────────
|
|
288
348
|
|
|
@@ -384,6 +444,12 @@ async function getRealApplyBump(): Promise<ApplyBumpFn> {
|
|
|
384
444
|
* - Semver label: "minor" (additive) or "breaking" (changed/removed).
|
|
385
445
|
* - Validation failure falls back to issue mode.
|
|
386
446
|
*
|
|
447
|
+
* In issue mode (#2297): one OPEN issue per lexicon, found by a hidden
|
|
448
|
+
* marker and edited in place on every re-run — the same sticky recipe
|
|
449
|
+
* reconcile.ts's `issue` mode uses, via the shared `postOrUpdateGithubIssue`.
|
|
450
|
+
* Never closed by this activity; see that function's doc comment for the
|
|
451
|
+
* close decision, which applies here unchanged.
|
|
452
|
+
*
|
|
387
453
|
* Never auto-merges or auto-publishes.
|
|
388
454
|
*/
|
|
389
455
|
export async function lexiconUpgrade(args: LexiconUpgradeArgs): Promise<LexiconUpgradeResult> {
|
|
@@ -493,16 +559,14 @@ export async function lexiconUpgrade(args: LexiconUpgradeArgs): Promise<LexiconU
|
|
|
493
559
|
const effectiveMode: LexiconUpgradeMode = mode === "pull-request" ? "issue" : mode;
|
|
494
560
|
if (effectiveMode === "issue") {
|
|
495
561
|
const title = `chore(${lexicon}): spec upgrade validation failed`;
|
|
496
|
-
const
|
|
497
|
-
`gh issue create --title ${shellQuote(title)} --body ${shellQuote(summary)}`,
|
|
498
|
-
);
|
|
562
|
+
const issueUrl = await postLexiconIssue(lexicon, title, summary, gh);
|
|
499
563
|
return {
|
|
500
564
|
lexicon,
|
|
501
565
|
mode,
|
|
502
566
|
hasUpgrade: true,
|
|
503
567
|
deltaText,
|
|
504
568
|
semverLabel,
|
|
505
|
-
issueUrl
|
|
569
|
+
issueUrl,
|
|
506
570
|
summary,
|
|
507
571
|
validationOk: false,
|
|
508
572
|
};
|
|
@@ -543,16 +607,14 @@ export async function lexiconUpgrade(args: LexiconUpgradeArgs): Promise<LexiconU
|
|
|
543
607
|
}
|
|
544
608
|
|
|
545
609
|
if (effectiveMode === "issue") {
|
|
546
|
-
const
|
|
547
|
-
`gh issue create --title ${shellQuote(title)} --body ${shellQuote(summary)}`,
|
|
548
|
-
);
|
|
610
|
+
const issueUrl = await postLexiconIssue(lexicon, title, summary, gh);
|
|
549
611
|
return {
|
|
550
612
|
lexicon,
|
|
551
613
|
mode: effectiveMode,
|
|
552
614
|
hasUpgrade: true,
|
|
553
615
|
deltaText,
|
|
554
616
|
semverLabel,
|
|
555
|
-
issueUrl
|
|
617
|
+
issueUrl,
|
|
556
618
|
summary,
|
|
557
619
|
validationOk: true,
|
|
558
620
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { computePlanDigest } from "../../lifecycle/plan-digest";
|
|
3
4
|
|
|
4
5
|
const execAsync = promisify(exec);
|
|
5
6
|
|
|
@@ -39,6 +40,40 @@ export interface LifecycleDiffResult {
|
|
|
39
40
|
* `chant lifecycle diff --live`).
|
|
40
41
|
*/
|
|
41
42
|
drifted: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* This diff's identity (#2300) — the change set `chant lifecycle diff`
|
|
45
|
+
* reported, hashed. `ApplyOp` hands it to its gate step's `plan`, so an
|
|
46
|
+
* approval binds to the change set the approver read rather than to the
|
|
47
|
+
* next run of the Op.
|
|
48
|
+
*
|
|
49
|
+
* Computed over {@link normalizeDiffChangeSet}'s canonical form of
|
|
50
|
+
* `output`, so incidental whitespace does not read as a changed plan, while
|
|
51
|
+
* any added, removed or reworded row does.
|
|
52
|
+
*/
|
|
53
|
+
planDigest: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The change set half of a `chant lifecycle diff` render, canonicalised for
|
|
58
|
+
* digesting (#2300).
|
|
59
|
+
*
|
|
60
|
+
* The diff render is line-oriented: section headers and the resource rows
|
|
61
|
+
* under them. Two runs over an unchanged environment print the same lines, so
|
|
62
|
+
* the lines are the change set. Normalising is deliberately minimal — CRLF to
|
|
63
|
+
* LF, trailing whitespace off each line, blank lines dropped — because
|
|
64
|
+
* anything more aggressive would start discarding rows, and a digest that
|
|
65
|
+
* discards rows is a digest that approves changes nobody saw.
|
|
66
|
+
*
|
|
67
|
+
* Unlike a terraform plan there is no timestamp to strip: the diff render
|
|
68
|
+
* carries none. If one is ever added it has to be dropped here, or every
|
|
69
|
+
* approval would be stale the moment it was written.
|
|
70
|
+
*/
|
|
71
|
+
export function normalizeDiffChangeSet(output: string): string[] {
|
|
72
|
+
return output
|
|
73
|
+
.replace(/\r\n/g, "\n")
|
|
74
|
+
.split("\n")
|
|
75
|
+
.map((line) => line.replace(/\s+$/, ""))
|
|
76
|
+
.filter((line) => line !== "");
|
|
42
77
|
}
|
|
43
78
|
|
|
44
79
|
/**
|
|
@@ -59,6 +94,20 @@ function detectDrift(output: string): boolean {
|
|
|
59
94
|
return DRIFT_HEADERS.some((h) => output.includes(`${h} (`) || output.includes(`\n${h}`));
|
|
60
95
|
}
|
|
61
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The identity of one `chant lifecycle diff` result (#2300). The environment
|
|
99
|
+
* and the `--live` flag are hashed alongside the change set because they say
|
|
100
|
+
* what the change set is *of*: the same rows against `staging` are not an
|
|
101
|
+
* approval to apply against `prod`.
|
|
102
|
+
*/
|
|
103
|
+
function lifecycleDiffDigest(args: LifecycleDiffArgs, output: string): string {
|
|
104
|
+
return computePlanDigest("lifecycle-diff", {
|
|
105
|
+
env: args.env,
|
|
106
|
+
live: args.live === true,
|
|
107
|
+
changeSet: normalizeDiffChangeSet(output),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
62
111
|
/**
|
|
63
112
|
* Run `chant lifecycle diff <env>` and return the output + structured drift
|
|
64
113
|
* flag. Read-only; intended for use inside watch/observation Ops.
|
|
@@ -75,11 +124,11 @@ export async function lifecycleDiff(args: LifecycleDiffArgs, signal?: AbortSigna
|
|
|
75
124
|
const { stdout, stderr } = await execAsync(`chant lifecycle diff ${args.env}${liveFlag}`, { signal });
|
|
76
125
|
const output = `${stdout}${stderr}`.trim();
|
|
77
126
|
if (output) console.log(output);
|
|
78
|
-
return { output, exitCode: 0, drifted: detectDrift(output) };
|
|
127
|
+
return { output, exitCode: 0, drifted: detectDrift(output), planDigest: lifecycleDiffDigest(args, output) };
|
|
79
128
|
} catch (err) {
|
|
80
129
|
const e = err as { code?: number; stdout?: string; stderr?: string };
|
|
81
130
|
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`.trim();
|
|
82
131
|
if (output) console.error(output);
|
|
83
|
-
return { output, exitCode: e.code ?? 1, drifted: detectDrift(output) };
|
|
132
|
+
return { output, exitCode: e.code ?? 1, drifted: detectDrift(output), planDigest: lifecycleDiffDigest(args, output) };
|
|
84
133
|
}
|
|
85
134
|
}
|