@intentius/chant-lexicon-gitlab 0.1.12 → 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.
- package/README.md +4 -0
- package/dist/integrity.json +3 -2
- package/dist/manifest.json +1 -1
- package/dist/skills/chant-gitlab-migrate.md +117 -0
- package/package.json +11 -4
- package/src/import/generator.ts +20 -2
- package/src/migrate/from-github/actions/index.ts +27 -0
- package/src/migrate/from-github/actions/registry.ts +112 -0
- package/src/migrate/from-github/actions/tier-1.test.ts +128 -0
- package/src/migrate/from-github/actions/tier-1.ts +325 -0
- package/src/migrate/from-github/actions/tier-2-3.test.ts +144 -0
- package/src/migrate/from-github/actions/tier-2.ts +296 -0
- package/src/migrate/from-github/actions/tier-3.ts +124 -0
- package/src/migrate/from-github/composites/patterns.ts +167 -0
- package/src/migrate/from-github/composites/rewriter.test.ts +98 -0
- package/src/migrate/from-github/composites/rewriter.ts +29 -0
- package/src/migrate/from-github/diagnostics.ts +45 -0
- package/src/migrate/from-github/emit-ts.test.ts +49 -0
- package/src/migrate/from-github/emit-yaml.ts +128 -0
- package/src/migrate/from-github/expressions.test.ts +124 -0
- package/src/migrate/from-github/expressions.ts +302 -0
- package/src/migrate/from-github/fixtures/README.md +27 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected-report.json +15 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected.gitlab-ci.yml +13 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/input.yml +7 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected-report.json +20 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected.gitlab-ci.yml +20 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/input.yml +12 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected-report.json +20 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected.gitlab-ci.yml +17 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/input.yml +12 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected-report.json +24 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected.gitlab-ci.yml +20 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/input.yml +16 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected-report.json +24 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected.gitlab-ci.yml +27 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/input.yml +20 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected-report.json +24 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected.gitlab-ci.yml +15 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/input.yml +13 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected-report.json +20 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected.gitlab-ci.yml +17 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/input.yml +11 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected-report.json +21 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected.gitlab-ci.yml +15 -0
- package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/input.yml +11 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected-report.json +20 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected.gitlab-ci.yml +16 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/input.yml +12 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected-report.json +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected.gitlab-ci.yml +31 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/input.yml +16 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected-report.json +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected.gitlab-ci.yml +20 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/input.yml +10 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected-report.json +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected.gitlab-ci.yml +18 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/input.yml +11 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected-report.json +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected.gitlab-ci.yml +24 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/input.yml +12 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected-report.json +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected.gitlab-ci.yml +18 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/06-services/input.yml +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected-report.json +20 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected.gitlab-ci.yml +17 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/input.yml +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected-report.json +13 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected.gitlab-ci.yml +14 -0
- package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/input.yml +7 -0
- package/src/migrate/from-github/fixtures.test.ts +92 -0
- package/src/migrate/from-github/index.ts +128 -0
- package/src/migrate/from-github/provenance.ts +68 -0
- package/src/migrate/from-github/rules.ts +82 -0
- package/src/migrate/from-github/stages.test.ts +99 -0
- package/src/migrate/from-github/stages.ts +177 -0
- package/src/migrate/from-github/transformer.test.ts +278 -0
- package/src/migrate/from-github/transformer.ts +719 -0
- package/src/migrate.mcp.test.ts +69 -0
- package/src/plugin.test.ts +7 -3
- package/src/plugin.ts +105 -1
- package/src/skills/chant-gitlab-migrate.md +117 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 marketplace action mappings — 14 common actions from the
|
|
3
|
+
* upstream skill's `references/marketplace-actions.md`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ActionMapping, ActionMappedResult, ActionMapCtx } from "./registry";
|
|
7
|
+
import { getDefaultRegistry } from "./registry";
|
|
8
|
+
|
|
9
|
+
const prov = (
|
|
10
|
+
ctx: ActionMapCtx,
|
|
11
|
+
actionName: string,
|
|
12
|
+
note: string,
|
|
13
|
+
category: "literal" | "needs-review" | "skipped" | "action-map" = "action-map",
|
|
14
|
+
) => ({
|
|
15
|
+
gitlabPath: `jobs.${ctx.logicalId}.script`,
|
|
16
|
+
gitlabLogicalId: ctx.logicalId,
|
|
17
|
+
sourceKey: `jobs.${ctx.jobName}.steps[${ctx.stepIndex}].uses`,
|
|
18
|
+
sourceFile: ctx.sourceFile,
|
|
19
|
+
category,
|
|
20
|
+
rule: `ACT-${actionName.replace(/[\/-]/g, "-")}`,
|
|
21
|
+
note,
|
|
22
|
+
actionRef: actionName,
|
|
23
|
+
mappingTier: 2 as const,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const getWith = (s: Record<string, unknown>): Record<string, unknown> =>
|
|
27
|
+
(s.with as Record<string, unknown>) ?? {};
|
|
28
|
+
|
|
29
|
+
const actionsSetupDotnet: ActionMapping = {
|
|
30
|
+
actionName: "actions/setup-dotnet",
|
|
31
|
+
tier: 2,
|
|
32
|
+
translate(step, ctx): ActionMappedResult {
|
|
33
|
+
const w = getWith(step);
|
|
34
|
+
const version = (w["dotnet-version"] as string) ?? "8.0";
|
|
35
|
+
return {
|
|
36
|
+
scriptLines: [],
|
|
37
|
+
image: `mcr.microsoft.com/dotnet/sdk:${version}`,
|
|
38
|
+
cache: { paths: [".nuget/packages/"] },
|
|
39
|
+
provenance: [prov(ctx, "actions/setup-dotnet", `setup-dotnet → image: mcr.microsoft.com/dotnet/sdk:${version}`)],
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const shivammathurSetupPhp: ActionMapping = {
|
|
45
|
+
actionName: "shivammathur/setup-php",
|
|
46
|
+
tier: 2,
|
|
47
|
+
translate(step, ctx): ActionMappedResult {
|
|
48
|
+
const w = getWith(step);
|
|
49
|
+
const version = (w["php-version"] as string) ?? "8.3";
|
|
50
|
+
const extensions = (w.extensions as string) ?? "";
|
|
51
|
+
const lines = ["apt-get update"];
|
|
52
|
+
if (extensions) lines.push(`docker-php-ext-install ${extensions.split(",").map((s) => s.trim()).join(" ")}`);
|
|
53
|
+
return {
|
|
54
|
+
scriptLines: [],
|
|
55
|
+
image: `php:${version}`,
|
|
56
|
+
beforeScript: lines,
|
|
57
|
+
provenance: [prov(ctx, "shivammathur/setup-php", `setup-php → image: php:${version} + before_script extensions`)],
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const awsConfigureCredentials: ActionMapping = {
|
|
63
|
+
actionName: "aws-actions/configure-aws-credentials",
|
|
64
|
+
tier: 2,
|
|
65
|
+
translate(step, ctx): ActionMappedResult {
|
|
66
|
+
const w = getWith(step);
|
|
67
|
+
const region = (w["aws-region"] as string) ?? "us-east-1";
|
|
68
|
+
return {
|
|
69
|
+
scriptLines: [],
|
|
70
|
+
variables: {
|
|
71
|
+
AWS_DEFAULT_REGION: region,
|
|
72
|
+
},
|
|
73
|
+
provenance: [prov(
|
|
74
|
+
ctx,
|
|
75
|
+
"aws-actions/configure-aws-credentials",
|
|
76
|
+
"Configure AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY as masked CI/CD variables, or use GitLab OIDC to AWS",
|
|
77
|
+
"needs-review",
|
|
78
|
+
)],
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const googleAuthAction: ActionMapping = {
|
|
84
|
+
actionName: "google-github-actions/auth",
|
|
85
|
+
tier: 2,
|
|
86
|
+
translate(_step, ctx): ActionMappedResult {
|
|
87
|
+
return {
|
|
88
|
+
scriptLines: [
|
|
89
|
+
"echo $GCP_SA_KEY | base64 -d > /tmp/gcp-key.json",
|
|
90
|
+
"gcloud auth activate-service-account --key-file=/tmp/gcp-key.json",
|
|
91
|
+
],
|
|
92
|
+
variables: { GOOGLE_APPLICATION_CREDENTIALS: "/tmp/gcp-key.json" },
|
|
93
|
+
provenance: [prov(
|
|
94
|
+
ctx,
|
|
95
|
+
"google-github-actions/auth",
|
|
96
|
+
"Set GCP_SA_KEY as masked CI/CD variable (base64-encoded), or use GitLab OIDC to GCP",
|
|
97
|
+
"needs-review",
|
|
98
|
+
)],
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const azureLogin: ActionMapping = {
|
|
104
|
+
actionName: "azure/login",
|
|
105
|
+
tier: 2,
|
|
106
|
+
translate(_step, ctx): ActionMappedResult {
|
|
107
|
+
return {
|
|
108
|
+
scriptLines: ["az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID"],
|
|
109
|
+
provenance: [prov(
|
|
110
|
+
ctx,
|
|
111
|
+
"azure/login",
|
|
112
|
+
"Set AZURE_CLIENT_ID/SECRET/TENANT_ID as masked CI/CD variables, or use GitLab OIDC to Azure",
|
|
113
|
+
"needs-review",
|
|
114
|
+
)],
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const hashicorpSetupTerraform: ActionMapping = {
|
|
120
|
+
actionName: "hashicorp/setup-terraform",
|
|
121
|
+
tier: 2,
|
|
122
|
+
translate(step, ctx): ActionMappedResult {
|
|
123
|
+
const w = getWith(step);
|
|
124
|
+
const version = (w.terraform_version as string) ?? "1.6";
|
|
125
|
+
return {
|
|
126
|
+
scriptLines: [],
|
|
127
|
+
image: `hashicorp/terraform:${version}`,
|
|
128
|
+
provenance: [prov(ctx, "hashicorp/setup-terraform", `setup-terraform → image: hashicorp/terraform:${version} (or use GitLab Terraform template)`)],
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const codecovAction: ActionMapping = {
|
|
134
|
+
actionName: "codecov/codecov-action",
|
|
135
|
+
tier: 2,
|
|
136
|
+
translate(step, ctx): ActionMappedResult {
|
|
137
|
+
const w = getWith(step);
|
|
138
|
+
const file = (w.files as string) ?? (w.file as string) ?? "coverage.xml";
|
|
139
|
+
return {
|
|
140
|
+
scriptLines: [
|
|
141
|
+
"pip install codecov-cli",
|
|
142
|
+
`codecovcli upload-process --file ${file} --token $CODECOV_TOKEN`,
|
|
143
|
+
],
|
|
144
|
+
provenance: [prov(ctx, "codecov/codecov-action", "codecov-action → codecov-cli; set CODECOV_TOKEN as masked variable")],
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const softpropsGhRelease: ActionMapping = {
|
|
150
|
+
actionName: "softprops/action-gh-release",
|
|
151
|
+
tier: 2,
|
|
152
|
+
translate(_step, ctx): ActionMappedResult {
|
|
153
|
+
return {
|
|
154
|
+
scriptLines: ["glab release create $CI_COMMIT_TAG"],
|
|
155
|
+
provenance: [prov(
|
|
156
|
+
ctx,
|
|
157
|
+
"softprops/action-gh-release",
|
|
158
|
+
"softprops/action-gh-release → glab release create (for GitLab) or curl GitHub API",
|
|
159
|
+
"needs-review",
|
|
160
|
+
)],
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const peterEvansCreatePr: ActionMapping = {
|
|
166
|
+
actionName: "peter-evans/create-pull-request",
|
|
167
|
+
tier: 2,
|
|
168
|
+
translate(step, ctx): ActionMappedResult {
|
|
169
|
+
const w = getWith(step);
|
|
170
|
+
const title = (w.title as string) ?? "Automated update";
|
|
171
|
+
return {
|
|
172
|
+
scriptLines: [
|
|
173
|
+
"git config user.email \"ci@example.com\"",
|
|
174
|
+
"git config user.name \"CI Bot\"",
|
|
175
|
+
"git add .",
|
|
176
|
+
`git commit -m "${title}"`,
|
|
177
|
+
`glab mr create --title "${title}"`,
|
|
178
|
+
],
|
|
179
|
+
provenance: [prov(
|
|
180
|
+
ctx,
|
|
181
|
+
"peter-evans/create-pull-request",
|
|
182
|
+
"create-pull-request → glab mr create",
|
|
183
|
+
"needs-review",
|
|
184
|
+
)],
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const jamesIvesPagesDeploy: ActionMapping = {
|
|
190
|
+
actionName: "JamesIves/github-pages-deploy-action",
|
|
191
|
+
tier: 2,
|
|
192
|
+
translate(step, ctx): ActionMappedResult {
|
|
193
|
+
const w = getWith(step);
|
|
194
|
+
const folder = (w.folder as string) ?? "build";
|
|
195
|
+
return {
|
|
196
|
+
scriptLines: [
|
|
197
|
+
"mkdir -p public",
|
|
198
|
+
`cp -r ${folder}/* public/`,
|
|
199
|
+
],
|
|
200
|
+
artifacts: { paths: ["public"] },
|
|
201
|
+
provenance: [prov(
|
|
202
|
+
ctx,
|
|
203
|
+
"JamesIves/github-pages-deploy-action",
|
|
204
|
+
"pages-deploy → GitLab Pages requires job name 'pages' and artifacts in public/. May need a separate `pages:` job.",
|
|
205
|
+
"needs-review",
|
|
206
|
+
)],
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const pnpmActionSetup: ActionMapping = {
|
|
212
|
+
actionName: "pnpm/action-setup",
|
|
213
|
+
tier: 2,
|
|
214
|
+
translate(step, ctx): ActionMappedResult {
|
|
215
|
+
const w = getWith(step);
|
|
216
|
+
const version = (w.version as string) ?? "9";
|
|
217
|
+
return {
|
|
218
|
+
scriptLines: [`npm install -g pnpm@${version}`],
|
|
219
|
+
cache: { paths: [".pnpm-store/"] },
|
|
220
|
+
provenance: [prov(ctx, "pnpm/action-setup", `pnpm/action-setup → npm install -g pnpm@${version}`)],
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const ovenSetupBun: ActionMapping = {
|
|
226
|
+
actionName: "oven-sh/setup-bun",
|
|
227
|
+
tier: 2,
|
|
228
|
+
translate(step, ctx): ActionMappedResult {
|
|
229
|
+
const w = getWith(step);
|
|
230
|
+
const version = (w["bun-version"] as string) ?? "latest";
|
|
231
|
+
return {
|
|
232
|
+
scriptLines: [],
|
|
233
|
+
image: `oven/bun:${version}`,
|
|
234
|
+
cache: { paths: [".bun/install/cache/"] },
|
|
235
|
+
provenance: [prov(ctx, "oven-sh/setup-bun", `setup-bun → image: oven/bun:${version}`)],
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const gradleBuildAction: ActionMapping = {
|
|
241
|
+
actionName: "gradle/gradle-build-action",
|
|
242
|
+
tier: 2,
|
|
243
|
+
translate(step, ctx): ActionMappedResult {
|
|
244
|
+
const w = getWith(step);
|
|
245
|
+
const version = (w["gradle-version"] as string) ?? "8";
|
|
246
|
+
return {
|
|
247
|
+
scriptLines: [],
|
|
248
|
+
image: `gradle:${version}`,
|
|
249
|
+
cache: { paths: [".gradle/"] },
|
|
250
|
+
provenance: [prov(ctx, "gradle/gradle-build-action", `gradle-build-action → image: gradle:${version}`)],
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const cypressAction: ActionMapping = {
|
|
256
|
+
actionName: "cypress-io/github-action",
|
|
257
|
+
tier: 2,
|
|
258
|
+
translate(step, ctx): ActionMappedResult {
|
|
259
|
+
const w = getWith(step);
|
|
260
|
+
const browser = (w.browser as string) ?? "chrome";
|
|
261
|
+
const start = w.start as string | undefined;
|
|
262
|
+
const lines: string[] = ["npm install"];
|
|
263
|
+
if (start) lines.push(`${start} &`);
|
|
264
|
+
lines.push(`npx cypress run --browser ${browser}`);
|
|
265
|
+
return {
|
|
266
|
+
scriptLines: lines,
|
|
267
|
+
image: "cypress/browsers:latest",
|
|
268
|
+
provenance: [prov(ctx, "cypress-io/github-action", `cypress-io → cypress/browsers image + npx cypress run`)],
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const TIER_2_MAPPINGS: ActionMapping[] = [
|
|
274
|
+
actionsSetupDotnet,
|
|
275
|
+
shivammathurSetupPhp,
|
|
276
|
+
awsConfigureCredentials,
|
|
277
|
+
googleAuthAction,
|
|
278
|
+
azureLogin,
|
|
279
|
+
hashicorpSetupTerraform,
|
|
280
|
+
codecovAction,
|
|
281
|
+
softpropsGhRelease,
|
|
282
|
+
peterEvansCreatePr,
|
|
283
|
+
jamesIvesPagesDeploy,
|
|
284
|
+
pnpmActionSetup,
|
|
285
|
+
ovenSetupBun,
|
|
286
|
+
gradleBuildAction,
|
|
287
|
+
cypressAction,
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
export function registerTier2(registry = getDefaultRegistry()): void {
|
|
291
|
+
for (const m of TIER_2_MAPPINGS) {
|
|
292
|
+
registry.register(m);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { TIER_2_MAPPINGS };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 3 marketplace action mappings — 5 niche actions. Several map to
|
|
3
|
+
* native GitLab keywords (rules:changes, retry:) rather than scripts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ActionMapping, ActionMappedResult, ActionMapCtx } from "./registry";
|
|
7
|
+
import { getDefaultRegistry } from "./registry";
|
|
8
|
+
|
|
9
|
+
const prov = (
|
|
10
|
+
ctx: ActionMapCtx,
|
|
11
|
+
actionName: string,
|
|
12
|
+
note: string,
|
|
13
|
+
category: "literal" | "needs-review" | "skipped" | "action-map" = "action-map",
|
|
14
|
+
) => ({
|
|
15
|
+
gitlabPath: `jobs.${ctx.logicalId}.script`,
|
|
16
|
+
gitlabLogicalId: ctx.logicalId,
|
|
17
|
+
sourceKey: `jobs.${ctx.jobName}.steps[${ctx.stepIndex}].uses`,
|
|
18
|
+
sourceFile: ctx.sourceFile,
|
|
19
|
+
category,
|
|
20
|
+
rule: `ACT-${actionName.replace(/[\/-]/g, "-")}`,
|
|
21
|
+
note,
|
|
22
|
+
actionRef: actionName,
|
|
23
|
+
mappingTier: 3 as const,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const getWith = (s: Record<string, unknown>): Record<string, unknown> =>
|
|
27
|
+
(s.with as Record<string, unknown>) ?? {};
|
|
28
|
+
|
|
29
|
+
const tjActionsChangedFiles: ActionMapping = {
|
|
30
|
+
actionName: "tj-actions/changed-files",
|
|
31
|
+
tier: 3,
|
|
32
|
+
translate(step, ctx): ActionMappedResult {
|
|
33
|
+
const w = getWith(step);
|
|
34
|
+
const files = (w.files as string) ?? "";
|
|
35
|
+
return {
|
|
36
|
+
scriptLines: [
|
|
37
|
+
`git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA${files ? ` | grep -E '${files.replace(/\n/g, "|")}' || true` : ""}`,
|
|
38
|
+
],
|
|
39
|
+
provenance: [prov(ctx, "tj-actions/changed-files", "tj-actions/changed-files → git diff inline; for path-based job gating prefer rules:changes:")],
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// dorny/paths-filter: native rules:changes:
|
|
45
|
+
const dornyPathsFilter: ActionMapping = {
|
|
46
|
+
actionName: "dorny/paths-filter",
|
|
47
|
+
tier: 3,
|
|
48
|
+
translate(_step, ctx): ActionMappedResult {
|
|
49
|
+
return {
|
|
50
|
+
scriptLines: [
|
|
51
|
+
"# dorny/paths-filter has no script equivalent in GitLab.",
|
|
52
|
+
"# Use rules:changes: on each job to gate on file path changes.",
|
|
53
|
+
"# Example: rules: [{ changes: ['src/backend/**'] }]",
|
|
54
|
+
],
|
|
55
|
+
provenance: [prov(
|
|
56
|
+
ctx,
|
|
57
|
+
"dorny/paths-filter",
|
|
58
|
+
"dorny/paths-filter → native rules:changes:; emit rule on each gated job",
|
|
59
|
+
"needs-review",
|
|
60
|
+
)],
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// nick-invision/retry / nick-fields/retry: native retry:
|
|
66
|
+
const nickFieldsRetry: ActionMapping = {
|
|
67
|
+
actionName: "nick-fields/retry",
|
|
68
|
+
tier: 3,
|
|
69
|
+
translate(step, ctx): ActionMappedResult {
|
|
70
|
+
const w = getWith(step);
|
|
71
|
+
const command = (w.command as string) ?? "";
|
|
72
|
+
return {
|
|
73
|
+
scriptLines: command ? [command] : [],
|
|
74
|
+
provenance: [prov(
|
|
75
|
+
ctx,
|
|
76
|
+
"nick-fields/retry",
|
|
77
|
+
"nick-fields/retry → native retry: { max, when } at job level; configure manually",
|
|
78
|
+
"needs-review",
|
|
79
|
+
)],
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const preCommitAction: ActionMapping = {
|
|
85
|
+
actionName: "pre-commit/action",
|
|
86
|
+
tier: 3,
|
|
87
|
+
translate(_step, ctx): ActionMappedResult {
|
|
88
|
+
return {
|
|
89
|
+
scriptLines: ["pip install pre-commit", "pre-commit run --all-files"],
|
|
90
|
+
provenance: [prov(ctx, "pre-commit/action", "pre-commit/action → pip install pre-commit + pre-commit run")],
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const slackapiGithubAction: ActionMapping = {
|
|
96
|
+
actionName: "slackapi/slack-github-action",
|
|
97
|
+
tier: 3,
|
|
98
|
+
translate(step, ctx): ActionMappedResult {
|
|
99
|
+
const w = getWith(step);
|
|
100
|
+
const payload = (w.payload as string) ?? '{"text":"Build completed"}';
|
|
101
|
+
return {
|
|
102
|
+
scriptLines: [
|
|
103
|
+
`curl -X POST -H 'Content-type: application/json' --data '${payload.replace(/'/g, "'\\''")}' $SLACK_WEBHOOK_URL`,
|
|
104
|
+
],
|
|
105
|
+
provenance: [prov(ctx, "slackapi/slack-github-action", "slack-github-action → curl to $SLACK_WEBHOOK_URL")],
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const TIER_3_MAPPINGS: ActionMapping[] = [
|
|
111
|
+
tjActionsChangedFiles,
|
|
112
|
+
dornyPathsFilter,
|
|
113
|
+
nickFieldsRetry,
|
|
114
|
+
preCommitAction,
|
|
115
|
+
slackapiGithubAction,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
export function registerTier3(registry = getDefaultRegistry()): void {
|
|
119
|
+
for (const m of TIER_3_MAPPINGS) {
|
|
120
|
+
registry.register(m);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { TIER_3_MAPPINGS };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern recognisers for `chant migrate --use-composites`.
|
|
3
|
+
*
|
|
4
|
+
* Each pattern walks the GitLab IR looking for a recognisable shape
|
|
5
|
+
* (e.g. NodePipeline-shaped 2-job setup) and rewrites the matched
|
|
6
|
+
* resources into a sentinel IR type (`GitLab::Composite::NodePipeline`)
|
|
7
|
+
* that the GitLab generator (`lexicons/gitlab/src/import/generator.ts`)
|
|
8
|
+
* will emit as `NodePipeline({...})` instead of raw `new Job(...)`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
|
|
12
|
+
import type { ProvenanceRecord } from "../provenance";
|
|
13
|
+
|
|
14
|
+
export interface CompositePattern {
|
|
15
|
+
name: string;
|
|
16
|
+
match(ir: TemplateIR): MatchResult | null;
|
|
17
|
+
rewrite(ir: TemplateIR, match: MatchResult): { ir: TemplateIR; provenance: ProvenanceRecord[] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MatchResult {
|
|
21
|
+
/** Resource logicalIds to remove */
|
|
22
|
+
removed: string[];
|
|
23
|
+
/** New composite resource to add */
|
|
24
|
+
added: ResourceIR;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getProp<T>(r: ResourceIR, k: string): T | undefined {
|
|
28
|
+
return r.properties[k] as T | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function looksLikeNodeImage(image: unknown): boolean {
|
|
32
|
+
return typeof image === "string" && /^node:/i.test(image);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function detectPackageManager(script: string[]): "npm" | "yarn" | "pnpm" | "bun" {
|
|
36
|
+
const text = script.join("\n");
|
|
37
|
+
if (/\b(bun|bunx)\s+(install|test|run)/.test(text)) return "bun";
|
|
38
|
+
if (/\bpnpm\s+(install|run|test)/.test(text)) return "pnpm";
|
|
39
|
+
if (/\byarn\s+(install|run|test)/.test(text)) return "yarn";
|
|
40
|
+
return "npm";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasInstallStep(script: string[], pm: "npm" | "yarn" | "pnpm" | "bun"): boolean {
|
|
44
|
+
const installers: Record<string, RegExp> = {
|
|
45
|
+
npm: /\bnpm\s+(ci|install)\b/,
|
|
46
|
+
yarn: /\byarn\s+install\b/,
|
|
47
|
+
pnpm: /\bpnpm\s+install\b/,
|
|
48
|
+
bun: /\bbun\s+install\b/,
|
|
49
|
+
};
|
|
50
|
+
return script.some((line) => installers[pm].test(line));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasBuildStep(script: string[], pm: "npm" | "yarn" | "pnpm" | "bun"): boolean {
|
|
54
|
+
const re = pm === "npm" ? /\bnpm\s+run\s+build\b/ : new RegExp(`\\b${pm}\\s+(run\\s+)?build\\b`);
|
|
55
|
+
return script.some((line) => re.test(line));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasTestStep(script: string[], pm: "npm" | "yarn" | "pnpm" | "bun"): boolean {
|
|
59
|
+
const re = pm === "npm"
|
|
60
|
+
? /\bnpm\s+(test|run\s+test)\b/
|
|
61
|
+
: new RegExp(`\\b${pm}\\s+(run\\s+)?test\\b`);
|
|
62
|
+
return script.some((line) => re.test(line));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nodePipelinePattern: CompositePattern = {
|
|
66
|
+
name: "NodePipeline",
|
|
67
|
+
match(ir: TemplateIR): MatchResult | null {
|
|
68
|
+
const jobs = ir.resources.filter((r) => r.type === "GitLab::CI::Job");
|
|
69
|
+
if (jobs.length !== 2) return null;
|
|
70
|
+
// Find build job + test job
|
|
71
|
+
let buildJob: ResourceIR | undefined;
|
|
72
|
+
let testJob: ResourceIR | undefined;
|
|
73
|
+
for (const j of jobs) {
|
|
74
|
+
const script = (getProp<string[]>(j, "script") ?? []);
|
|
75
|
+
if (!looksLikeNodeImage(getProp(j, "image"))) return null;
|
|
76
|
+
const pm = detectPackageManager(script);
|
|
77
|
+
if (hasBuildStep(script, pm) && hasInstallStep(script, pm)) {
|
|
78
|
+
buildJob = j;
|
|
79
|
+
} else if (hasTestStep(script, pm) && hasInstallStep(script, pm)) {
|
|
80
|
+
testJob = j;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!buildJob || !testJob) return null;
|
|
84
|
+
if (buildJob.logicalId === testJob.logicalId) return null;
|
|
85
|
+
|
|
86
|
+
const buildScript = (getProp<string[]>(buildJob, "script") ?? []);
|
|
87
|
+
const pm = detectPackageManager(buildScript);
|
|
88
|
+
const image = getProp<string>(buildJob, "image") ?? "node:22";
|
|
89
|
+
const nodeVersion = image.replace(/^node:/, "") || "22";
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
removed: [buildJob.logicalId, testJob.logicalId],
|
|
93
|
+
added: {
|
|
94
|
+
logicalId: "app",
|
|
95
|
+
type: "GitLab::Composite::NodePipeline",
|
|
96
|
+
properties: {
|
|
97
|
+
nodeVersion,
|
|
98
|
+
packageManager: pm,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
rewrite(ir, match): { ir: TemplateIR; provenance: ProvenanceRecord[] } {
|
|
104
|
+
const keep = ir.resources.filter((r) => !match.removed.includes(r.logicalId));
|
|
105
|
+
return {
|
|
106
|
+
ir: { ...ir, resources: [...keep, match.added] },
|
|
107
|
+
provenance: [
|
|
108
|
+
{
|
|
109
|
+
gitlabPath: `jobs.${match.added.logicalId}`,
|
|
110
|
+
gitlabLogicalId: match.added.logicalId,
|
|
111
|
+
category: "synthesis",
|
|
112
|
+
rule: "MIG-COMPOSITE-NODEPIPELINE",
|
|
113
|
+
note: `Recognised NodePipeline shape (${match.removed.join(", ")}) → NodePipeline composite`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const nodeCiPattern: CompositePattern = {
|
|
121
|
+
name: "NodeCI",
|
|
122
|
+
match(ir: TemplateIR): MatchResult | null {
|
|
123
|
+
const jobs = ir.resources.filter((r) => r.type === "GitLab::CI::Job");
|
|
124
|
+
if (jobs.length !== 1) return null;
|
|
125
|
+
const j = jobs[0];
|
|
126
|
+
const script = (getProp<string[]>(j, "script") ?? []);
|
|
127
|
+
if (!looksLikeNodeImage(getProp(j, "image"))) return null;
|
|
128
|
+
const pm = detectPackageManager(script);
|
|
129
|
+
if (!hasInstallStep(script, pm)) return null;
|
|
130
|
+
if (!hasBuildStep(script, pm) && !hasTestStep(script, pm)) return null;
|
|
131
|
+
const image = getProp<string>(j, "image") ?? "node:22";
|
|
132
|
+
const nodeVersion = image.replace(/^node:/, "") || "22";
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
removed: [j.logicalId],
|
|
136
|
+
added: {
|
|
137
|
+
logicalId: "app",
|
|
138
|
+
type: "GitLab::Composite::NodeCI",
|
|
139
|
+
properties: {
|
|
140
|
+
nodeVersion,
|
|
141
|
+
packageManager: pm,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
rewrite(ir, match): { ir: TemplateIR; provenance: ProvenanceRecord[] } {
|
|
147
|
+
const keep = ir.resources.filter((r) => !match.removed.includes(r.logicalId));
|
|
148
|
+
return {
|
|
149
|
+
ir: { ...ir, resources: [...keep, match.added] },
|
|
150
|
+
provenance: [
|
|
151
|
+
{
|
|
152
|
+
gitlabPath: `jobs.${match.added.logicalId}`,
|
|
153
|
+
gitlabLogicalId: match.added.logicalId,
|
|
154
|
+
category: "synthesis",
|
|
155
|
+
rule: "MIG-COMPOSITE-NODECI",
|
|
156
|
+
note: `Recognised NodeCI shape (${match.removed.join(", ")}) → NodeCI composite`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const PATTERNS: CompositePattern[] = [
|
|
164
|
+
// Match NodePipeline first (2 jobs); NodeCI second (single job)
|
|
165
|
+
nodePipelinePattern,
|
|
166
|
+
nodeCiPattern,
|
|
167
|
+
];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { applyComposites } from "./rewriter";
|
|
3
|
+
import type { TemplateIR } from "@intentius/chant/import/parser";
|
|
4
|
+
|
|
5
|
+
function nodeJob(id: string, opts: { image: string; script: string[]; stage?: string }): TemplateIR["resources"][0] {
|
|
6
|
+
return {
|
|
7
|
+
logicalId: id,
|
|
8
|
+
type: "GitLab::CI::Job",
|
|
9
|
+
properties: {
|
|
10
|
+
image: opts.image,
|
|
11
|
+
script: opts.script,
|
|
12
|
+
...(opts.stage ? { stage: opts.stage } : {}),
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("applyComposites — NodePipeline", () => {
|
|
18
|
+
test("matches 2-job build+test shape", () => {
|
|
19
|
+
const ir: TemplateIR = {
|
|
20
|
+
resources: [
|
|
21
|
+
nodeJob("build", { image: "node:22", script: ["npm ci", "npm run build"], stage: "build" }),
|
|
22
|
+
nodeJob("test", { image: "node:22", script: ["npm ci", "npm test"], stage: "test" }),
|
|
23
|
+
],
|
|
24
|
+
parameters: [],
|
|
25
|
+
};
|
|
26
|
+
const r = applyComposites(ir);
|
|
27
|
+
const composites = r.ir.resources.filter((res) => res.type === "GitLab::Composite::NodePipeline");
|
|
28
|
+
expect(composites).toHaveLength(1);
|
|
29
|
+
expect(r.ir.resources.filter((res) => res.type === "GitLab::CI::Job")).toHaveLength(0);
|
|
30
|
+
expect(r.provenance[0].rule).toBe("MIG-COMPOSITE-NODEPIPELINE");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("rejects non-node images", () => {
|
|
34
|
+
const ir: TemplateIR = {
|
|
35
|
+
resources: [
|
|
36
|
+
nodeJob("build", { image: "alpine:3", script: ["npm ci", "npm run build"] }),
|
|
37
|
+
nodeJob("test", { image: "node:22", script: ["npm ci", "npm test"] }),
|
|
38
|
+
],
|
|
39
|
+
parameters: [],
|
|
40
|
+
};
|
|
41
|
+
const r = applyComposites(ir);
|
|
42
|
+
expect(r.ir.resources.filter((res) => res.type === "GitLab::CI::Job")).toHaveLength(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("rejects jobs without install step", () => {
|
|
46
|
+
const ir: TemplateIR = {
|
|
47
|
+
resources: [
|
|
48
|
+
nodeJob("build", { image: "node:22", script: ["npm run build"] }),
|
|
49
|
+
nodeJob("test", { image: "node:22", script: ["npm test"] }),
|
|
50
|
+
],
|
|
51
|
+
parameters: [],
|
|
52
|
+
};
|
|
53
|
+
const r = applyComposites(ir);
|
|
54
|
+
expect(r.ir.resources.filter((res) => res.type === "GitLab::CI::Job")).toHaveLength(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("detects pnpm package manager", () => {
|
|
58
|
+
const ir: TemplateIR = {
|
|
59
|
+
resources: [
|
|
60
|
+
nodeJob("build", { image: "node:20", script: ["pnpm install", "pnpm build"] }),
|
|
61
|
+
nodeJob("test", { image: "node:20", script: ["pnpm install", "pnpm test"] }),
|
|
62
|
+
],
|
|
63
|
+
parameters: [],
|
|
64
|
+
};
|
|
65
|
+
const r = applyComposites(ir);
|
|
66
|
+
const composite = r.ir.resources.find((res) => res.type === "GitLab::Composite::NodePipeline");
|
|
67
|
+
expect(composite?.properties.packageManager).toBe("pnpm");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("applyComposites — NodeCI", () => {
|
|
72
|
+
test("matches single-job node setup", () => {
|
|
73
|
+
const ir: TemplateIR = {
|
|
74
|
+
resources: [
|
|
75
|
+
nodeJob("ci", { image: "node:20", script: ["npm ci", "npm test"] }),
|
|
76
|
+
],
|
|
77
|
+
parameters: [],
|
|
78
|
+
};
|
|
79
|
+
const r = applyComposites(ir);
|
|
80
|
+
const composites = r.ir.resources.filter((res) => res.type === "GitLab::Composite::NodeCI");
|
|
81
|
+
expect(composites).toHaveLength(1);
|
|
82
|
+
expect(r.provenance[0].rule).toBe("MIG-COMPOSITE-NODECI");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("falls back to raw jobs when nothing matches", () => {
|
|
86
|
+
const ir: TemplateIR = {
|
|
87
|
+
resources: [
|
|
88
|
+
nodeJob("a", { image: "ubuntu:24.04", script: ["make"] }),
|
|
89
|
+
nodeJob("b", { image: "ubuntu:24.04", script: ["./deploy.sh"] }),
|
|
90
|
+
nodeJob("c", { image: "ubuntu:24.04", script: ["./teardown.sh"] }),
|
|
91
|
+
],
|
|
92
|
+
parameters: [],
|
|
93
|
+
};
|
|
94
|
+
const r = applyComposites(ir);
|
|
95
|
+
expect(r.ir.resources).toHaveLength(3);
|
|
96
|
+
expect(r.provenance).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
});
|