@intentius/chant-lexicon-github 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/components/generate-pipeline.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__fixtures__/pr-loop.github.golden.yml +17 -3
- package/src/components/generate-pipeline.member.test.ts +1 -1
- package/src/components/generate-pipeline.test.ts +5 -2
- package/src/components/generate-pipeline.ts +41 -8
- package/src/components/generate-pr-pipeline.test.ts +79 -0
- package/src/composites/composites.test.ts +128 -125
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAGH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAKjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAGH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAKjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAqMD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAWpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CA+KnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAU/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "bc9354c7588b77975a1fc52ebe5fb14915137fa87a6982c32c464f12c30c2b80",
|
|
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": "3b71f70f79d394ab86c7cd10ffc18228599568f05f9588335786c50ce73dc3a2"
|
|
82
82
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.3",
|
|
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.106.3",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
plan:
|
|
16
16
|
if: github.event_name == 'pull_request'
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
|
-
container: node:22
|
|
18
|
+
container: node:22
|
|
19
19
|
permissions:
|
|
20
20
|
contents: read
|
|
21
21
|
pull-requests: write
|
|
@@ -24,6 +24,7 @@ jobs:
|
|
|
24
24
|
- uses: actions/checkout@v7
|
|
25
25
|
with:
|
|
26
26
|
fetch-depth: 0
|
|
27
|
+
- run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
27
28
|
- name: Plan the members this pull request reaches
|
|
28
29
|
env:
|
|
29
30
|
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
|
@@ -41,7 +42,7 @@ jobs:
|
|
|
41
42
|
apply:
|
|
42
43
|
if: github.event_name == 'push'
|
|
43
44
|
runs-on: ubuntu-latest
|
|
44
|
-
container: node:22
|
|
45
|
+
container: node:22
|
|
45
46
|
concurrency:
|
|
46
47
|
group: chant-apply-prod
|
|
47
48
|
cancel-in-progress: false
|
|
@@ -53,11 +54,24 @@ jobs:
|
|
|
53
54
|
- uses: actions/checkout@v7
|
|
54
55
|
with:
|
|
55
56
|
fetch-depth: 0
|
|
57
|
+
- run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
58
|
+
- name: Restore the record of an earlier attempt at this commit
|
|
59
|
+
uses: actions/cache/restore@v4
|
|
60
|
+
with:
|
|
61
|
+
path: .chant/pr-resume/pr-apply.json
|
|
62
|
+
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
63
|
+
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
56
64
|
- name: Apply the plan a reviewer approved
|
|
57
65
|
env:
|
|
58
66
|
BASE_SHA: '${{ github.event.before }}'
|
|
59
67
|
GITHUB_TOKEN: '${{ github.token }}'
|
|
60
|
-
run: chant components pr-apply --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --require-review
|
|
68
|
+
run: chant components pr-apply --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --require-review --resume .chant/pr-resume/pr-apply.json
|
|
69
|
+
- name: Keep the record for a re-run
|
|
70
|
+
if: always()
|
|
71
|
+
uses: actions/cache/save@v4
|
|
72
|
+
with:
|
|
73
|
+
path: .chant/pr-resume/pr-apply.json
|
|
74
|
+
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
61
75
|
- name: Keep the apply report
|
|
62
76
|
if: always()
|
|
63
77
|
uses: actions/upload-artifact@v4
|
|
@@ -49,7 +49,7 @@ describe("generateGithubPipeline for a workspace member", () => {
|
|
|
49
49
|
test("run steps start in the member's directory, and the name carries the member", () => {
|
|
50
50
|
const doc = parseYAML(generateGithubPipeline(components, { env: "staging", member: api }).yaml) as unknown as Doc;
|
|
51
51
|
expect(doc.defaults).toEqual({ run: { "working-directory": "services/api" } });
|
|
52
|
-
expect(doc.name).toBe("chant-components
|
|
52
|
+
expect(doc.name).toBe("chant-components.api.staging");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
test("artifact paths move under the member's directory, where the run steps write them", () => {
|
|
@@ -438,10 +438,13 @@ describe("generateGithubPipeline: gated waves (#3049)", () => {
|
|
|
438
438
|
const steps = parsed.jobs["wave-2"].steps;
|
|
439
439
|
expect(steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
440
440
|
expect(steps[1]).toMatchObject({ uses: "actions/download-artifact@v4", with: { name: "fan-out-record-wave-1", path: ".chant" } });
|
|
441
|
-
|
|
441
|
+
// The default image is the pr-loop one: node:22, with OpenTofu installed when missing.
|
|
442
|
+
expect(parsed.jobs["wave-2"]).toMatchObject({ container: "node:22" });
|
|
443
|
+
expect(steps[2].run).toMatch(/^command -v tofu /);
|
|
444
|
+
expect(steps[3].run).toBe(
|
|
442
445
|
"chant components fan-out --base HEAD~1 --env prod --wave-gate release --wave 2 --resume .chant/fan-out.json",
|
|
443
446
|
);
|
|
444
|
-
expect(steps[
|
|
447
|
+
expect(steps[4]).toMatchObject({ if: "always()", with: { name: "fan-out-record-wave-2", path: ".chant/fan-out.json" } });
|
|
445
448
|
// Wave 1 has nothing to download.
|
|
446
449
|
expect(parsed.jobs["wave-1"].steps.some((s) => s.uses === "actions/download-artifact@v4")).toBe(false);
|
|
447
450
|
});
|
|
@@ -60,7 +60,7 @@ import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
|
|
|
60
60
|
import { resolveComponentGraph, type DriverComponent } from "@intentius/chant/components/driver";
|
|
61
61
|
import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
|
|
62
62
|
import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
|
|
63
|
-
import { PR_LOOP_REPORT_DIR, prApplyGroup, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
63
|
+
import { PR_APPLY_RECORD, PR_LOOP_IMAGE, PR_LOOP_REPORT_DIR, prApplyGroup, prApplyRecordKey, prLoopJobs, prLoopSetup } from "@intentius/chant/components/pr-pipeline";
|
|
64
64
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
65
65
|
import type {
|
|
66
66
|
ComponentPipelineJob as GeneratedJob,
|
|
@@ -142,6 +142,7 @@ function gatedWaveGithubDoc(
|
|
|
142
142
|
with: { name: waveRecordArtifact(job.wave - 1), path: dirnameOf(GATED_WAVE_RECORD) },
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
for (const line of prLoopSetup(image)) steps.push({ run: line });
|
|
145
146
|
for (const line of beforeScript) steps.push({ run: line });
|
|
146
147
|
steps.push({ run: job.command.join(" ") });
|
|
147
148
|
for (const line of extraScript) steps.push({ run: line });
|
|
@@ -183,6 +184,15 @@ function gatedWaveGithubDoc(
|
|
|
183
184
|
* job can push, to record a pending fact on `chant/lifecycle`, and runs one
|
|
184
185
|
* at a time per environment. Both keep their report as an artifact, and both
|
|
185
186
|
* check out the full history, since each measures the change with git.
|
|
187
|
+
* The apply job restores its attempt record from the cache before it runs
|
|
188
|
+
* and saves it afterwards, failed or not, so a re-run of a failed apply
|
|
189
|
+
* finishes it under the same approval (#3543).
|
|
190
|
+
*
|
|
191
|
+
* For a workspace member (#3465) the run steps start in the member's
|
|
192
|
+
* directory and the report artifact is kept from there. The triggers take no
|
|
193
|
+
* path filter, since a change outside the member can reach it; the plan
|
|
194
|
+
* selects from the whole change and passes `--member`, which keeps the
|
|
195
|
+
* member's gate, note and statuses apart from the other members'.
|
|
186
196
|
*/
|
|
187
197
|
function prLoopGithubDoc(
|
|
188
198
|
env: string,
|
|
@@ -193,17 +203,40 @@ function prLoopGithubDoc(
|
|
|
193
203
|
): GithubPipelineDoc {
|
|
194
204
|
const loop = options.prLoop!;
|
|
195
205
|
const branch = loop.branch ?? "main";
|
|
196
|
-
const
|
|
206
|
+
const member = options.member;
|
|
207
|
+
const rooted = !member || member.dir === "." || member.dir === "";
|
|
208
|
+
const [plan, apply] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
|
|
209
|
+
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
210
|
+
// The apply's attempt record (#3543) lives in the cache under the pushed
|
|
211
|
+
// commit. Cache keys cannot be overwritten, so each attempt saves under
|
|
212
|
+
// its run and attempt, and a re-run restores the newest for the commit.
|
|
213
|
+
const recordPath = member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD;
|
|
214
|
+
const recordPrefix = `${prApplyRecordKey(env, "${{ github.sha }}", member?.name)}-`;
|
|
215
|
+
const recordKey = `${recordPrefix}\${{ github.run_id }}-\${{ github.run_attempt }}`;
|
|
216
|
+
const restoreRecord = {
|
|
217
|
+
name: "Restore the record of an earlier attempt at this commit",
|
|
218
|
+
uses: "actions/cache/restore@v4",
|
|
219
|
+
with: { path: recordPath, key: recordKey, "restore-keys": recordPrefix },
|
|
220
|
+
};
|
|
221
|
+
const saveRecord = {
|
|
222
|
+
name: "Keep the record for a re-run",
|
|
223
|
+
if: "always()",
|
|
224
|
+
uses: "actions/cache/save@v4",
|
|
225
|
+
with: { path: recordPath, key: recordKey },
|
|
226
|
+
};
|
|
197
227
|
const steps = (job: typeof plan, name: string): Array<Record<string, unknown>> => [
|
|
198
228
|
{ uses: actionRef("actions/checkout"), with: { "fetch-depth": 0 } },
|
|
229
|
+
...prLoopSetup(image).map((line) => ({ run: line })),
|
|
199
230
|
...beforeScript.map((line) => ({ run: line })),
|
|
231
|
+
...(job.jobName === "apply" ? [restoreRecord] : []),
|
|
200
232
|
{ name, env: job.env, run: job.command },
|
|
201
233
|
...extraScript.map((line) => ({ run: line })),
|
|
234
|
+
...(job.jobName === "apply" ? [saveRecord] : []),
|
|
202
235
|
{
|
|
203
236
|
name: `Keep the ${job.jobName} report`,
|
|
204
237
|
if: "always()",
|
|
205
238
|
uses: "actions/upload-artifact@v4",
|
|
206
|
-
with: { name: `chant-pr-${job.jobName}`, path:
|
|
239
|
+
with: { name: `chant-pr-${job.jobName}`, path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
|
|
207
240
|
},
|
|
208
241
|
];
|
|
209
242
|
const jobsDoc: Record<string, unknown> = {
|
|
@@ -218,15 +251,16 @@ function prLoopGithubDoc(
|
|
|
218
251
|
if: "github.event_name == 'push'",
|
|
219
252
|
"runs-on": "ubuntu-latest",
|
|
220
253
|
container: image,
|
|
221
|
-
concurrency: { group: prApplyGroup(env), "cancel-in-progress": false },
|
|
254
|
+
concurrency: { group: prApplyGroup(env, member?.name), "cancel-in-progress": false },
|
|
222
255
|
permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
|
|
223
256
|
steps: steps(apply, "Apply the plan a reviewer approved"),
|
|
224
257
|
},
|
|
225
258
|
};
|
|
226
259
|
return {
|
|
227
|
-
name: `chant-pr-${env}`,
|
|
260
|
+
name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
|
|
228
261
|
environment: env,
|
|
229
262
|
on: { pull_request: { branches: [branch] }, push: { branches: [branch] } },
|
|
263
|
+
...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
|
|
230
264
|
env: { ...options.variables, CHANT_ENV: env },
|
|
231
265
|
jobsDoc,
|
|
232
266
|
stages: ["plan", "apply"],
|
|
@@ -302,7 +336,7 @@ export function buildGithubPipelineDoc(
|
|
|
302
336
|
options: GenerateGithubOptions = {},
|
|
303
337
|
): GithubPipelineDoc {
|
|
304
338
|
const env = options.env ?? "production";
|
|
305
|
-
const image = options.image ?? DEFAULT_IMAGE;
|
|
339
|
+
const image = options.image ?? (options.prLoop || options.gatedWaves ? PR_LOOP_IMAGE : DEFAULT_IMAGE);
|
|
306
340
|
const runCommand = options.runCommand ?? ["chant", "run", "--components", "{name}", "--env", env];
|
|
307
341
|
const beforeScript = options.beforeScript ?? [];
|
|
308
342
|
const extraScript = options.extraScript ?? [];
|
|
@@ -314,7 +348,6 @@ export function buildGithubPipelineDoc(
|
|
|
314
348
|
if (options.gatedWaves || options.promoteTo !== undefined) {
|
|
315
349
|
throw new Error("a pull-request pipeline has no wave or promote jobs; drop --wave-gate and --promote-to, or --pr-loop");
|
|
316
350
|
}
|
|
317
|
-
if (options.member) throw new Error("a pull-request pipeline is not generated for a workspace member yet; generate it from the project outside the workspace");
|
|
318
351
|
return prLoopGithubDoc(env, image, beforeScript, extraScript, options);
|
|
319
352
|
}
|
|
320
353
|
|
|
@@ -501,7 +534,7 @@ function scopeToMember(doc: GithubPipelineDoc, member: PipelineMember): GithubPi
|
|
|
501
534
|
}
|
|
502
535
|
return {
|
|
503
536
|
...doc,
|
|
504
|
-
name: `chant-components
|
|
537
|
+
name: `chant-components.${member.name}.${doc.environment}`,
|
|
505
538
|
...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
|
|
506
539
|
jobsDoc,
|
|
507
540
|
};
|
|
@@ -11,6 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { describe, expect, test } from "vitest";
|
|
12
12
|
import { parseYAML } from "@intentius/chant/yaml";
|
|
13
13
|
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
14
|
+
import { prApplyGroup, prApplyRecordKey } from "@intentius/chant/components/pr-pipeline";
|
|
14
15
|
import { generateGithubPipeline } from "./generate-pipeline";
|
|
15
16
|
|
|
16
17
|
const ESTATE: DriverComponent[] = [
|
|
@@ -30,6 +31,8 @@ function hasActionlint(): boolean {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
interface Step {
|
|
34
|
+
name?: string;
|
|
35
|
+
if?: string;
|
|
33
36
|
uses?: string;
|
|
34
37
|
run?: string;
|
|
35
38
|
env?: Record<string, string>;
|
|
@@ -73,6 +76,35 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
73
76
|
for (const job of Object.values(doc.jobs)) expect(job.steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
74
77
|
});
|
|
75
78
|
|
|
79
|
+
test("the apply resumes from a record the cache keeps for the pushed commit across a re-run (#3543)", () => {
|
|
80
|
+
const steps = doc.jobs.apply.steps;
|
|
81
|
+
const restore = steps.findIndex((s) => s.uses === "actions/cache/restore@v4");
|
|
82
|
+
const run = steps.findIndex((s) => s.run?.includes("pr-apply"));
|
|
83
|
+
const save = steps.findIndex((s) => s.uses === "actions/cache/save@v4");
|
|
84
|
+
expect(restore).toBeGreaterThan(-1);
|
|
85
|
+
expect(restore).toBeLessThan(run);
|
|
86
|
+
expect(save).toBeGreaterThan(run);
|
|
87
|
+
expect(steps[run].run).toContain("--resume .chant/pr-resume/pr-apply.json");
|
|
88
|
+
const key = "chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}";
|
|
89
|
+
expect(steps[restore].with).toEqual({ path: ".chant/pr-resume/pr-apply.json", key, "restore-keys": "chant-apply-prod-${{ github.sha }}-" });
|
|
90
|
+
// Saved even when the apply failed, which is when a re-run needs it.
|
|
91
|
+
expect(steps[save].if).toBe("always()");
|
|
92
|
+
expect(steps[save].with).toEqual({ path: ".chant/pr-resume/pr-apply.json", key });
|
|
93
|
+
// The plan job runs the pull request's code: it neither reads nor writes the record.
|
|
94
|
+
expect(doc.jobs.plan.steps.some((s) => s.uses?.startsWith("actions/cache"))).toBe(false);
|
|
95
|
+
expect(doc.jobs.plan.steps.find((s) => s.run?.includes("pr-plan"))!.run).not.toContain("--resume");
|
|
96
|
+
// The report artifact does not carry the record, which holds the members' outputs.
|
|
97
|
+
const keep = steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
|
|
98
|
+
expect(keep.with?.path).toBe(".chant/pr");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("runs in an image with git and installs OpenTofu, unless the caller names an image", () => {
|
|
102
|
+
expect(doc.jobs.plan.steps[1].run).toContain("install-opentofu");
|
|
103
|
+
const own = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: {} }).yaml;
|
|
104
|
+
expect(own).toContain("container: example/ci:1");
|
|
105
|
+
expect(own).not.toContain("install-opentofu");
|
|
106
|
+
});
|
|
107
|
+
|
|
76
108
|
test("the gate, branch and review requirement are options", () => {
|
|
77
109
|
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { gate: "infra-apply", branch: "trunk", requireReview: false } }).yaml;
|
|
78
110
|
expect(yaml).toContain("--gate infra-apply");
|
|
@@ -85,6 +117,53 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
85
117
|
expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, promoteTo: "prod" })).toThrow(/pull-request pipeline has no wave or promote jobs/);
|
|
86
118
|
});
|
|
87
119
|
|
|
120
|
+
test("inside a workspace member: runs in the member's directory and keeps its own gate, note and apply group (#3465)", () => {
|
|
121
|
+
const member = { name: "network", dir: "infra/network", file: ".github/workflows/chant-pr.network.prod.yml" };
|
|
122
|
+
const scoped = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member });
|
|
123
|
+
const mdoc = parseYAML(scoped.yaml) as {
|
|
124
|
+
name: string;
|
|
125
|
+
on: Record<string, unknown>;
|
|
126
|
+
defaults?: { run: Record<string, string> };
|
|
127
|
+
jobs: Record<string, Job>;
|
|
128
|
+
};
|
|
129
|
+
expect(mdoc.name).toBe("chant-pr.network.prod");
|
|
130
|
+
// No path filter: a change outside the member can reach it.
|
|
131
|
+
expect(mdoc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] } });
|
|
132
|
+
expect(mdoc.defaults).toEqual({ run: { "working-directory": "infra/network" } });
|
|
133
|
+
const plan = mdoc.jobs.plan.steps.find((s) => s.run?.includes("pr-plan"))!;
|
|
134
|
+
expect(plan.run).toBe('chant components pr-plan --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge github --member network');
|
|
135
|
+
const apply = mdoc.jobs.apply.steps.find((s) => s.run?.includes("pr-apply"))!;
|
|
136
|
+
expect(apply.run).toContain("--member network");
|
|
137
|
+
expect(mdoc.jobs.apply.concurrency).toEqual({ group: "chant-apply.network.prod", "cancel-in-progress": false });
|
|
138
|
+
// The record path is relative to the member for --resume and to the repository for the cache.
|
|
139
|
+
expect(apply.run).toContain("--resume .chant/pr-resume/pr-apply.json");
|
|
140
|
+
const restore = mdoc.jobs.apply.steps.find((s) => s.uses === "actions/cache/restore@v4")!;
|
|
141
|
+
expect(restore.with).toMatchObject({ path: "infra/network/.chant/pr-resume/pr-apply.json", "restore-keys": "chant-apply.network.prod-${{ github.sha }}-" });
|
|
142
|
+
for (const job of Object.values(mdoc.jobs)) {
|
|
143
|
+
const keep = job.steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
|
|
144
|
+
expect(keep.with?.path).toBe("infra/network/.chant/pr");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("no two member and environment pairs share an apply group or record key", () => {
|
|
149
|
+
expect(prApplyGroup("b-c", "a")).toBe("chant-apply.a.b-c");
|
|
150
|
+
expect(prApplyGroup("c", "a-b")).toBe("chant-apply.a-b.c");
|
|
151
|
+
expect(prApplyGroup("b-c", "a")).not.toBe(prApplyGroup("c", "a-b"));
|
|
152
|
+
expect(prApplyRecordKey("b-c", "abc", "a")).not.toBe(prApplyRecordKey("c", "abc", "a-b"));
|
|
153
|
+
// A single project's group never looks like a member's.
|
|
154
|
+
expect(prApplyGroup("a-b-c")).toBe("chant-apply-a-b-c");
|
|
155
|
+
expect(prApplyGroup("a.b-c")).not.toBe(prApplyGroup("b-c", "a"));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a member at the workspace root keeps the root's paths but its own names", () => {
|
|
159
|
+
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member: { name: "root", dir: "." } }).yaml;
|
|
160
|
+
const mdoc = parseYAML(yaml) as { name: string; defaults?: unknown; jobs: Record<string, Job> };
|
|
161
|
+
expect(mdoc.name).toBe("chant-pr.root.prod");
|
|
162
|
+
expect(mdoc.defaults).toBeUndefined();
|
|
163
|
+
expect(yaml).toContain("--member root");
|
|
164
|
+
expect(yaml).toContain("path: .chant/pr");
|
|
165
|
+
});
|
|
166
|
+
|
|
88
167
|
test.skipIf(!hasActionlint())("passes actionlint", () => {
|
|
89
168
|
const dir = mkdtempSync(join(tmpdir(), "chant-actionlint-"));
|
|
90
169
|
try {
|
|
@@ -13,30 +13,33 @@ import { DockerBuild } from "./docker-build";
|
|
|
13
13
|
import { DeployEnvironment } from "./deploy-environment";
|
|
14
14
|
import { GoCI } from "./go-ci";
|
|
15
15
|
|
|
16
|
+
// The generated resource classes type `props` as unknown; the tests read it as a record.
|
|
17
|
+
const propsOf = (entity: unknown): Record<string, any> => (entity as { props: Record<string, any> }).props;
|
|
18
|
+
|
|
16
19
|
// ── Checkout ────────────────────────────────────────────────────────
|
|
17
20
|
|
|
18
21
|
describe("Checkout", () => {
|
|
19
22
|
test("returns step with checkout action", () => {
|
|
20
23
|
const result = Checkout({});
|
|
21
24
|
expect(result.step).toBeDefined();
|
|
22
|
-
expect(result.step.
|
|
23
|
-
expect(result.step.
|
|
25
|
+
expect(propsOf(result.step).uses).toBe("actions/checkout@v7");
|
|
26
|
+
expect(propsOf(result.step).name).toBe("Checkout");
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
test("passes ref as with input", () => {
|
|
27
30
|
const result = Checkout({ ref: "develop" });
|
|
28
|
-
expect(result.step.
|
|
29
|
-
expect((result.step.
|
|
31
|
+
expect(propsOf(result.step).with).toBeDefined();
|
|
32
|
+
expect((propsOf(result.step).with as Record<string, string>).ref).toBe("develop");
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
test("passes fetchDepth as with input", () => {
|
|
33
36
|
const result = Checkout({ fetchDepth: 0 });
|
|
34
|
-
expect((result.step.
|
|
37
|
+
expect((propsOf(result.step).with as Record<string, string>)["fetch-depth"]).toBe("0");
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
test("omits with when no optional props", () => {
|
|
38
41
|
const result = Checkout({});
|
|
39
|
-
expect(result.step.
|
|
42
|
+
expect(propsOf(result.step).with).toBeUndefined();
|
|
40
43
|
});
|
|
41
44
|
});
|
|
42
45
|
|
|
@@ -45,18 +48,18 @@ describe("Checkout", () => {
|
|
|
45
48
|
describe("SetupNode", () => {
|
|
46
49
|
test("returns step with setup-node action", () => {
|
|
47
50
|
const result = SetupNode({});
|
|
48
|
-
expect(result.step.
|
|
49
|
-
expect(result.step.
|
|
51
|
+
expect(propsOf(result.step).uses).toBe("actions/setup-node@v7");
|
|
52
|
+
expect(propsOf(result.step).name).toBe("Setup Node.js");
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
test("passes nodeVersion", () => {
|
|
53
56
|
const result = SetupNode({ nodeVersion: "22" });
|
|
54
|
-
expect((result.step.
|
|
57
|
+
expect((propsOf(result.step).with as Record<string, string>)["node-version"]).toBe("22");
|
|
55
58
|
});
|
|
56
59
|
|
|
57
60
|
test("passes cache option", () => {
|
|
58
61
|
const result = SetupNode({ cache: "npm" });
|
|
59
|
-
expect((result.step.
|
|
62
|
+
expect((propsOf(result.step).with as Record<string, string>).cache).toBe("npm");
|
|
60
63
|
});
|
|
61
64
|
});
|
|
62
65
|
|
|
@@ -65,12 +68,12 @@ describe("SetupNode", () => {
|
|
|
65
68
|
describe("SetupGo", () => {
|
|
66
69
|
test("returns step with setup-go action", () => {
|
|
67
70
|
const result = SetupGo({});
|
|
68
|
-
expect(result.step.
|
|
71
|
+
expect(propsOf(result.step).uses).toBe("actions/setup-go@v5");
|
|
69
72
|
});
|
|
70
73
|
|
|
71
74
|
test("passes goVersion", () => {
|
|
72
75
|
const result = SetupGo({ goVersion: "1.22" });
|
|
73
|
-
expect((result.step.
|
|
76
|
+
expect((propsOf(result.step).with as Record<string, string>)["go-version"]).toBe("1.22");
|
|
74
77
|
});
|
|
75
78
|
});
|
|
76
79
|
|
|
@@ -79,12 +82,12 @@ describe("SetupGo", () => {
|
|
|
79
82
|
describe("SetupPython", () => {
|
|
80
83
|
test("returns step with setup-python action", () => {
|
|
81
84
|
const result = SetupPython({});
|
|
82
|
-
expect(result.step.
|
|
85
|
+
expect(propsOf(result.step).uses).toBe("actions/setup-python@v5");
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
test("passes pythonVersion", () => {
|
|
86
89
|
const result = SetupPython({ pythonVersion: "3.12" });
|
|
87
|
-
expect((result.step.
|
|
90
|
+
expect((propsOf(result.step).with as Record<string, string>)["python-version"]).toBe("3.12");
|
|
88
91
|
});
|
|
89
92
|
});
|
|
90
93
|
|
|
@@ -93,9 +96,9 @@ describe("SetupPython", () => {
|
|
|
93
96
|
describe("CacheAction", () => {
|
|
94
97
|
test("returns step with cache action", () => {
|
|
95
98
|
const result = CacheAction({ path: "node_modules", key: "npm-${{ hashFiles('package-lock.json') }}" });
|
|
96
|
-
expect(result.step.
|
|
97
|
-
expect((result.step.
|
|
98
|
-
expect((result.step.
|
|
99
|
+
expect(propsOf(result.step).uses).toBe("actions/cache@v4");
|
|
100
|
+
expect((propsOf(result.step).with as Record<string, string>).path).toBe("node_modules");
|
|
101
|
+
expect((propsOf(result.step).with as Record<string, string>).key).toBe("npm-${{ hashFiles('package-lock.json') }}");
|
|
99
102
|
});
|
|
100
103
|
});
|
|
101
104
|
|
|
@@ -104,14 +107,14 @@ describe("CacheAction", () => {
|
|
|
104
107
|
describe("UploadArtifact", () => {
|
|
105
108
|
test("returns step with upload action", () => {
|
|
106
109
|
const result = UploadArtifact({ name: "build", path: "dist/" });
|
|
107
|
-
expect(result.step.
|
|
108
|
-
expect((result.step.
|
|
109
|
-
expect((result.step.
|
|
110
|
+
expect(propsOf(result.step).uses).toBe("actions/upload-artifact@v4");
|
|
111
|
+
expect((propsOf(result.step).with as Record<string, string>).name).toBe("build");
|
|
112
|
+
expect((propsOf(result.step).with as Record<string, string>).path).toBe("dist/");
|
|
110
113
|
});
|
|
111
114
|
|
|
112
115
|
test("passes retentionDays", () => {
|
|
113
116
|
const result = UploadArtifact({ name: "build", path: "dist/", retentionDays: 7 });
|
|
114
|
-
expect((result.step.
|
|
117
|
+
expect((propsOf(result.step).with as Record<string, string>)["retention-days"]).toBe("7");
|
|
115
118
|
});
|
|
116
119
|
});
|
|
117
120
|
|
|
@@ -120,8 +123,8 @@ describe("UploadArtifact", () => {
|
|
|
120
123
|
describe("DownloadArtifact", () => {
|
|
121
124
|
test("returns step with download action", () => {
|
|
122
125
|
const result = DownloadArtifact({ name: "build" });
|
|
123
|
-
expect(result.step.
|
|
124
|
-
expect((result.step.
|
|
126
|
+
expect(propsOf(result.step).uses).toBe("actions/download-artifact@v4");
|
|
127
|
+
expect((propsOf(result.step).with as Record<string, string>).name).toBe("build");
|
|
125
128
|
});
|
|
126
129
|
});
|
|
127
130
|
|
|
@@ -136,39 +139,39 @@ describe("NodeCI", () => {
|
|
|
136
139
|
|
|
137
140
|
test("workflow has CI name", () => {
|
|
138
141
|
const result = NodeCI({});
|
|
139
|
-
expect(result.workflow.
|
|
142
|
+
expect(propsOf(result.workflow).name).toBe("CI");
|
|
140
143
|
});
|
|
141
144
|
|
|
142
145
|
test("job has ubuntu-latest runner", () => {
|
|
143
146
|
const result = NodeCI({});
|
|
144
|
-
expect(result.job
|
|
147
|
+
expect(propsOf(result.job)["runs-on"]).toBe("ubuntu-latest");
|
|
145
148
|
});
|
|
146
149
|
|
|
147
150
|
test("job has steps array", () => {
|
|
148
151
|
const result = NodeCI({});
|
|
149
|
-
expect(Array.isArray(result.job.
|
|
150
|
-
expect((result.job.
|
|
152
|
+
expect(Array.isArray(propsOf(result.job).steps)).toBe(true);
|
|
153
|
+
expect((propsOf(result.job).steps as unknown[]).length).toBe(5);
|
|
151
154
|
});
|
|
152
155
|
|
|
153
156
|
test("uses default npm commands", () => {
|
|
154
157
|
const result = NodeCI({});
|
|
155
|
-
const steps = result.job.
|
|
158
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
156
159
|
const installStep = steps[2];
|
|
157
|
-
expect(installStep.
|
|
160
|
+
expect(propsOf(installStep).run).toBe("npm ci");
|
|
158
161
|
});
|
|
159
162
|
|
|
160
163
|
test("uses pnpm when specified", () => {
|
|
161
164
|
const result = NodeCI({ packageManager: "pnpm" });
|
|
162
|
-
const steps = result.job.
|
|
165
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
163
166
|
const installStep = steps[2];
|
|
164
|
-
expect(installStep.
|
|
167
|
+
expect(propsOf(installStep).run).toBe("pnpm install");
|
|
165
168
|
});
|
|
166
169
|
|
|
167
170
|
test("uses custom installCommand", () => {
|
|
168
171
|
const result = NodeCI({ installCommand: "yarn install --frozen-lockfile" });
|
|
169
|
-
const steps = result.job.
|
|
172
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
170
173
|
const installStep = steps[2];
|
|
171
|
-
expect(installStep.
|
|
174
|
+
expect(propsOf(installStep).run).toBe("yarn install --frozen-lockfile");
|
|
172
175
|
});
|
|
173
176
|
});
|
|
174
177
|
|
|
@@ -184,98 +187,98 @@ describe("NodePipeline", () => {
|
|
|
184
187
|
|
|
185
188
|
test("workflow has Node Pipeline name", () => {
|
|
186
189
|
const result = NodePipeline({});
|
|
187
|
-
expect(result.workflow.
|
|
190
|
+
expect(propsOf(result.workflow).name).toBe("Node Pipeline");
|
|
188
191
|
});
|
|
189
192
|
|
|
190
193
|
test("buildJob has 5 steps (checkout, setup, install, build, upload)", () => {
|
|
191
194
|
const result = NodePipeline({});
|
|
192
|
-
const steps = result.buildJob.
|
|
195
|
+
const steps = propsOf(result.buildJob).steps as unknown[];
|
|
193
196
|
expect(steps.length).toBe(5);
|
|
194
197
|
});
|
|
195
198
|
|
|
196
199
|
test("testJob has 5 steps (checkout, setup, install, download, test)", () => {
|
|
197
200
|
const result = NodePipeline({});
|
|
198
|
-
const steps = result.testJob.
|
|
201
|
+
const steps = propsOf(result.testJob).steps as unknown[];
|
|
199
202
|
expect(steps.length).toBe(5);
|
|
200
203
|
});
|
|
201
204
|
|
|
202
205
|
test("testJob depends on build", () => {
|
|
203
206
|
const result = NodePipeline({});
|
|
204
|
-
expect(result.testJob.
|
|
207
|
+
expect(propsOf(result.testJob).needs).toEqual(["build"]);
|
|
205
208
|
});
|
|
206
209
|
|
|
207
210
|
test("uses npm ci by default", () => {
|
|
208
211
|
const result = NodePipeline({});
|
|
209
|
-
const steps = result.buildJob.
|
|
212
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
210
213
|
expect(steps[2].props.run).toBe("npm ci");
|
|
211
214
|
});
|
|
212
215
|
|
|
213
216
|
test("uses pnpm install with frozen lockfile", () => {
|
|
214
217
|
const result = NodePipeline({ packageManager: "pnpm" });
|
|
215
|
-
const steps = result.buildJob.
|
|
218
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
216
219
|
expect(steps[2].props.run).toBe("pnpm install --frozen-lockfile");
|
|
217
220
|
});
|
|
218
221
|
|
|
219
222
|
test("uses yarn install with frozen lockfile", () => {
|
|
220
223
|
const result = NodePipeline({ packageManager: "yarn" });
|
|
221
|
-
const steps = result.buildJob.
|
|
224
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
222
225
|
expect(steps[2].props.run).toBe("yarn install --frozen-lockfile");
|
|
223
226
|
});
|
|
224
227
|
|
|
225
228
|
test("bun uses oven-sh/setup-bun instead of setup-node", () => {
|
|
226
229
|
const result = NodePipeline({ packageManager: "bun" });
|
|
227
|
-
const steps = result.buildJob.
|
|
230
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
228
231
|
expect(steps[1].props.uses).toBe("oven-sh/setup-bun@v2");
|
|
229
232
|
expect(steps[1].props.name).toBe("Setup Bun");
|
|
230
233
|
});
|
|
231
234
|
|
|
232
235
|
test("bun uses bun install with frozen lockfile", () => {
|
|
233
236
|
const result = NodePipeline({ packageManager: "bun" });
|
|
234
|
-
const steps = result.buildJob.
|
|
237
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
235
238
|
expect(steps[2].props.run).toBe("bun install --frozen-lockfile");
|
|
236
239
|
});
|
|
237
240
|
|
|
238
241
|
test("npm setup step has cache: npm", () => {
|
|
239
242
|
const result = NodePipeline({ packageManager: "npm" });
|
|
240
|
-
const steps = result.buildJob.
|
|
243
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
241
244
|
expect((steps[1].props.with as Record<string, string>).cache).toBe("npm");
|
|
242
245
|
});
|
|
243
246
|
|
|
244
247
|
test("upload step uses upload-artifact with configured name", () => {
|
|
245
248
|
const result = NodePipeline({ artifactName: "my-build" });
|
|
246
|
-
const steps = result.buildJob.
|
|
249
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
247
250
|
expect(steps[4].props.uses).toBe("actions/upload-artifact@v4");
|
|
248
251
|
expect((steps[4].props.with as Record<string, string>).name).toBe("my-build");
|
|
249
252
|
});
|
|
250
253
|
|
|
251
254
|
test("download step in testJob uses configured artifact name", () => {
|
|
252
255
|
const result = NodePipeline({ artifactName: "my-build" });
|
|
253
|
-
const steps = result.testJob.
|
|
256
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
254
257
|
expect(steps[3].props.uses).toBe("actions/download-artifact@v4");
|
|
255
258
|
expect((steps[3].props.with as Record<string, string>).name).toBe("my-build");
|
|
256
259
|
});
|
|
257
260
|
|
|
258
261
|
test("uses custom runsOn", () => {
|
|
259
262
|
const result = NodePipeline({ runsOn: "self-hosted" });
|
|
260
|
-
expect(result.buildJob
|
|
261
|
-
expect(result.testJob
|
|
263
|
+
expect(propsOf(result.buildJob)["runs-on"]).toBe("self-hosted");
|
|
264
|
+
expect(propsOf(result.testJob)["runs-on"]).toBe("self-hosted");
|
|
262
265
|
});
|
|
263
266
|
|
|
264
267
|
test("uses custom installCommand", () => {
|
|
265
268
|
const result = NodePipeline({ installCommand: "npm install --legacy-peer-deps" });
|
|
266
|
-
const steps = result.buildJob.
|
|
269
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
267
270
|
expect(steps[2].props.run).toBe("npm install --legacy-peer-deps");
|
|
268
271
|
});
|
|
269
272
|
|
|
270
273
|
test("build step uses configured script", () => {
|
|
271
274
|
const result = NodePipeline({ buildScript: "compile" });
|
|
272
|
-
const steps = result.buildJob.
|
|
275
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
273
276
|
expect(steps[3].props.run).toBe("npm run compile");
|
|
274
277
|
});
|
|
275
278
|
|
|
276
279
|
test("test step uses configured script", () => {
|
|
277
280
|
const result = NodePipeline({ testScript: "test:ci" });
|
|
278
|
-
const steps = result.testJob.
|
|
281
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
279
282
|
expect(steps[4].props.run).toBe("npm run test:ci");
|
|
280
283
|
});
|
|
281
284
|
});
|
|
@@ -285,7 +288,7 @@ describe("NodePipeline", () => {
|
|
|
285
288
|
describe("BunPipeline", () => {
|
|
286
289
|
test("defaults to bun package manager", () => {
|
|
287
290
|
const result = BunPipeline({});
|
|
288
|
-
const steps = result.buildJob.
|
|
291
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
289
292
|
expect(steps[1].props.uses).toBe("oven-sh/setup-bun@v2");
|
|
290
293
|
expect(steps[2].props.run).toBe("bun install --frozen-lockfile");
|
|
291
294
|
});
|
|
@@ -294,7 +297,7 @@ describe("BunPipeline", () => {
|
|
|
294
297
|
describe("PnpmPipeline", () => {
|
|
295
298
|
test("defaults to pnpm package manager", () => {
|
|
296
299
|
const result = PnpmPipeline({});
|
|
297
|
-
const steps = result.buildJob.
|
|
300
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
298
301
|
expect((steps[1].props.with as Record<string, string>).cache).toBe("pnpm");
|
|
299
302
|
expect(steps[2].props.run).toBe("pnpm install --frozen-lockfile");
|
|
300
303
|
});
|
|
@@ -303,7 +306,7 @@ describe("PnpmPipeline", () => {
|
|
|
303
306
|
describe("YarnPipeline", () => {
|
|
304
307
|
test("defaults to yarn package manager", () => {
|
|
305
308
|
const result = YarnPipeline({});
|
|
306
|
-
const steps = result.buildJob.
|
|
309
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
307
310
|
expect((steps[1].props.with as Record<string, string>).cache).toBe("yarn");
|
|
308
311
|
expect(steps[2].props.run).toBe("yarn install --frozen-lockfile");
|
|
309
312
|
});
|
|
@@ -320,7 +323,7 @@ describe("PythonCI", () => {
|
|
|
320
323
|
|
|
321
324
|
test("workflow has Python CI name", () => {
|
|
322
325
|
const result = PythonCI({});
|
|
323
|
-
expect(result.workflow.
|
|
326
|
+
expect(propsOf(result.workflow).name).toBe("Python CI");
|
|
324
327
|
});
|
|
325
328
|
|
|
326
329
|
test("includes lintJob by default", () => {
|
|
@@ -335,66 +338,66 @@ describe("PythonCI", () => {
|
|
|
335
338
|
|
|
336
339
|
test("testJob uses setup-python with default version", () => {
|
|
337
340
|
const result = PythonCI({});
|
|
338
|
-
const steps = result.testJob.
|
|
341
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
339
342
|
const setupStep = steps[1];
|
|
340
|
-
expect(setupStep.
|
|
341
|
-
expect((setupStep.
|
|
343
|
+
expect(propsOf(setupStep).uses).toBe("actions/setup-python@v5");
|
|
344
|
+
expect((propsOf(setupStep).with as Record<string, string>)["python-version"]).toBe("3.12");
|
|
342
345
|
});
|
|
343
346
|
|
|
344
347
|
test("testJob uses pip cache by default", () => {
|
|
345
348
|
const result = PythonCI({});
|
|
346
|
-
const steps = result.testJob.
|
|
349
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
347
350
|
expect((steps[1].props.with as Record<string, string>).cache).toBe("pip");
|
|
348
351
|
});
|
|
349
352
|
|
|
350
353
|
test("poetry mode uses poetry cache", () => {
|
|
351
354
|
const result = PythonCI({ usePoetry: true });
|
|
352
|
-
const steps = result.testJob.
|
|
355
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
353
356
|
expect((steps[1].props.with as Record<string, string>).cache).toBe("poetry");
|
|
354
357
|
});
|
|
355
358
|
|
|
356
359
|
test("poetry mode installs poetry first", () => {
|
|
357
360
|
const result = PythonCI({ usePoetry: true });
|
|
358
|
-
const steps = result.testJob.
|
|
361
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
359
362
|
expect(steps[2].props.run).toBe("pip install poetry");
|
|
360
363
|
expect(steps[3].props.run).toBe("poetry install");
|
|
361
364
|
});
|
|
362
365
|
|
|
363
366
|
test("pip mode installs from requirements.txt", () => {
|
|
364
367
|
const result = PythonCI({});
|
|
365
|
-
const steps = result.testJob.
|
|
368
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
366
369
|
expect(steps[2].props.run).toBe("pip install -r requirements.txt");
|
|
367
370
|
});
|
|
368
371
|
|
|
369
372
|
test("uses custom requirements file", () => {
|
|
370
373
|
const result = PythonCI({ requirementsFile: "requirements-dev.txt" });
|
|
371
|
-
const steps = result.testJob.
|
|
374
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
372
375
|
expect(steps[2].props.run).toBe("pip install -r requirements-dev.txt");
|
|
373
376
|
});
|
|
374
377
|
|
|
375
378
|
test("test step uses configured command", () => {
|
|
376
379
|
const result = PythonCI({ testCommand: "python -m pytest" });
|
|
377
|
-
const steps = result.testJob.
|
|
380
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
378
381
|
const lastStep = steps[steps.length - 1];
|
|
379
|
-
expect(lastStep.
|
|
382
|
+
expect(propsOf(lastStep).run).toBe("python -m pytest");
|
|
380
383
|
});
|
|
381
384
|
|
|
382
385
|
test("lint step uses configured command", () => {
|
|
383
386
|
const result = PythonCI({ lintCommand: "flake8 ." });
|
|
384
|
-
const steps = result.lintJob
|
|
387
|
+
const steps = propsOf(result.lintJob!).steps as Array<{ props: Record<string, unknown> }>;
|
|
385
388
|
const lastStep = steps[steps.length - 1];
|
|
386
|
-
expect(lastStep.
|
|
389
|
+
expect(propsOf(lastStep).run).toBe("flake8 .");
|
|
387
390
|
});
|
|
388
391
|
|
|
389
392
|
test("uses custom python version", () => {
|
|
390
393
|
const result = PythonCI({ pythonVersion: "3.11" });
|
|
391
|
-
const steps = result.testJob.
|
|
394
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
392
395
|
expect((steps[1].props.with as Record<string, string>)["python-version"]).toBe("3.11");
|
|
393
396
|
});
|
|
394
397
|
|
|
395
398
|
test("uses custom runsOn", () => {
|
|
396
399
|
const result = PythonCI({ runsOn: "macos-latest" });
|
|
397
|
-
expect(result.testJob
|
|
400
|
+
expect(propsOf(result.testJob)["runs-on"]).toBe("macos-latest");
|
|
398
401
|
});
|
|
399
402
|
});
|
|
400
403
|
|
|
@@ -409,90 +412,90 @@ describe("DockerBuild", () => {
|
|
|
409
412
|
|
|
410
413
|
test("workflow has Docker Build name", () => {
|
|
411
414
|
const result = DockerBuild({});
|
|
412
|
-
expect(result.workflow.
|
|
415
|
+
expect(propsOf(result.workflow).name).toBe("Docker Build");
|
|
413
416
|
});
|
|
414
417
|
|
|
415
418
|
test("workflow has packages write permission", () => {
|
|
416
419
|
const result = DockerBuild({});
|
|
417
|
-
expect((result.workflow.
|
|
418
|
-
expect((result.workflow.
|
|
420
|
+
expect((propsOf(result.workflow).permissions as Record<string, string>).packages).toBe("write");
|
|
421
|
+
expect((propsOf(result.workflow).permissions as Record<string, string>).contents).toBe("read");
|
|
419
422
|
});
|
|
420
423
|
|
|
421
424
|
test("job has 5 steps", () => {
|
|
422
425
|
const result = DockerBuild({});
|
|
423
|
-
const steps = result.job.
|
|
426
|
+
const steps = propsOf(result.job).steps as unknown[];
|
|
424
427
|
expect(steps.length).toBe(5);
|
|
425
428
|
});
|
|
426
429
|
|
|
427
430
|
test("uses docker/login-action", () => {
|
|
428
431
|
const result = DockerBuild({});
|
|
429
|
-
const steps = result.job.
|
|
432
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
430
433
|
expect(steps[1].props.uses).toBe("docker/login-action@v3");
|
|
431
434
|
});
|
|
432
435
|
|
|
433
436
|
test("uses docker/setup-buildx-action", () => {
|
|
434
437
|
const result = DockerBuild({});
|
|
435
|
-
const steps = result.job.
|
|
438
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
436
439
|
expect(steps[2].props.uses).toBe("docker/setup-buildx-action@v3");
|
|
437
440
|
});
|
|
438
441
|
|
|
439
442
|
test("uses docker/metadata-action", () => {
|
|
440
443
|
const result = DockerBuild({});
|
|
441
|
-
const steps = result.job.
|
|
444
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
442
445
|
expect(steps[3].props.uses).toBe("docker/metadata-action@v5");
|
|
443
446
|
});
|
|
444
447
|
|
|
445
448
|
test("uses docker/build-push-action", () => {
|
|
446
449
|
const result = DockerBuild({});
|
|
447
|
-
const steps = result.job.
|
|
450
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
448
451
|
expect(steps[4].props.uses).toBe("docker/build-push-action@v6");
|
|
449
452
|
});
|
|
450
453
|
|
|
451
454
|
test("login step uses ghcr.io by default", () => {
|
|
452
455
|
const result = DockerBuild({});
|
|
453
|
-
const steps = result.job.
|
|
456
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
454
457
|
expect((steps[1].props.with as Record<string, string>).registry).toBe("ghcr.io");
|
|
455
458
|
});
|
|
456
459
|
|
|
457
460
|
test("uses custom registry", () => {
|
|
458
461
|
const result = DockerBuild({ registry: "docker.io" });
|
|
459
|
-
const steps = result.job.
|
|
462
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
460
463
|
expect((steps[1].props.with as Record<string, string>).registry).toBe("docker.io");
|
|
461
464
|
});
|
|
462
465
|
|
|
463
466
|
test("build-push step has push: true by default", () => {
|
|
464
467
|
const result = DockerBuild({});
|
|
465
|
-
const steps = result.job.
|
|
468
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
466
469
|
expect((steps[4].props.with as Record<string, string>).push).toBe("true");
|
|
467
470
|
});
|
|
468
471
|
|
|
469
472
|
test("build-push step uses configured dockerfile", () => {
|
|
470
473
|
const result = DockerBuild({ dockerfile: "Dockerfile.prod" });
|
|
471
|
-
const steps = result.job.
|
|
474
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
472
475
|
expect((steps[4].props.with as Record<string, string>).file).toBe("Dockerfile.prod");
|
|
473
476
|
});
|
|
474
477
|
|
|
475
478
|
test("build-push step includes platforms when specified", () => {
|
|
476
479
|
const result = DockerBuild({ platforms: ["linux/amd64", "linux/arm64"] });
|
|
477
|
-
const steps = result.job.
|
|
480
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
478
481
|
expect((steps[4].props.with as Record<string, string>).platforms).toBe("linux/amd64,linux/arm64");
|
|
479
482
|
});
|
|
480
483
|
|
|
481
484
|
test("build-push step omits platforms when not specified", () => {
|
|
482
485
|
const result = DockerBuild({});
|
|
483
|
-
const steps = result.job.
|
|
486
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
484
487
|
expect((steps[4].props.with as Record<string, string>).platforms).toBeUndefined();
|
|
485
488
|
});
|
|
486
489
|
|
|
487
490
|
test("build-push step includes build-args when specified", () => {
|
|
488
491
|
const result = DockerBuild({ buildArgs: { NODE_ENV: "production", VERSION: "1.0" } });
|
|
489
|
-
const steps = result.job.
|
|
492
|
+
const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
|
|
490
493
|
expect((steps[4].props.with as Record<string, string>)["build-args"]).toBe("NODE_ENV=production\nVERSION=1.0");
|
|
491
494
|
});
|
|
492
495
|
|
|
493
496
|
test("uses custom runsOn", () => {
|
|
494
497
|
const result = DockerBuild({ runsOn: "self-hosted" });
|
|
495
|
-
expect(result.job
|
|
498
|
+
expect(propsOf(result.job)["runs-on"]).toBe("self-hosted");
|
|
496
499
|
});
|
|
497
500
|
});
|
|
498
501
|
|
|
@@ -507,7 +510,7 @@ describe("DeployEnvironment", () => {
|
|
|
507
510
|
|
|
508
511
|
test("deployJob has environment with name", () => {
|
|
509
512
|
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
510
|
-
expect((result.deployJob.
|
|
513
|
+
expect((propsOf(result.deployJob).environment as Record<string, string>).name).toBe("staging");
|
|
511
514
|
});
|
|
512
515
|
|
|
513
516
|
test("deployJob has environment url when specified", () => {
|
|
@@ -516,12 +519,12 @@ describe("DeployEnvironment", () => {
|
|
|
516
519
|
deployScript: "npm run deploy",
|
|
517
520
|
url: "https://staging.example.com",
|
|
518
521
|
});
|
|
519
|
-
expect((result.deployJob.
|
|
522
|
+
expect((propsOf(result.deployJob).environment as Record<string, string>).url).toBe("https://staging.example.com");
|
|
520
523
|
});
|
|
521
524
|
|
|
522
525
|
test("deployJob has concurrency group", () => {
|
|
523
526
|
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
524
|
-
const concurrency = result.deployJob.
|
|
527
|
+
const concurrency = propsOf(result.deployJob).concurrency as Record<string, unknown>;
|
|
525
528
|
expect(concurrency.group).toBe("deploy-staging");
|
|
526
529
|
expect(concurrency["cancel-in-progress"]).toBe(true);
|
|
527
530
|
});
|
|
@@ -532,13 +535,13 @@ describe("DeployEnvironment", () => {
|
|
|
532
535
|
deployScript: "npm run deploy",
|
|
533
536
|
concurrencyGroup: "custom-group",
|
|
534
537
|
});
|
|
535
|
-
const concurrency = result.deployJob.
|
|
538
|
+
const concurrency = propsOf(result.deployJob).concurrency as Record<string, unknown>;
|
|
536
539
|
expect(concurrency.group).toBe("custom-group");
|
|
537
540
|
});
|
|
538
541
|
|
|
539
542
|
test("deployJob has checkout + deploy steps", () => {
|
|
540
543
|
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
541
|
-
const steps = result.deployJob.
|
|
544
|
+
const steps = propsOf(result.deployJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
542
545
|
expect(steps.length).toBe(2);
|
|
543
546
|
expect(steps[0].props.uses).toBe("actions/checkout@v7");
|
|
544
547
|
expect(steps[1].props.run).toBe("npm run deploy");
|
|
@@ -549,7 +552,7 @@ describe("DeployEnvironment", () => {
|
|
|
549
552
|
name: "staging",
|
|
550
553
|
deployScript: ["npm run build", "npm run deploy"],
|
|
551
554
|
});
|
|
552
|
-
const steps = result.deployJob.
|
|
555
|
+
const steps = propsOf(result.deployJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
553
556
|
expect(steps.length).toBe(3);
|
|
554
557
|
expect(steps[1].props.run).toBe("npm run build");
|
|
555
558
|
expect(steps[2].props.run).toBe("npm run deploy");
|
|
@@ -557,7 +560,7 @@ describe("DeployEnvironment", () => {
|
|
|
557
560
|
|
|
558
561
|
test("cleanupJob has default cleanup script", () => {
|
|
559
562
|
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
560
|
-
const steps = result.cleanupJob.
|
|
563
|
+
const steps = propsOf(result.cleanupJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
561
564
|
expect(steps[1].props.run).toBe('echo "Cleaning up..."');
|
|
562
565
|
});
|
|
563
566
|
|
|
@@ -567,13 +570,13 @@ describe("DeployEnvironment", () => {
|
|
|
567
570
|
deployScript: "npm run deploy",
|
|
568
571
|
cleanupScript: "npm run teardown",
|
|
569
572
|
});
|
|
570
|
-
const steps = result.cleanupJob.
|
|
573
|
+
const steps = propsOf(result.cleanupJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
571
574
|
expect(steps[1].props.run).toBe("npm run teardown");
|
|
572
575
|
});
|
|
573
576
|
|
|
574
577
|
test("cleanupJob has environment with name", () => {
|
|
575
578
|
const result = DeployEnvironment({ name: "production", deployScript: "npm run deploy" });
|
|
576
|
-
expect((result.cleanupJob.
|
|
579
|
+
expect((propsOf(result.cleanupJob).environment as Record<string, string>).name).toBe("production");
|
|
577
580
|
});
|
|
578
581
|
|
|
579
582
|
test("uses custom runsOn", () => {
|
|
@@ -582,8 +585,8 @@ describe("DeployEnvironment", () => {
|
|
|
582
585
|
deployScript: "npm run deploy",
|
|
583
586
|
runsOn: "self-hosted",
|
|
584
587
|
});
|
|
585
|
-
expect(result.deployJob
|
|
586
|
-
expect(result.cleanupJob
|
|
588
|
+
expect(propsOf(result.deployJob)["runs-on"]).toBe("self-hosted");
|
|
589
|
+
expect(propsOf(result.cleanupJob)["runs-on"]).toBe("self-hosted");
|
|
587
590
|
});
|
|
588
591
|
});
|
|
589
592
|
|
|
@@ -599,7 +602,7 @@ describe("GoCI", () => {
|
|
|
599
602
|
|
|
600
603
|
test("workflow has Go CI name", () => {
|
|
601
604
|
const result = GoCI({});
|
|
602
|
-
expect(result.workflow.
|
|
605
|
+
expect(propsOf(result.workflow).name).toBe("Go CI");
|
|
603
606
|
});
|
|
604
607
|
|
|
605
608
|
test("includes lintJob by default", () => {
|
|
@@ -614,63 +617,63 @@ describe("GoCI", () => {
|
|
|
614
617
|
|
|
615
618
|
test("buildJob has 3 steps (checkout, setup-go, build)", () => {
|
|
616
619
|
const result = GoCI({});
|
|
617
|
-
const steps = result.buildJob.
|
|
620
|
+
const steps = propsOf(result.buildJob).steps as unknown[];
|
|
618
621
|
expect(steps.length).toBe(3);
|
|
619
622
|
});
|
|
620
623
|
|
|
621
624
|
test("testJob has 3 steps (checkout, setup-go, test)", () => {
|
|
622
625
|
const result = GoCI({});
|
|
623
|
-
const steps = result.testJob.
|
|
626
|
+
const steps = propsOf(result.testJob).steps as unknown[];
|
|
624
627
|
expect(steps.length).toBe(3);
|
|
625
628
|
});
|
|
626
629
|
|
|
627
630
|
test("buildJob uses setup-go with default version", () => {
|
|
628
631
|
const result = GoCI({});
|
|
629
|
-
const steps = result.buildJob.
|
|
632
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
630
633
|
expect(steps[1].props.uses).toBe("actions/setup-go@v5");
|
|
631
634
|
expect((steps[1].props.with as Record<string, string>)["go-version"]).toBe("1.22");
|
|
632
635
|
});
|
|
633
636
|
|
|
634
637
|
test("uses custom go version", () => {
|
|
635
638
|
const result = GoCI({ goVersion: "1.21" });
|
|
636
|
-
const steps = result.buildJob.
|
|
639
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
637
640
|
expect((steps[1].props.with as Record<string, string>)["go-version"]).toBe("1.21");
|
|
638
641
|
});
|
|
639
642
|
|
|
640
643
|
test("buildJob runs go build by default", () => {
|
|
641
644
|
const result = GoCI({});
|
|
642
|
-
const steps = result.buildJob.
|
|
645
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
643
646
|
expect(steps[2].props.run).toBe("go build ./...");
|
|
644
647
|
});
|
|
645
648
|
|
|
646
649
|
test("testJob runs go test with race detector by default", () => {
|
|
647
650
|
const result = GoCI({});
|
|
648
|
-
const steps = result.testJob.
|
|
651
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
649
652
|
expect(steps[2].props.run).toBe("go test ./... -v -race");
|
|
650
653
|
});
|
|
651
654
|
|
|
652
655
|
test("uses custom build command", () => {
|
|
653
656
|
const result = GoCI({ buildCommand: "go build -o bin/app ./cmd/app" });
|
|
654
|
-
const steps = result.buildJob.
|
|
657
|
+
const steps = propsOf(result.buildJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
655
658
|
expect(steps[2].props.run).toBe("go build -o bin/app ./cmd/app");
|
|
656
659
|
});
|
|
657
660
|
|
|
658
661
|
test("uses custom test command", () => {
|
|
659
662
|
const result = GoCI({ testCommand: "go test -short ./..." });
|
|
660
|
-
const steps = result.testJob.
|
|
663
|
+
const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
|
|
661
664
|
expect(steps[2].props.run).toBe("go test -short ./...");
|
|
662
665
|
});
|
|
663
666
|
|
|
664
667
|
test("lintJob uses golangci-lint-action", () => {
|
|
665
668
|
const result = GoCI({});
|
|
666
|
-
const steps = result.lintJob
|
|
669
|
+
const steps = propsOf(result.lintJob!).steps as Array<{ props: Record<string, unknown> }>;
|
|
667
670
|
expect(steps[2].props.uses).toBe("golangci/golangci-lint-action@v6");
|
|
668
671
|
});
|
|
669
672
|
|
|
670
673
|
test("uses custom runsOn", () => {
|
|
671
674
|
const result = GoCI({ runsOn: "macos-latest" });
|
|
672
|
-
expect(result.buildJob
|
|
673
|
-
expect(result.testJob
|
|
675
|
+
expect(propsOf(result.buildJob)["runs-on"]).toBe("macos-latest");
|
|
676
|
+
expect(propsOf(result.testJob)["runs-on"]).toBe("macos-latest");
|
|
674
677
|
});
|
|
675
678
|
});
|
|
676
679
|
|
|
@@ -682,8 +685,8 @@ describe("defaults overrides", () => {
|
|
|
682
685
|
ref: "main",
|
|
683
686
|
defaults: { step: { id: "my-checkout" } },
|
|
684
687
|
});
|
|
685
|
-
expect(result.step.
|
|
686
|
-
expect(result.step.
|
|
688
|
+
expect(propsOf(result.step).id).toBe("my-checkout");
|
|
689
|
+
expect(propsOf(result.step).uses).toBe("actions/checkout@v7");
|
|
687
690
|
});
|
|
688
691
|
|
|
689
692
|
test("SetupNode defaults override step props", () => {
|
|
@@ -691,8 +694,8 @@ describe("defaults overrides", () => {
|
|
|
691
694
|
nodeVersion: "20",
|
|
692
695
|
defaults: { step: { id: "setup" } },
|
|
693
696
|
});
|
|
694
|
-
expect(result.step.
|
|
695
|
-
expect(result.step.
|
|
697
|
+
expect(propsOf(result.step).id).toBe("setup");
|
|
698
|
+
expect(propsOf(result.step).name).toBe("Setup Node.js");
|
|
696
699
|
});
|
|
697
700
|
|
|
698
701
|
test("NodeCI defaults override job and workflow props", () => {
|
|
@@ -702,8 +705,8 @@ describe("defaults overrides", () => {
|
|
|
702
705
|
workflow: { concurrency: "ci-${{ github.ref }}" } as any,
|
|
703
706
|
},
|
|
704
707
|
});
|
|
705
|
-
expect(result.job
|
|
706
|
-
expect(result.workflow.
|
|
708
|
+
expect(propsOf(result.job)["timeout-minutes"]).toBe(30);
|
|
709
|
+
expect(propsOf(result.workflow).concurrency).toBe("ci-${{ github.ref }}");
|
|
707
710
|
});
|
|
708
711
|
|
|
709
712
|
test("NodePipeline defaults override individual jobs", () => {
|
|
@@ -713,8 +716,8 @@ describe("defaults overrides", () => {
|
|
|
713
716
|
testJob: { "timeout-minutes": 20 } as any,
|
|
714
717
|
},
|
|
715
718
|
});
|
|
716
|
-
expect(result.buildJob
|
|
717
|
-
expect(result.testJob
|
|
719
|
+
expect(propsOf(result.buildJob)["timeout-minutes"]).toBe(15);
|
|
720
|
+
expect(propsOf(result.testJob)["timeout-minutes"]).toBe(20);
|
|
718
721
|
});
|
|
719
722
|
|
|
720
723
|
test("DockerBuild defaults override job and workflow", () => {
|
|
@@ -724,8 +727,8 @@ describe("defaults overrides", () => {
|
|
|
724
727
|
workflow: { concurrency: "docker" } as any,
|
|
725
728
|
},
|
|
726
729
|
});
|
|
727
|
-
expect(result.job
|
|
728
|
-
expect(result.workflow.
|
|
730
|
+
expect(propsOf(result.job)["timeout-minutes"]).toBe(60);
|
|
731
|
+
expect(propsOf(result.workflow).concurrency).toBe("docker");
|
|
729
732
|
});
|
|
730
733
|
|
|
731
734
|
test("DeployEnvironment defaults override deploy and cleanup jobs", () => {
|
|
@@ -737,8 +740,8 @@ describe("defaults overrides", () => {
|
|
|
737
740
|
cleanupJob: { "timeout-minutes": 10 } as any,
|
|
738
741
|
},
|
|
739
742
|
});
|
|
740
|
-
expect(result.deployJob
|
|
741
|
-
expect(result.cleanupJob
|
|
743
|
+
expect(propsOf(result.deployJob)["timeout-minutes"]).toBe(30);
|
|
744
|
+
expect(propsOf(result.cleanupJob)["timeout-minutes"]).toBe(10);
|
|
742
745
|
});
|
|
743
746
|
|
|
744
747
|
test("GoCI defaults override workflow name", () => {
|
|
@@ -747,7 +750,7 @@ describe("defaults overrides", () => {
|
|
|
747
750
|
workflow: { name: "Custom Go CI" } as any,
|
|
748
751
|
},
|
|
749
752
|
});
|
|
750
|
-
expect(result.workflow.
|
|
753
|
+
expect(propsOf(result.workflow).name).toBe("Custom Go CI");
|
|
751
754
|
});
|
|
752
755
|
|
|
753
756
|
test("PythonCI defaults override testJob", () => {
|
|
@@ -756,7 +759,7 @@ describe("defaults overrides", () => {
|
|
|
756
759
|
testJob: { "timeout-minutes": 45 } as any,
|
|
757
760
|
},
|
|
758
761
|
});
|
|
759
|
-
expect(result.testJob
|
|
762
|
+
expect(propsOf(result.testJob)["timeout-minutes"]).toBe(45);
|
|
760
763
|
});
|
|
761
764
|
|
|
762
765
|
test("UploadArtifact defaults override step props", () => {
|
|
@@ -765,8 +768,8 @@ describe("defaults overrides", () => {
|
|
|
765
768
|
path: "dist/",
|
|
766
769
|
defaults: { step: { id: "upload" } },
|
|
767
770
|
});
|
|
768
|
-
expect(result.step.
|
|
769
|
-
expect(result.step.
|
|
771
|
+
expect(propsOf(result.step).id).toBe("upload");
|
|
772
|
+
expect(propsOf(result.step).uses).toBe("actions/upload-artifact@v4");
|
|
770
773
|
});
|
|
771
774
|
|
|
772
775
|
test("CacheAction defaults override step props", () => {
|
|
@@ -775,7 +778,7 @@ describe("defaults overrides", () => {
|
|
|
775
778
|
key: "cache-key",
|
|
776
779
|
defaults: { step: { id: "cache-step" } },
|
|
777
780
|
});
|
|
778
|
-
expect(result.step.
|
|
779
|
-
expect(result.step.
|
|
781
|
+
expect(propsOf(result.step).id).toBe("cache-step");
|
|
782
|
+
expect(propsOf(result.step).uses).toBe("actions/cache@v4");
|
|
780
783
|
});
|
|
781
784
|
});
|