@intentius/chant-lexicon-gitlab 0.1.11 → 0.1.14

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.
Files changed (82) hide show
  1. package/README.md +4 -0
  2. package/dist/integrity.json +3 -2
  3. package/dist/manifest.json +1 -1
  4. package/dist/skills/chant-gitlab-migrate.md +117 -0
  5. package/package.json +11 -4
  6. package/src/import/generator.ts +20 -2
  7. package/src/migrate/from-github/actions/index.ts +27 -0
  8. package/src/migrate/from-github/actions/registry.ts +112 -0
  9. package/src/migrate/from-github/actions/tier-1.test.ts +128 -0
  10. package/src/migrate/from-github/actions/tier-1.ts +325 -0
  11. package/src/migrate/from-github/actions/tier-2-3.test.ts +144 -0
  12. package/src/migrate/from-github/actions/tier-2.ts +296 -0
  13. package/src/migrate/from-github/actions/tier-3.ts +124 -0
  14. package/src/migrate/from-github/composites/patterns.ts +167 -0
  15. package/src/migrate/from-github/composites/rewriter.test.ts +98 -0
  16. package/src/migrate/from-github/composites/rewriter.ts +29 -0
  17. package/src/migrate/from-github/diagnostics.ts +45 -0
  18. package/src/migrate/from-github/emit-ts.test.ts +49 -0
  19. package/src/migrate/from-github/emit-yaml.ts +128 -0
  20. package/src/migrate/from-github/expressions.test.ts +124 -0
  21. package/src/migrate/from-github/expressions.ts +302 -0
  22. package/src/migrate/from-github/fixtures/README.md +27 -0
  23. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected-report.json +15 -0
  24. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected.gitlab-ci.yml +13 -0
  25. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/input.yml +7 -0
  26. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected-report.json +20 -0
  27. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected.gitlab-ci.yml +20 -0
  28. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/input.yml +12 -0
  29. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected-report.json +20 -0
  30. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected.gitlab-ci.yml +17 -0
  31. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/input.yml +12 -0
  32. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected-report.json +24 -0
  33. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected.gitlab-ci.yml +20 -0
  34. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/input.yml +16 -0
  35. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected-report.json +24 -0
  36. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected.gitlab-ci.yml +27 -0
  37. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/input.yml +20 -0
  38. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected-report.json +24 -0
  39. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected.gitlab-ci.yml +15 -0
  40. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/input.yml +13 -0
  41. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected-report.json +20 -0
  42. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected.gitlab-ci.yml +17 -0
  43. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/input.yml +11 -0
  44. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected-report.json +21 -0
  45. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected.gitlab-ci.yml +15 -0
  46. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/input.yml +11 -0
  47. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected-report.json +20 -0
  48. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected.gitlab-ci.yml +16 -0
  49. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/input.yml +12 -0
  50. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected-report.json +13 -0
  51. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected.gitlab-ci.yml +31 -0
  52. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/input.yml +16 -0
  53. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected-report.json +13 -0
  54. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected.gitlab-ci.yml +20 -0
  55. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/input.yml +10 -0
  56. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected-report.json +13 -0
  57. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected.gitlab-ci.yml +18 -0
  58. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/input.yml +11 -0
  59. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected-report.json +13 -0
  60. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected.gitlab-ci.yml +24 -0
  61. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/input.yml +12 -0
  62. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected-report.json +13 -0
  63. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected.gitlab-ci.yml +18 -0
  64. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/input.yml +13 -0
  65. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected-report.json +20 -0
  66. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected.gitlab-ci.yml +17 -0
  67. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/input.yml +13 -0
  68. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected-report.json +13 -0
  69. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected.gitlab-ci.yml +14 -0
  70. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/input.yml +7 -0
  71. package/src/migrate/from-github/fixtures.test.ts +92 -0
  72. package/src/migrate/from-github/index.ts +128 -0
  73. package/src/migrate/from-github/provenance.ts +68 -0
  74. package/src/migrate/from-github/rules.ts +82 -0
  75. package/src/migrate/from-github/stages.test.ts +99 -0
  76. package/src/migrate/from-github/stages.ts +177 -0
  77. package/src/migrate/from-github/transformer.test.ts +278 -0
  78. package/src/migrate/from-github/transformer.ts +719 -0
  79. package/src/migrate.mcp.test.ts +69 -0
  80. package/src/plugin.test.ts +7 -3
  81. package/src/plugin.ts +105 -1
  82. package/src/skills/chant-gitlab-migrate.md +117 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Tier 1 marketplace action mappings — the 14 most-used GitHub Actions
