@intentius/chant-lexicon-github 0.106.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "86245b91fbd6f3569b7e78ca9b9d4241f4709598483c4b73a55627a46b2c72fd",
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": "4970f8db5a1ce849770c89a6393a48f46854cc626fc8e458a21afe9c0567e9f3"
81
+ "composite": "3b71f70f79d394ab86c7cd10ffc18228599568f05f9588335786c50ce73dc3a2"
82
82
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.106.1",
3
+ "version": "0.106.3",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitHub",
6
6
  "intrinsics": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-github",
3
- "version": "0.106.1",
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.106.1",
64
+ "@intentius/chant": "^0.106.3",
65
65
  "typescript": "^5.9.3"
66
66
  }
67
67
  }
@@ -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-api-staging");
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", () => {
@@ -257,7 +257,7 @@ function prLoopGithubDoc(
257
257
  },
258
258
  };
259
259
  return {
260
- name: member ? `chant-pr-${member.name}-${env}` : `chant-pr-${env}`,
260
+ name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
261
261
  environment: env,
262
262
  on: { pull_request: { branches: [branch] }, push: { branches: [branch] } },
263
263
  ...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
@@ -534,7 +534,7 @@ function scopeToMember(doc: GithubPipelineDoc, member: PipelineMember): GithubPi
534
534
  }
535
535
  return {
536
536
  ...doc,
537
- name: `chant-components-${member.name}-${doc.environment}`,
537
+ name: `chant-components.${member.name}.${doc.environment}`,
538
538
  ...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
539
539
  jobsDoc,
540
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[] = [
@@ -117,7 +118,7 @@ describe("the GitHub pull-request workflow", () => {
117
118
  });
118
119
 
119
120
  test("inside a workspace member: runs in the member's directory and keeps its own gate, note and apply group (#3465)", () => {
120
- const member = { name: "network", dir: "infra/network", file: ".github/workflows/chant-pr-network-prod.yml" };
121
+ const member = { name: "network", dir: "infra/network", file: ".github/workflows/chant-pr.network.prod.yml" };
121
122
  const scoped = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member });
122
123
  const mdoc = parseYAML(scoped.yaml) as {
123
124
  name: string;
@@ -125,7 +126,7 @@ describe("the GitHub pull-request workflow", () => {
125
126
  defaults?: { run: Record<string, string> };
126
127
  jobs: Record<string, Job>;
127
128
  };
128
- expect(mdoc.name).toBe("chant-pr-network-prod");
129
+ expect(mdoc.name).toBe("chant-pr.network.prod");
129
130
  // No path filter: a change outside the member can reach it.
130
131
  expect(mdoc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] } });
131
132
  expect(mdoc.defaults).toEqual({ run: { "working-directory": "infra/network" } });
@@ -133,21 +134,31 @@ describe("the GitHub pull-request workflow", () => {
133
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');
134
135
  const apply = mdoc.jobs.apply.steps.find((s) => s.run?.includes("pr-apply"))!;
135
136
  expect(apply.run).toContain("--member network");
136
- expect(mdoc.jobs.apply.concurrency).toEqual({ group: "chant-apply-network-prod", "cancel-in-progress": false });
137
+ expect(mdoc.jobs.apply.concurrency).toEqual({ group: "chant-apply.network.prod", "cancel-in-progress": false });
137
138
  // The record path is relative to the member for --resume and to the repository for the cache.
138
139
  expect(apply.run).toContain("--resume .chant/pr-resume/pr-apply.json");
139
140
  const restore = mdoc.jobs.apply.steps.find((s) => s.uses === "actions/cache/restore@v4")!;
140
- expect(restore.with).toMatchObject({ path: "infra/network/.chant/pr-resume/pr-apply.json", "restore-keys": "chant-apply-network-prod-${{ github.sha }}-" });
141
+ expect(restore.with).toMatchObject({ path: "infra/network/.chant/pr-resume/pr-apply.json", "restore-keys": "chant-apply.network.prod-${{ github.sha }}-" });
141
142
  for (const job of Object.values(mdoc.jobs)) {
142
143
  const keep = job.steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
143
144
  expect(keep.with?.path).toBe("infra/network/.chant/pr");
144
145
  }
145
146
  });
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
+
147
158
  test("a member at the workspace root keeps the root's paths but its own names", () => {
148
159
  const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member: { name: "root", dir: "." } }).yaml;
149
160
  const mdoc = parseYAML(yaml) as { name: string; defaults?: unknown; jobs: Record<string, Job> };
150
- expect(mdoc.name).toBe("chant-pr-root-prod");
161
+ expect(mdoc.name).toBe("chant-pr.root.prod");
151
162
  expect(mdoc.defaults).toBeUndefined();
152
163
  expect(yaml).toContain("--member root");
153
164
  expect(yaml).toContain("path: .chant/pr");
@@ -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.props.uses).toBe("actions/checkout@v7");
23
- expect(result.step.props.name).toBe("Checkout");
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.props.with).toBeDefined();
29
- expect((result.step.props.with as Record<string, string>).ref).toBe("develop");
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.props.with as Record<string, string>)["fetch-depth"]).toBe("0");
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.props.with).toBeUndefined();
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.props.uses).toBe("actions/setup-node@v7");
49
- expect(result.step.props.name).toBe("Setup Node.js");
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.props.with as Record<string, string>)["node-version"]).toBe("22");
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.props.with as Record<string, string>).cache).toBe("npm");
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.props.uses).toBe("actions/setup-go@v5");
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.props.with as Record<string, string>)["go-version"]).toBe("1.22");
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.props.uses).toBe("actions/setup-python@v5");
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.props.with as Record<string, string>)["python-version"]).toBe("3.12");
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.props.uses).toBe("actions/cache@v4");
97
- expect((result.step.props.with as Record<string, string>).path).toBe("node_modules");
98
- expect((result.step.props.with as Record<string, string>).key).toBe("npm-${{ hashFiles('package-lock.json') }}");
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.props.uses).toBe("actions/upload-artifact@v4");
108
- expect((result.step.props.with as Record<string, string>).name).toBe("build");
109
- expect((result.step.props.with as Record<string, string>).path).toBe("dist/");
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.props.with as Record<string, string>)["retention-days"]).toBe("7");
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.props.uses).toBe("actions/download-artifact@v4");
124
- expect((result.step.props.with as Record<string, string>).name).toBe("build");
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.props.name).toBe("CI");
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.props["runs-on"]).toBe("ubuntu-latest");
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.props.steps)).toBe(true);
150
- expect((result.job.props.steps as unknown[]).length).toBe(5);
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.props.steps as Array<{ props: Record<string, unknown> }>;
158
+ const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
156
159
  const installStep = steps[2];
