@intentius/chant 0.60.0 → 0.62.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/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +0 -18
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/codegen/json-schema.d.ts +5 -2
- package/dist/codegen/json-schema.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +70 -2
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon.d.ts +51 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.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/observe.d.ts.map +1 -1
- package/dist/observation.d.ts +21 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/activities/lexicon-upgrade.d.ts +19 -1
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +225 -14
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +17 -2
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +8 -0
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.ts +1 -1
- package/src/cli/handlers/lifecycle.ts +1 -0
- package/src/cli/handlers/operator.test.ts +102 -1
- package/src/cli/handlers/operator.ts +75 -5
- package/src/codegen/json-schema.test.ts +159 -0
- package/src/codegen/json-schema.ts +106 -8
- package/src/graph-ir.test.ts +63 -0
- package/src/graph-ir.ts +125 -8
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/assert-live.ts +1 -0
- package/src/lifecycle/git.test.ts +49 -5
- package/src/lifecycle/git.ts +312 -11
- package/src/lifecycle/observe.ts +3 -0
- package/src/observation.test.ts +21 -9
- package/src/observation.ts +31 -2
- package/src/op/activities/lexicon-upgrade.test.ts +122 -39
- package/src/op/activities/lexicon-upgrade.ts +55 -9
- package/src/op/activities/reconcile.test.ts +527 -1
- package/src/op/activities/reconcile.ts +446 -23
- package/src/op/gate.test.ts +504 -0
- package/src/op/gate.ts +9 -1
- package/src/op/local-executor.test.ts +115 -0
- package/src/op/local-executor.ts +130 -21
- package/src/op/operator.test.ts +20 -0
- package/src/op/operator.ts +39 -1
- package/src/op/runtimes/local.ts +11 -0
- package/src/runtime-adapter.ts +17 -3
|
@@ -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,93 @@ 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: string[] = [];
|
|
261
|
+
const gh = vi.fn(async (cmd: string) => {
|
|
262
|
+
calls.push(cmd);
|
|
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
|
+
try {
|
|
271
|
+
const r = await lexiconUpgrade({
|
|
272
|
+
lexicon: "gcp",
|
|
273
|
+
mode: "issue",
|
|
274
|
+
_checkPinned: checkPinned,
|
|
275
|
+
_gh: gh,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(r.issueUrl).toBe("https://github.com/acme/infra/issues/11");
|
|
279
|
+
expect(calls.some((c) => c.includes("gh issue create"))).toBe(false);
|
|
280
|
+
const post = calls.find((c) => c.includes("--method POST"));
|
|
281
|
+
expect(post).toContain("https://api.github.com/repos/acme/infra/issues");
|
|
282
|
+
expect(post).toContain("<!-- chant-lexicon-upgrade:gcp -->");
|
|
283
|
+
} finally {
|
|
284
|
+
vi.unstubAllEnvs();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("a re-run on a GitHub Actions job PATCHes the issue it already owns (#2297)", async () => {
|
|
289
|
+
const checkPinned: CheckPinnedFn = vi.fn(async () => pinnedResult());
|
|
290
|
+
const calls: string[] = [];
|
|
291
|
+
const gh = vi.fn(async (cmd: string) => {
|
|
292
|
+
calls.push(cmd);
|
|
293
|
+
if (cmd.includes("--paginate")) return { stdout: "11\n", stderr: "" }; // marker search found issue 11
|
|
294
|
+
if (cmd.includes("--method PATCH")) return { stdout: "https://github.com/acme/infra/issues/11\n", stderr: "" };
|
|
295
|
+
return { stdout: "", stderr: "" };
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
vi.stubEnv("GITHUB_REPOSITORY", "acme/infra");
|
|
299
|
+
vi.stubEnv("GITHUB_API_URL", "");
|
|
300
|
+
try {
|
|
301
|
+
const r = await lexiconUpgrade({
|
|
302
|
+
lexicon: "gcp",
|
|
303
|
+
mode: "issue",
|
|
304
|
+
_checkPinned: checkPinned,
|
|
305
|
+
_gh: gh,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(r.issueUrl).toBe("https://github.com/acme/infra/issues/11");
|
|
309
|
+
expect(calls.some((c) => c.includes("--method POST"))).toBe(false);
|
|
310
|
+
expect(calls.some((c) => c.includes("gh issue create"))).toBe(false);
|
|
311
|
+
} finally {
|
|
312
|
+
vi.unstubAllEnvs();
|
|
313
|
+
}
|
|
241
314
|
});
|
|
242
315
|
});
|
|
243
316
|
|
|
@@ -260,21 +333,26 @@ describe("lexiconUpgrade breakage handling", () => {
|
|
|
260
333
|
);
|
|
261
334
|
const { gh, calls } = recordingGh();
|
|
262
335
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
336
|
+
vi.stubEnv("GITHUB_REPOSITORY", "");
|
|
337
|
+
try {
|
|
338
|
+
const r = await lexiconUpgrade({
|
|
339
|
+
lexicon: "k8s",
|
|
340
|
+
mode: "pull-request",
|
|
341
|
+
_checkPinned: checkPinned,
|
|
342
|
+
_gh: gh,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(r.validationOk).toBe(false);
|
|
346
|
+
expect(r.issueUrl).toBe("https://gh/issue/1");
|
|
347
|
+
expect(r.prUrl).toBeUndefined();
|
|
348
|
+
// must NOT open a PR, must NOT push
|
|
349
|
+
expect(calls.some((c) => c.includes("gh pr create"))).toBe(false);
|
|
350
|
+
expect(calls.some((c) => c.includes("gh issue create"))).toBe(true);
|
|
351
|
+
// the issue body carries the failure diff
|
|
352
|
+
expect(r.summary).toContain("type error");
|
|
353
|
+
} finally {
|
|
354
|
+
vi.unstubAllEnvs();
|
|
355
|
+
}
|
|
278
356
|
});
|
|
279
357
|
|
|
280
358
|
test("upstream fetch error surfaces as a non-upgrade report, never a PR", async () => {
|
|
@@ -473,20 +551,25 @@ describe("lexiconUpgrade pull-request mode: package.json version bump", () => {
|
|
|
473
551
|
return { stdout: "", stderr: "" };
|
|
474
552
|
});
|
|
475
553
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
554
|
+
vi.stubEnv("GITHUB_REPOSITORY", "");
|
|
555
|
+
try {
|
|
556
|
+
const r = await lexiconUpgrade({
|
|
557
|
+
lexicon: "aws",
|
|
558
|
+
mode: "pull-request",
|
|
559
|
+
_checkRolling: checkRolling,
|
|
560
|
+
_bumpPackageVersion: bumpFn,
|
|
561
|
+
_gh: gh,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Rolling drift → issue, never a baseline-only PR, a branch push, or a bump.
|
|
565
|
+
expect(r.mode).toBe("issue");
|
|
566
|
+
expect(r.issueUrl).toBe("https://gh/issue/9");
|
|
567
|
+
expect(calls.some((c) => c.includes("gh issue create"))).toBe(true);
|
|
568
|
+
expect(calls.some((c) => c.includes("gh pr create"))).toBe(false);
|
|
569
|
+
expect(calls.some((c) => c.startsWith("git "))).toBe(false);
|
|
570
|
+
expect(bumpFn).not.toHaveBeenCalled();
|
|
571
|
+
} finally {
|
|
572
|
+
vi.unstubAllEnvs();
|
|
573
|
+
}
|
|
491
574
|
});
|
|
492
575
|
});
|
|
@@ -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;
|
|
@@ -284,6 +285,49 @@ function shellQuote(s: string): string {
|
|
|
284
285
|
|
|
285
286
|
const defaultGh: GhRunner = async (cmd) => execAsync(cmd);
|
|
286
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The hidden marker that makes this lexicon's upgrade-status issue findable
|
|
290
|
+
* across re-runs (#2297): written as the issue body's first line, matched by
|
|
291
|
+
* `startswith` — the same recipe reconcile.ts's `issueMarker` names for a
|
|
292
|
+
* reconcile finding, kept as its own function because the two activities own
|
|
293
|
+
* different issues. Keyed on `lexicon`, not on the validation outcome: a
|
|
294
|
+
* lexicon has one upgrade-status issue that flips between "validation
|
|
295
|
+
* failed" and "upgrade ready" as reality changes, rather than a second issue
|
|
296
|
+
* for the other outcome. `lexicon` is a closed set of known identifiers
|
|
297
|
+
* (see `SupportedLexicon`), so unlike `issueMarker` this needs no slugifying.
|
|
298
|
+
*/
|
|
299
|
+
export function lexiconUpgradeIssueMarker(lexicon: SupportedLexicon): string {
|
|
300
|
+
return `<!-- chant-lexicon-upgrade:${lexicon} -->`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Open or update this lexicon's upgrade-status issue (#2297). On a GitHub
|
|
305
|
+
* Actions or Forgejo Actions job (`GITHUB_REPOSITORY` set) this is
|
|
306
|
+
* `postOrUpdateGithubIssue`'s sticky recipe from reconcile.ts: find the OPEN
|
|
307
|
+
* issue already carrying this lexicon's marker, edit it, open one only when
|
|
308
|
+
* there is none — see that function's own doc comment for the search
|
|
309
|
+
* mechanism and the close decision (never closed here either). Outside any
|
|
310
|
+
* known CI job, falls back to `gh`'s own ambient `gh issue create`, still
|
|
311
|
+
* marker-prefixed so a later CI run finds and edits it instead of opening a
|
|
312
|
+
* second one.
|
|
313
|
+
*/
|
|
314
|
+
async function postLexiconIssue(
|
|
315
|
+
lexicon: SupportedLexicon,
|
|
316
|
+
title: string,
|
|
317
|
+
body: string,
|
|
318
|
+
gh: GhRunner,
|
|
319
|
+
): Promise<string> {
|
|
320
|
+
const marker = lexiconUpgradeIssueMarker(lexicon);
|
|
321
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
322
|
+
if (repo) {
|
|
323
|
+
return postOrUpdateGithubIssue(repo, marker, title, body, gh);
|
|
324
|
+
}
|
|
325
|
+
const { stdout } = await gh(
|
|
326
|
+
`gh issue create --title ${shellQuote(title)} --body ${shellQuote(`${marker}\n\n${body}`)}`,
|
|
327
|
+
);
|
|
328
|
+
return stdout.trim();
|
|
329
|
+
}
|
|
330
|
+
|
|
287
331
|
// ── PR idempotency helpers ────────────────────────────────────────────
|
|
288
332
|
|
|
289
333
|
/**
|
|
@@ -384,6 +428,12 @@ async function getRealApplyBump(): Promise<ApplyBumpFn> {
|
|
|
384
428
|
* - Semver label: "minor" (additive) or "breaking" (changed/removed).
|
|
385
429
|
* - Validation failure falls back to issue mode.
|
|
386
430
|
*
|
|
431
|
+
* In issue mode (#2297): one OPEN issue per lexicon, found by a hidden
|
|
432
|
+
* marker and edited in place on every re-run — the same sticky recipe
|
|
433
|
+
* reconcile.ts's `issue` mode uses, via the shared `postOrUpdateGithubIssue`.
|
|
434
|
+
* Never closed by this activity; see that function's doc comment for the
|
|
435
|
+
* close decision, which applies here unchanged.
|
|
436
|
+
*
|
|
387
437
|
* Never auto-merges or auto-publishes.
|
|
388
438
|
*/
|
|
389
439
|
export async function lexiconUpgrade(args: LexiconUpgradeArgs): Promise<LexiconUpgradeResult> {
|
|
@@ -493,16 +543,14 @@ export async function lexiconUpgrade(args: LexiconUpgradeArgs): Promise<LexiconU
|
|
|
493
543
|
const effectiveMode: LexiconUpgradeMode = mode === "pull-request" ? "issue" : mode;
|
|
494
544
|
if (effectiveMode === "issue") {
|
|
495
545
|
const title = `chore(${lexicon}): spec upgrade validation failed`;
|
|
496
|
-
const
|
|
497
|
-
`gh issue create --title ${shellQuote(title)} --body ${shellQuote(summary)}`,
|
|
498
|
-
);
|
|
546
|
+
const issueUrl = await postLexiconIssue(lexicon, title, summary, gh);
|
|
499
547
|
return {
|
|
500
548
|
lexicon,
|
|
501
549
|
mode,
|
|
502
550
|
hasUpgrade: true,
|
|
503
551
|
deltaText,
|
|
504
552
|
semverLabel,
|
|
505
|
-
issueUrl
|
|
553
|
+
issueUrl,
|
|
506
554
|
summary,
|
|
507
555
|
validationOk: false,
|
|
508
556
|
};
|
|
@@ -543,16 +591,14 @@ export async function lexiconUpgrade(args: LexiconUpgradeArgs): Promise<LexiconU
|
|
|
543
591
|
}
|
|
544
592
|
|
|
545
593
|
if (effectiveMode === "issue") {
|
|
546
|
-
const
|
|
547
|
-
`gh issue create --title ${shellQuote(title)} --body ${shellQuote(summary)}`,
|
|
548
|
-
);
|
|
594
|
+
const issueUrl = await postLexiconIssue(lexicon, title, summary, gh);
|
|
549
595
|
return {
|
|
550
596
|
lexicon,
|
|
551
597
|
mode: effectiveMode,
|
|
552
598
|
hasUpgrade: true,
|
|
553
599
|
deltaText,
|
|
554
600
|
semverLabel,
|
|
555
|
-
issueUrl
|
|
601
|
+
issueUrl,
|
|
556
602
|
summary,
|
|
557
603
|
validationOk: true,
|
|
558
604
|
};
|