3
+ * marketplace actions, per the upstream skill's `references/marketplace-actions.md`.
4
+ *
5
+ * Each mapping returns:
6
+ * - scriptLines: appended to job.script
7
+ * - image: job-level image override (last write wins)
8
+ * - services: docker:dind etc.
9
+ * - cache / artifacts: native GitLab keywords
10
+ * - provenance: per-step records
11
+ */
12
+
13
+ import type { ActionMapping, ActionMappedResult, ActionMapCtx } from "./registry";
14
+ import { getDefaultRegistry } from "./registry";
15
+
16
+ const prov = (
17
+ ctx: ActionMapCtx,
18
+ actionName: string,
19
+ tier: 1 | 2 | 3,
20
+ note: string,
21
+ category: "literal" | "needs-review" | "skipped" | "action-map" = "action-map",
22
+ ) => ({
23
+ gitlabPath: `jobs.${ctx.logicalId}.script`,
24
+ gitlabLogicalId: ctx.logicalId,
25
+ sourceKey: `jobs.${ctx.jobName}.steps[${ctx.stepIndex}].uses`,
26
+ sourceFile: ctx.sourceFile,
27
+ category,
28
+ rule: `ACT-${actionName.replace(/[\/-]/g, "-")}`,
29
+ note,
30
+ actionRef: actionName,
31
+ mappingTier: tier,
32
+ });
33
+
34
+ function withVal<T>(o: Record<string, unknown> | undefined, key: string): T | undefined {
35
+ if (!o) return undefined;
36
+ return o[key] as T | undefined;
37
+ }
38
+
39
+ function getWith(step: Record<string, unknown>): Record<string, unknown> {
40
+ return (step.with as Record<string, unknown>) ?? {};
41
+ }
42
+
43
+ // actions/checkout: GitLab clones automatically. Emit no script lines.
44
+ const actionsCheckout: ActionMapping = {
45
+ actionName: "actions/checkout",
46
+ tier: 1,
47
+ translate(step, ctx): ActionMappedResult {
48
+ const w = getWith(step);
49
+ const variables: Record<string, unknown> = {};
50
+ if (w["fetch-depth"] !== undefined) {
51
+ variables.GIT_DEPTH = w["fetch-depth"];
52
+ }
53
+ if (w.submodules === true || w.submodules === "true" || w.submodules === "recursive") {
54
+ variables.GIT_SUBMODULE_STRATEGY = w.submodules === "recursive" ? "recursive" : "normal";
55
+ }
56
+ return {
57
+ scriptLines: [],
58
+ variables: Object.keys(variables).length > 0 ? variables : undefined,
59
+ provenance: [prov(ctx, "actions/checkout", 1, "Removed — GitLab clones the repository automatically", "skipped")],
60
+ };
61
+ },
62
+ };
63
+
64
+ // actions/setup-node: image: node:<version>
65
+ const actionsSetupNode: ActionMapping = {
66
+ actionName: "actions/setup-node",
67
+ tier: 1,
68
+ translate(step, ctx): ActionMappedResult {
69
+ const w = getWith(step);
70
+ const version = (w["node-version"] as string) ?? "22";
71
+ const cache = w.cache as string | undefined;
72
+ const cacheKey: Record<string, unknown> | undefined = cache === "npm"
73
+ ? { key: { files: ["package-lock.json"] }, paths: [".npm/"] }
74
+ : cache === "yarn"
75
+ ? { key: { files: ["yarn.lock"] }, paths: [".yarn/cache/"] }
76
+ : cache === "pnpm"
77
+ ? { key: { files: ["pnpm-lock.yaml"] }, paths: [".pnpm-store/"] }
78
+ : undefined;
79
+ return {
80
+ scriptLines: [],
81
+ image: `node:${version}`,
82
+ cache: cacheKey,
83
+ provenance: [prov(ctx, "actions/setup-node", 1, `setup-node → image: node:${version}`)],
84
+ };
85
+ },
86
+ };
87
+
88
+ const actionsSetupPython: ActionMapping = {
89
+ actionName: "actions/setup-python",
90
+ tier: 1,
91
+ translate(step, ctx): ActionMappedResult {
92
+ const w = getWith(step);
93
+ const version = (w["python-version"] as string) ?? "3.12";
94
+ const cache = w.cache as string | undefined;
95
+ const cacheKey: Record<string, unknown> | undefined = cache === "pip"
96
+ ? { paths: [".cache/pip/"] }
97
+ : cache === "poetry"
98
+ ? { paths: [".cache/pypoetry/"] }
99
+ : undefined;
100
+ return {
101
+ scriptLines: [],
102
+ image: `python:${version}`,
103
+ cache: cacheKey,
104
+ provenance: [prov(ctx, "actions/setup-python", 1, `setup-python → image: python:${version}`)],
105
+ };
106
+ },
107
+ };
108
+
109
+ const actionsSetupJava: ActionMapping = {
110
+ actionName: "actions/setup-java",
111
+ tier: 1,
112
+ translate(step, ctx): ActionMappedResult {
113
+ const w = getWith(step);
114
+ const version = (w["java-version"] as string) ?? "17";
115
+ const cache = w.cache as string | undefined;
116
+ const cacheKey: Record<string, unknown> | undefined = cache === "maven"
117
+ ? { paths: [".m2/repository/"] }
118
+ : cache === "gradle"
119
+ ? { paths: [".gradle/"] }
120
+ : undefined;
121
+ return {
122
+ scriptLines: [],
123
+ image: `eclipse-temurin:${version}`,
124
+ cache: cacheKey,
125
+ provenance: [prov(ctx, "actions/setup-java", 1, `setup-java → image: eclipse-temurin:${version}`)],
126
+ };
127
+ },
128
+ };
129
+
130
+ const actionsSetupGo: ActionMapping = {
131
+ actionName: "actions/setup-go",
132
+ tier: 1,
133
+ translate(step, ctx): ActionMappedResult {
134
+ const w = getWith(step);
135
+ const version = (w["go-version"] as string) ?? "1.22";
136
+ return {
137
+ scriptLines: [],
138
+ image: `golang:${version}`,
139
+ cache: w.cache === true || w.cache === "true"
140
+ ? { paths: [".cache/go-build/", "go/pkg/mod/"] }
141
+ : undefined,
142
+ provenance: [prov(ctx, "actions/setup-go", 1, `setup-go → image: golang:${version}`)],
143
+ };
144
+ },
145
+ };
146
+
147
+ const actionsSetupRuby: ActionMapping = {
148
+ actionName: "actions/setup-ruby",
149
+ tier: 1,
150
+ translate(step, ctx): ActionMappedResult {
151
+ const w = getWith(step);
152
+ const version = (w["ruby-version"] as string) ?? "3.3";
153
+ return {
154
+ scriptLines: [],
155
+ image: `ruby:${version}`,
156
+ cache: w["bundler-cache"] === true || w["bundler-cache"] === "true"
157
+ ? { paths: ["vendor/bundle/"] }
158
+ : undefined,
159
+ provenance: [prov(ctx, "actions/setup-ruby", 1, `setup-ruby → image: ruby:${version}`)],
160
+ };
161
+ },
162
+ };
163
+
164
+ const actionsCache: ActionMapping = {
165
+ actionName: "actions/cache",
166
+ tier: 1,
167
+ translate(step, ctx): ActionMappedResult {
168
+ const w = getWith(step);
169
+ const paths = (w.path as string)?.split("\n").map((p) => p.trim()).filter(Boolean) ?? [];
170
+ const key = w.key as string | undefined;
171
+ const cache: Record<string, unknown> = {};
172
+ if (paths.length > 0) cache.paths = paths;
173
+ if (key) cache.key = key;
174
+ return {
175
+ scriptLines: [],
176
+ cache: Object.keys(cache).length > 0 ? cache : undefined,
177
+ provenance: [prov(ctx, "actions/cache", 1, "actions/cache → native cache: keyword")],
178
+ };
179
+ },
180
+ };
181
+
182
+ const actionsUploadArtifact: ActionMapping = {
183
+ actionName: "actions/upload-artifact",
184
+ tier: 1,
185
+ translate(step, ctx): ActionMappedResult {
186
+ const w = getWith(step);
187
+ const paths = (w.path as string)?.split("\n").map((p) => p.trim()).filter(Boolean) ?? [];
188
+ const name = w.name as string | undefined;
189
+ const retention = w["retention-days"] as number | string | undefined;
190
+ const artifacts: Record<string, unknown> = {};
191
+ if (paths.length > 0) artifacts.paths = paths;
192
+ if (name) artifacts.name = name;
193
+ if (retention !== undefined) artifacts.expire_in = `${retention} days`;
194
+ return {
195
+ scriptLines: [],
196
+ artifacts: Object.keys(artifacts).length > 0 ? artifacts : undefined,
197
+ provenance: [prov(ctx, "actions/upload-artifact", 1, "upload-artifact → native artifacts: keyword")],
198
+ };
199
+ },
200
+ };
201
+
202
+ const actionsDownloadArtifact: ActionMapping = {
203
+ actionName: "actions/download-artifact",
204
+ tier: 1,
205
+ translate(_step, ctx): ActionMappedResult {
206
+ return {
207
+ scriptLines: [],
208
+ provenance: [prov(ctx, "actions/download-artifact", 1, "download-artifact removed — GitLab auto-passes artifacts via stages or needs:artifacts:")],
209
+ };
210
+ },
211
+ };
212
+
213
+ const dockerLoginAction: ActionMapping = {
214
+ actionName: "docker/login-action",
215
+ tier: 1,
216
+ translate(step, ctx): ActionMappedResult {
217
+ const w = getWith(step);
218
+ const registry = (w.registry as string) ?? "";
219
+ const userVar = withVal<string>(w, "username") ?? "$CI_REGISTRY_USER";
220
+ const passVar = withVal<string>(w, "password") ?? "$CI_REGISTRY_PASSWORD";
221
+ return {
222
+ scriptLines: [`docker login -u ${userVar} -p ${passVar} ${registry}`.trim()],
223
+ provenance: [prov(ctx, "docker/login-action", 1, "docker/login-action → inline docker login")],
224
+ };
225
+ },
226
+ };
227
+
228
+ const dockerBuildPushAction: ActionMapping = {
229
+ actionName: "docker/build-push-action",
230
+ tier: 1,
231
+ translate(step, ctx): ActionMappedResult {
232
+ const w = getWith(step);
233
+ const context = (w.context as string) ?? ".";
234
+ const file = (w.file as string) ?? "Dockerfile";
235
+ const push = w.push === true || w.push === "true";
236
+ const tags = (w.tags as string)?.split(/[\n,]/).map((t) => t.trim()).filter(Boolean) ?? [];
237
+ const lines: string[] = [];
238
+ const tagFlags = tags.map((t) => `-t ${t}`).join(" ");
239
+ lines.push(`docker build -f ${file} ${tagFlags} ${context}`.trim());
240
+ if (push) {
241
+ for (const t of tags) lines.push(`docker push ${t}`);
242
+ }
243
+ return {
244
+ scriptLines: lines,
245
+ image: "docker:latest",
246
+ services: [{ name: "docker:dind", alias: "docker", variables: { DOCKER_TLS_CERTDIR: "/certs" } }],
247
+ provenance: [prov(ctx, "docker/build-push-action", 1, "docker/build-push-action → inline docker build/push + docker:dind service")],
248
+ };
249
+ },
250
+ };
251
+
252
+ const dockerSetupBuildxAction: ActionMapping = {
253
+ actionName: "docker/setup-buildx-action",
254
+ tier: 1,
255
+ translate(_step, ctx): ActionMappedResult {
256
+ return {
257
+ scriptLines: ["docker buildx create --use"],
258
+ image: "docker:latest",
259
+ services: [{ name: "docker:dind", alias: "docker", variables: { DOCKER_TLS_CERTDIR: "/certs" } }],
260
+ provenance: [prov(ctx, "docker/setup-buildx-action", 1, "docker/setup-buildx-action → docker buildx create --use + docker:dind")],
261
+ };
262
+ },
263
+ };
264
+
265
+ const dockerSetupQemuAction: ActionMapping = {
266
+ actionName: "docker/setup-qemu-action",
267
+ tier: 1,
268
+ translate(step, ctx): ActionMappedResult {
269
+ const w = getWith(step);
270
+ const platforms = (w.platforms as string) ?? "linux/amd64,linux/arm64";
271
+ return {
272
+ scriptLines: [`docker run --rm --privileged tonistiigi/binfmt --install ${platforms}`],
273
+ provenance: [prov(ctx, "docker/setup-qemu-action", 1, "docker/setup-qemu-action → tonistiigi/binfmt")],
274
+ };
275
+ },
276
+ };
277
+
278
+ const actionsGithubScript: ActionMapping = {
279
+ actionName: "actions/github-script",
280
+ tier: 1,
281
+ translate(_step, ctx): ActionMappedResult {
282
+ return {
283
+ scriptLines: [
284
+ "# TODO(migration): actions/github-script has no direct GitLab equivalent.",
285
+ "# Replace with glab CLI or curl against the GitLab API.",
286
+ ],
287
+ provenance: [prov(
288
+ ctx,
289
+ "actions/github-script",
290
+ 1,
291
+ "actions/github-script: use glab CLI or REST API",
292
+ "needs-review",
293
+ )],
294
+ };
295
+ },
296
+ };
297
+
298
+ const TIER_1_MAPPINGS: ActionMapping[] = [
299
+ actionsCheckout,
300
+ actionsSetupNode,
301
+ actionsSetupPython,
302
+ actionsSetupJava,
303
+ actionsSetupGo,
304
+ actionsSetupRuby,
305
+ actionsCache,
306
+ actionsUploadArtifact,
307
+ actionsDownloadArtifact,
308
+ dockerLoginAction,
309
+ dockerBuildPushAction,
310
+ dockerSetupBuildxAction,
311
+ dockerSetupQemuAction,
312
+ actionsGithubScript,
313
+ ];
314
+
315
+ /**
316
+ * Register all Tier 1 mappings into the given registry (or default).
317
+ * Idempotent — call before `lookupAction` is first invoked.
318
+ */
319
+ export function registerTier1(registry = getDefaultRegistry()): void {
320
+ for (const m of TIER_1_MAPPINGS) {
321
+ registry.register(m);
322
+ }
323
+ }
324
+
325
+ export { TIER_1_MAPPINGS };
@@ -0,0 +1,144 @@
1
+ import { describe, test, expect, beforeAll } from "vitest";
2
+ import { createRegistry, lookupAction } from "./registry";
3
+ import { registerTier2 } from "./tier-2";
4
+ import { registerTier3 } from "./tier-3";
5
+ import type { ActionMapCtx } from "./registry";
6
+
7
+ const ctx: ActionMapCtx = { logicalId: "job", jobName: "job", stepIndex: 0 };
8
+ const reg = createRegistry();
9
+ beforeAll(() => {
10
+ registerTier2(reg);
11
+ registerTier3(reg);
12
+ });
13
+
14
+ function call(uses: string, w: Record<string, unknown> = {}) {
15
+ const m = lookupAction(uses, reg);
16
+ if (!m) throw new Error(`No mapping: ${uses}`);
17
+ return m.translate({ uses, with: w }, ctx);
18
+ }
19
+
20
+ describe("Tier 2 mappings", () => {
21
+ test("actions/setup-dotnet → mcr.microsoft.com/dotnet/sdk", () => {
22
+ const r = call("actions/setup-dotnet@v4", { "dotnet-version": "8.0" });
23
+ expect(r.image).toBe("mcr.microsoft.com/dotnet/sdk:8.0");
24
+ });
25
+
26
+ test("shivammathur/setup-php → php image + before_script", () => {
27
+ const r = call("shivammathur/setup-php@v2", { "php-version": "8.2", extensions: "mbstring,intl" });
28
+ expect(r.image).toBe("php:8.2");
29
+ expect(r.beforeScript?.join("\n")).toContain("docker-php-ext-install");
30
+ });
31
+
32
+ test("aws-actions/configure-aws-credentials → needs-review + variables", () => {
33
+ const r = call("aws-actions/configure-aws-credentials@v4", { "aws-region": "us-west-2" });
34
+ expect(r.variables?.AWS_DEFAULT_REGION).toBe("us-west-2");
35
+ expect(r.provenance[0].category).toBe("needs-review");
36
+ });
37
+
38
+ test("google-github-actions/auth emits base64 decode", () => {
39
+ const r = call("google-github-actions/auth@v2");
40
+ expect(r.scriptLines.some((l) => l.includes("base64 -d"))).toBe(true);
41
+ expect(r.variables?.GOOGLE_APPLICATION_CREDENTIALS).toBeDefined();
42
+ });
43
+
44
+ test("azure/login → az login service-principal", () => {
45
+ const r = call("azure/login@v2");
46
+ expect(r.scriptLines.some((l) => l.includes("az login"))).toBe(true);
47
+ });
48
+
49
+ test("hashicorp/setup-terraform → hashicorp/terraform image", () => {
50
+ const r = call("hashicorp/setup-terraform@v3", { terraform_version: "1.6" });
51
+ expect(r.image).toBe("hashicorp/terraform:1.6");
52
+ });
53
+
54
+ test("codecov/codecov-action → codecov-cli script", () => {
55
+ const r = call("codecov/codecov-action@v4", { files: "coverage.xml" });
56
+ expect(r.scriptLines.some((l) => l.includes("codecov-cli"))).toBe(true);
57
+ expect(r.scriptLines.some((l) => l.includes("coverage.xml"))).toBe(true);
58
+ });
59
+
60
+ test("softprops/action-gh-release → glab release create", () => {
61
+ const r = call("softprops/action-gh-release@v2");
62
+ expect(r.scriptLines.some((l) => l.includes("glab release"))).toBe(true);
63
+ });
64
+
65
+ test("peter-evans/create-pull-request → git commit + glab mr", () => {
66
+ const r = call("peter-evans/create-pull-request@v6", { title: "Update deps" });
67
+ expect(r.scriptLines.some((l) => l.includes("glab mr create"))).toBe(true);
68
+ expect(r.scriptLines.some((l) => l.includes("Update deps"))).toBe(true);
69
+ });
70
+
71
+ test("JamesIves/github-pages-deploy-action → public/ artifact", () => {
72
+ const r = call("JamesIves/github-pages-deploy-action@v4", { folder: "dist" });
73
+ expect(r.artifacts?.paths).toEqual(["public"]);
74
+ expect(r.scriptLines.some((l) => l.includes("cp -r dist"))).toBe(true);
75
+ });
76
+
77
+ test("pnpm/action-setup → npm install -g pnpm", () => {
78
+ const r = call("pnpm/action-setup@v4", { version: "9" });
79
+ expect(r.scriptLines[0]).toContain("pnpm@9");
80
+ });
81
+
82
+ test("oven-sh/setup-bun → oven/bun image", () => {
83
+ const r = call("oven-sh/setup-bun@v2", { "bun-version": "1.1.0" });
84
+ expect(r.image).toBe("oven/bun:1.1.0");
85
+ });
86
+
87
+ test("gradle/gradle-build-action → gradle image", () => {
88
+ const r = call("gradle/gradle-build-action@v3", { "gradle-version": "8" });
89
+ expect(r.image).toBe("gradle:8");
90
+ });
91
+
92
+ test("cypress-io/github-action → cypress/browsers image + npx cypress run", () => {
93
+ const r = call("cypress-io/github-action@v6", { browser: "firefox", start: "npm start" });
94
+ expect(r.image).toBe("cypress/browsers:latest");
95
+ expect(r.scriptLines.some((l) => l.includes("npx cypress run --browser firefox"))).toBe(true);
96
+ });
97
+ });
98
+
99
+ describe("Tier 3 mappings", () => {
100
+ test("tj-actions/changed-files → git diff", () => {
101
+ const r = call("tj-actions/changed-files@v44", { files: "src/**" });
102
+ expect(r.scriptLines[0]).toContain("git diff --name-only");
103
+ });
104
+
105
+ test("dorny/paths-filter → needs-review (native rules:changes)", () => {
106
+ const r = call("dorny/paths-filter@v3");
107
+ expect(r.provenance[0].category).toBe("needs-review");
108
+ expect(r.scriptLines.some((l) => l.includes("rules:changes"))).toBe(true);
109
+ });
110
+
111
+ test("nick-fields/retry → needs-review (native retry:)", () => {
112
+ const r = call("nick-fields/retry@v3", { command: "npm test" });
113
+ expect(r.provenance[0].category).toBe("needs-review");
114
+ expect(r.scriptLines).toContain("npm test");
115
+ });
116
+
117
+ test("pre-commit/action → pip install + pre-commit run", () => {
118
+ const r = call("pre-commit/action@v3");
119
+ expect(r.scriptLines.some((l) => l.includes("pip install pre-commit"))).toBe(true);
120
+ expect(r.scriptLines.some((l) => l.includes("pre-commit run"))).toBe(true);
121
+ });
122
+
123
+ test("slackapi/slack-github-action → curl webhook", () => {
124
+ const r = call("slackapi/slack-github-action@v1", { payload: '{"text":"hi"}' });
125
+ expect(r.scriptLines.some((l) => l.includes("$SLACK_WEBHOOK_URL"))).toBe(true);
126
+ });
127
+
128
+ test("registry covers 5 Tier 3 + 14 Tier 2 = 19 actions", () => {
129
+ const names = [
130
+ "actions/setup-dotnet", "shivammathur/setup-php",
131
+ "aws-actions/configure-aws-credentials", "google-github-actions/auth",
132
+ "azure/login", "hashicorp/setup-terraform", "codecov/codecov-action",
133
+ "softprops/action-gh-release", "peter-evans/create-pull-request",
134
+ "JamesIves/github-pages-deploy-action", "pnpm/action-setup",
135
+ "oven-sh/setup-bun", "gradle/gradle-build-action", "cypress-io/github-action",
136
+ "tj-actions/changed-files", "dorny/paths-filter", "nick-fields/retry",
137
+ "pre-commit/action", "slackapi/slack-github-action",
138
+ ];
139
+ expect(names).toHaveLength(19);
140
+ for (const n of names) {
141
+ expect(lookupAction(`${n}@v1`, reg)).toBeDefined();
142
+ }
143
+ });
144
+ });