157
- expect(installStep.props.run).toBe("npm ci");
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.props.steps as Array<{ props: Record<string, unknown> }>;
165
+ const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
163
166
  const installStep = steps[2];
164
- expect(installStep.props.run).toBe("pnpm install");
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.props.steps as Array<{ props: Record<string, unknown> }>;
172
+ const steps = propsOf(result.job).steps as Array<{ props: Record<string, unknown> }>;
170
173
  const installStep = steps[2];
171
- expect(installStep.props.run).toBe("yarn install --frozen-lockfile");
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.props.name).toBe("Node Pipeline");
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.props.steps as unknown[];
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.props.steps as unknown[];
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.props.needs).toEqual(["build"]);
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props["runs-on"]).toBe("self-hosted");
261
- expect(result.testJob.props["runs-on"]).toBe("self-hosted");
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.name).toBe("Python CI");
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.props.steps as Array<{ props: Record<string, unknown> }>;
341
+ const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
339
342
  const setupStep = steps[1];
340
- expect(setupStep.props.uses).toBe("actions/setup-python@v5");
341
- expect((setupStep.props.with as Record<string, string>)["python-version"]).toBe("3.12");
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
380
+ const steps = propsOf(result.testJob).steps as Array<{ props: Record<string, unknown> }>;
378
381
  const lastStep = steps[steps.length - 1];
379
- expect(lastStep.props.run).toBe("python -m pytest");
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!.props.steps as Array<{ props: Record<string, unknown> }>;
387
+ const steps = propsOf(result.lintJob!).steps as Array<{ props: Record<string, unknown> }>;
385
388
  const lastStep = steps[steps.length - 1];
386
- expect(lastStep.props.run).toBe("flake8 .");
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props["runs-on"]).toBe("macos-latest");
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.props.name).toBe("Docker Build");
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.props.permissions as Record<string, string>).packages).toBe("write");
418
- expect((result.workflow.props.permissions as Record<string, string>).contents).toBe("read");
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.props.steps as unknown[];
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props["runs-on"]).toBe("self-hosted");
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.props.environment as Record<string, string>).name).toBe("staging");
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.props.environment as Record<string, string>).url).toBe("https://staging.example.com");
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.props.concurrency as Record<string, unknown>;
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.props.concurrency as Record<string, unknown>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.environment as Record<string, string>).name).toBe("production");
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.props["runs-on"]).toBe("self-hosted");
586
- expect(result.cleanupJob.props["runs-on"]).toBe("self-hosted");
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.props.name).toBe("Go CI");
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.props.steps as unknown[];
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.props.steps as unknown[];
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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.props.steps as Array<{ props: Record<string, unknown> }>;
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!.props.steps as Array<{ props: Record<string, unknown> }>;
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.props["runs-on"]).toBe("macos-latest");
673
- expect(result.testJob.props["runs-on"]).toBe("macos-latest");
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.props.id).toBe("my-checkout");
686
- expect(result.step.props.uses).toBe("actions/checkout@v7");
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.props.id).toBe("setup");
695
- expect(result.step.props.name).toBe("Setup Node.js");
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.props["timeout-minutes"]).toBe(30);
706
- expect(result.workflow.props.concurrency).toBe("ci-${{ github.ref }}");
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.props["timeout-minutes"]).toBe(15);
717
- expect(result.testJob.props["timeout-minutes"]).toBe(20);
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.props["timeout-minutes"]).toBe(60);
728
- expect(result.workflow.props.concurrency).toBe("docker");
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.props["timeout-minutes"]).toBe(30);
741
- expect(result.cleanupJob.props["timeout-minutes"]).toBe(10);
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.props.name).toBe("Custom Go CI");
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.props["timeout-minutes"]).toBe(45);
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.props.id).toBe("upload");
769
- expect(result.step.props.uses).toBe("actions/upload-artifact@v4");
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.props.id).toBe("cache-step");
779
- expect(result.step.props.uses).toBe("actions/cache@v4");
781
+ expect(propsOf(result.step).id).toBe("cache-step");
782
+ expect(propsOf(result.step).uses).toBe("actions/cache@v4");
780
783
  });
781
784
  });