@intentius/chant-lexicon-github 0.115.0 → 0.116.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/components/generate-op-waves-pipeline.d.ts +31 -0
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/generate-op-waves-pipeline.test.ts +33 -0
- package/src/components/generate-op-waves-pipeline.ts +115 -0
- package/src/plugin.ts +3 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op waves → GitHub Actions workflow (#3679).
|
|
3
|
+
*
|
|
4
|
+
* One workflow for the whole spec, on a push to its branches (and by hand),
|
|
5
|
+
* one run at a time. Each job is one of core's `opWaveJobs`: a wave, or a wide
|
|
6
|
+
* wave's deciding job and share jobs, chained by `needs:` so a wave that waits
|
|
7
|
+
* at its gate (exit 3) or fails stops every later wave. Re-running the failed
|
|
8
|
+
* jobs after `chant approve` carries on from that wave.
|
|
9
|
+
*
|
|
10
|
+
* Every job checks out the full history, because the runner reads the gate
|
|
11
|
+
* policy from the spec file at the base commit and records pending gates on
|
|
12
|
+
* `chant/lifecycle`, which is why the workflow asks for `contents: write`. A
|
|
13
|
+
* deciding job uploads its decision, even when it waits, and each share job
|
|
14
|
+
* downloads it. The forgejo generator reuses {@link buildGithubOpWavesDoc}
|
|
15
|
+
* and applies its dialect.
|
|
16
|
+
*/
|
|
17
|
+
import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
18
|
+
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
19
|
+
import { type GithubOpPipelineDoc } from "./generate-op-pipeline.js";
|
|
20
|
+
/** The default image: it carries git, which the runner needs to read the base commit. */
|
|
21
|
+
export declare const OP_WAVES_IMAGE = "node:22";
|
|
22
|
+
/** The artifact a wide wave's deciding job hands its share jobs. */
|
|
23
|
+
export declare function opWaveRecordArtifact(spec: Pick<OpWavesSpec, "name">, wave: number): string;
|
|
24
|
+
/** The workflow document, before emission, and the jobs it holds. */
|
|
25
|
+
export declare function buildGithubOpWavesDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions): {
|
|
26
|
+
doc: GithubOpPipelineDoc;
|
|
27
|
+
jobs: OpWavesPipelineResult["jobs"];
|
|
28
|
+
};
|
|
29
|
+
/** Render an Op waves spec as one GitHub Actions workflow, `<name>.yml`. */
|
|
30
|
+
export declare function generateGithubOpWavesPipeline(spec: OpWavesSpec, options: OpWavesPipelineOptions): OpWavesPipelineResult;
|
|
31
|
+
//# sourceMappingURL=generate-op-waves-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAEhC,yFAAyF;AACzF,eAAO,MAAM,cAAc,YAAY,CAAC;AAExC,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1F;AAOD,qEAAqE;AACrE,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,sBAAsB,GAC9B;IAAE,GAAG,EAAE,mBAAmB,CAAC;IAAC,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAA;CAAE,CA0DnE;AAQD,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAGvH"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "f949c0f2abdd7cdae723d3c5e0b478955312e5519455f20f466f85267a324ebd",
|
|
5
5
|
"meta.json": "c50e0ef3845c370c3ad2210b8bba455980417c0f818aa6ed46e06d4fb8b1b9f9",
|
|
6
6
|
"types/index.d.ts": "bdd54a4145a39a9dd4f7bbc8c500191acb6bc9057a87da0040475afac84db397",
|
|
7
7
|
"rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
"skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
|
|
79
79
|
"skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
|
|
80
80
|
},
|
|
81
|
-
"composite": "
|
|
81
|
+
"composite": "2903da07a25bb2dc474145fdbefc01607599f0f8e64039b5c6428964a8c15b45"
|
|
82
82
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AA8B7F,eAAO,MAAM,YAAY,EAAE,aAgV1B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.116.0",
|
|
4
4
|
"description": "GitHub Actions lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@intentius/chant": "^0.
|
|
64
|
+
"@intentius/chant": "^0.116.0",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
3
|
+
import { generateGithubOpWavesPipeline } from "./generate-op-waves-pipeline";
|
|
4
|
+
|
|
5
|
+
const spec: OpWavesSpec = {
|
|
6
|
+
name: "migrations",
|
|
7
|
+
op: "migrate",
|
|
8
|
+
plan: ["./bin/plan", "{target}", "{plan}"],
|
|
9
|
+
waves: [
|
|
10
|
+
{ name: "dev", runs: [{ target: "dev" }], gate: "never" },
|
|
11
|
+
{ name: "prod", runs: [{ target: "a" }, { target: "b" }], gate: "on-destructive", shares: 2, environment: { name: "production" } },
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("generateGithubOpWavesPipeline", () => {
|
|
16
|
+
test("one workflow; each wave needs the one before, and a wide wave hands its decision to its shares", () => {
|
|
17
|
+
const { files, jobs } = generateGithubOpWavesPipeline(spec, { specFile: "waves.json" });
|
|
18
|
+
expect(files.map((f) => f.name)).toEqual(["migrations.yml"]);
|
|
19
|
+
expect(jobs.map((j) => j.jobName)).toEqual(["wave-1-dev", "wave-2-prod-decide", "wave-2-prod-share-1", "wave-2-prod-share-2"]);
|
|
20
|
+
const yaml = files[0]!.yaml;
|
|
21
|
+
expect(yaml).toContain("push:\n branches:\n - main");
|
|
22
|
+
expect(yaml).toContain("permissions:\n contents: write");
|
|
23
|
+
expect(yaml).toContain("fetch-depth: 0");
|
|
24
|
+
expect(yaml).toContain("run: chant run wave --spec waves.json --wave 1\n");
|
|
25
|
+
expect(yaml).toMatch(/wave-2-prod-decide:\n\s+runs-on: ubuntu-latest\n\s+needs:\n\s+- wave-1-dev/);
|
|
26
|
+
expect(yaml).toContain("run: chant run wave --spec waves.json --wave 2 --decide");
|
|
27
|
+
expect(yaml).toMatch(/wave-2-prod-share-2:\n\s+runs-on: ubuntu-latest\n\s+needs:\n\s+- wave-2-prod-decide/);
|
|
28
|
+
expect(yaml).toContain("run: chant run wave --spec waves.json --wave 2 --share 2");
|
|
29
|
+
expect(yaml).toContain("uses: actions/upload-artifact@v4");
|
|
30
|
+
expect(yaml).toContain("name: migrations-wave-2-decision");
|
|
31
|
+
expect(yaml).toContain("environment:\n name: production");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op waves → GitHub Actions workflow (#3679).
|
|
3
|
+
*
|
|
4
|
+
* One workflow for the whole spec, on a push to its branches (and by hand),
|
|
5
|
+
* one run at a time. Each job is one of core's `opWaveJobs`: a wave, or a wide
|
|
6
|
+
* wave's deciding job and share jobs, chained by `needs:` so a wave that waits
|
|
7
|
+
* at its gate (exit 3) or fails stops every later wave. Re-running the failed
|
|
8
|
+
* jobs after `chant approve` carries on from that wave.
|
|
9
|
+
*
|
|
10
|
+
* Every job checks out the full history, because the runner reads the gate
|
|
11
|
+
* policy from the spec file at the base commit and records pending gates on
|
|
12
|
+
* `chant/lifecycle`, which is why the workflow asks for `contents: write`. A
|
|
13
|
+
* deciding job uploads its decision, even when it waits, and each share job
|
|
14
|
+
* downloads it. The forgejo generator reuses {@link buildGithubOpWavesDoc}
|
|
15
|
+
* and applies its dialect.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { opWaveJobs, type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
19
|
+
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
20
|
+
import { actionRef } from "../action-pins";
|
|
21
|
+
import {
|
|
22
|
+
assertEnvironment,
|
|
23
|
+
assertSetupSteps,
|
|
24
|
+
emitOpPipelineYAML,
|
|
25
|
+
type GithubOpPipelineDoc,
|
|
26
|
+
} from "./generate-op-pipeline";
|
|
27
|
+
|
|
28
|
+
/** The default image: it carries git, which the runner needs to read the base commit. */
|
|
29
|
+
export const OP_WAVES_IMAGE = "node:22";
|
|
30
|
+
|
|
31
|
+
/** The artifact a wide wave's deciding job hands its share jobs. */
|
|
32
|
+
export function opWaveRecordArtifact(spec: Pick<OpWavesSpec, "name">, wave: number): string {
|
|
33
|
+
return `${spec.name}-wave-${wave}-decision`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function dirOf(path: string): string {
|
|
37
|
+
const i = path.lastIndexOf("/");
|
|
38
|
+
return i < 0 ? "." : path.slice(0, i);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The workflow document, before emission, and the jobs it holds. */
|
|
42
|
+
export function buildGithubOpWavesDoc(
|
|
43
|
+
spec: OpWavesSpec,
|
|
44
|
+
options: OpWavesPipelineOptions,
|
|
45
|
+
): { doc: GithubOpPipelineDoc; jobs: OpWavesPipelineResult["jobs"] } {
|
|
46
|
+
const jobs = opWaveJobs(spec, options.specFile);
|
|
47
|
+
const image = options.image ?? OP_WAVES_IMAGE;
|
|
48
|
+
for (const wave of spec.waves) {
|
|
49
|
+
assertSetupSteps(`${spec.name}/${wave.name}`, wave.setup ?? []);
|
|
50
|
+
if (wave.environment) assertEnvironment(`${spec.name}/${wave.name}`, wave.environment);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const jobsDoc: Record<string, unknown> = {};
|
|
54
|
+
for (const job of jobs) {
|
|
55
|
+
const wave = spec.waves[job.wave - 1]!;
|
|
56
|
+
const steps: Array<Record<string, unknown>> = [{ uses: actionRef("actions/checkout"), with: { "fetch-depth": 0 } }];
|
|
57
|
+
if (job.kind === "share") {
|
|
58
|
+
steps.push({
|
|
59
|
+
name: `Download wave ${job.wave}'s decision`,
|
|
60
|
+
uses: "actions/download-artifact@v4",
|
|
61
|
+
with: { name: opWaveRecordArtifact(spec, job.wave), path: dirOf(job.record!) },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const step of wave.setup ?? []) steps.push({ ...step });
|
|
65
|
+
for (const line of options.beforeScript ?? []) steps.push({ run: line });
|
|
66
|
+
steps.push({ name: describeJob(job.kind, job.wave, wave.name, job.share), run: job.command.join(" ") });
|
|
67
|
+
for (const line of options.extraScript ?? []) steps.push({ run: line });
|
|
68
|
+
if (job.kind === "decide") {
|
|
69
|
+
steps.push({
|
|
70
|
+
name: `Keep wave ${job.wave}'s decision`,
|
|
71
|
+
if: "always()",
|
|
72
|
+
uses: "actions/upload-artifact@v4",
|
|
73
|
+
with: {
|
|
74
|
+
name: opWaveRecordArtifact(spec, job.wave),
|
|
75
|
+
path: job.record!,
|
|
76
|
+
"if-no-files-found": "ignore",
|
|
77
|
+
"include-hidden-files": true,
|
|
78
|
+
overwrite: true,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
jobsDoc[job.jobName] = {
|
|
83
|
+
"runs-on": "ubuntu-latest",
|
|
84
|
+
...(job.needs.length > 0 ? { needs: job.needs } : {}),
|
|
85
|
+
container: image,
|
|
86
|
+
...(wave.environment
|
|
87
|
+
? { environment: { name: wave.environment.name, ...(wave.environment.url ? { url: wave.environment.url } : {}) } }
|
|
88
|
+
: {}),
|
|
89
|
+
...(wave.variables && Object.keys(wave.variables).length > 0 ? { env: wave.variables } : {}),
|
|
90
|
+
steps,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const doc: GithubOpPipelineDoc = {
|
|
95
|
+
name: spec.name,
|
|
96
|
+
on: { push: { branches: spec.branches?.length ? spec.branches : ["main"] }, workflow_dispatch: {} },
|
|
97
|
+
...(options.variables && Object.keys(options.variables).length > 0 ? { env: options.variables } : {}),
|
|
98
|
+
concurrency: { group: spec.name, "cancel-in-progress": false },
|
|
99
|
+
permissions: { contents: "write" },
|
|
100
|
+
jobsDoc,
|
|
101
|
+
};
|
|
102
|
+
return { doc, jobs };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function describeJob(kind: "wave" | "decide" | "share", wave: number, name: string, share?: number): string {
|
|
106
|
+
if (kind === "decide") return `Plan wave ${wave} (${name}) and decide its gate`;
|
|
107
|
+
if (kind === "share") return `Apply share ${share} of wave ${wave} (${name})`;
|
|
108
|
+
return `Plan, gate and apply wave ${wave} (${name})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Render an Op waves spec as one GitHub Actions workflow, `<name>.yml`. */
|
|
112
|
+
export function generateGithubOpWavesPipeline(spec: OpWavesSpec, options: OpWavesPipelineOptions): OpWavesPipelineResult {
|
|
113
|
+
const { doc, jobs } = buildGithubOpWavesDoc(spec, options);
|
|
114
|
+
return { files: [{ name: `${spec.name}.yml`, yaml: emitOpPipelineYAML(doc) }], jobs };
|
|
115
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { GitHubActionsGenerator } from "./import/generator";
|
|
|
34
34
|
import { githubContextTools } from "./mcp/context-tools";
|
|
35
35
|
import { generateGithubPipeline } from "./components/generate-pipeline";
|
|
36
36
|
import { generateGithubOpPipeline } from "./components/generate-op-pipeline";
|
|
37
|
+
import { generateGithubOpWavesPipeline } from "./components/generate-op-waves-pipeline";
|
|
37
38
|
|
|
38
39
|
export const githubPlugin: LexiconPlugin = {
|
|
39
40
|
name: "github",
|
|
@@ -42,6 +43,8 @@ export const githubPlugin: LexiconPlugin = {
|
|
|
42
43
|
generateComponentPipeline: (components, options) => generateGithubPipeline(components, options),
|
|
43
44
|
// Generate mode, Op counterpart (#927): synthesize one cron-triggered workflow per scheduled Op.
|
|
44
45
|
generateOpPipeline: (ops, options) => generateGithubOpPipeline(ops, options),
|
|
46
|
+
// Ordered waves of Op runs, each behind one gate (#3679).
|
|
47
|
+
generateOpWavesPipeline: (spec, options) => generateGithubOpWavesPipeline(spec, options),
|
|
45
48
|
serializer: githubSerializer,
|
|
46
49
|
|
|
47
50
|
lintRules(): LintRule[] {
|