@intentius/chant-lexicon-gitlab 0.124.30 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +157 -0
- package/dist/codegen/fetch.js +93 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +36 -0
- package/dist/codegen/generate-typescript.js +105 -0
- package/dist/codegen/generate.js +125 -0
- package/dist/codegen/naming.js +48 -0
- package/dist/codegen/package.js +41 -0
- package/dist/codegen/parse.js +539 -0
- package/dist/codegen/patches.js +84 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/components/ci-jobs.js +16 -0
- package/dist/components/generate-op-pipeline.js +288 -0
- package/dist/components/generate-op-waves-pipeline.js +125 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +369 -0
- package/dist/composites/catalog.js +418 -0
- package/dist/composites/docker-build.js +38 -0
- package/dist/composites/index.js +5 -0
- package/dist/composites/mr-plan-report.js +42 -0
- package/dist/composites/node-pipeline.js +67 -0
- package/dist/composites/python-pipeline.js +45 -0
- package/dist/composites/review-app.js +32 -0
- package/dist/coverage.js +35 -0
- package/dist/generated/index.js +25 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/graph-edges.js +202 -0
- package/dist/import/generator.js +196 -0
- package/dist/import/parser.js +111 -0
- package/dist/index.js +26 -0
- package/dist/integrity.json +3 -3
- package/dist/intrinsics.js +33 -0
- package/dist/lint/audit-catalog.js +53 -0
- package/dist/lint/audit-lineage.js +64 -0
- package/dist/lint/post-synth/index.js +84 -0
- package/dist/lint/post-synth/wgl010.js +34 -0
- package/dist/lint/post-synth/wgl011.js +48 -0
- package/dist/lint/post-synth/wgl012.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl012.js +68 -0
- package/dist/lint/post-synth/wgl013.js +57 -0
- package/dist/lint/post-synth/wgl014.js +43 -0
- package/dist/lint/post-synth/wgl015.js +70 -0
- package/dist/lint/post-synth/wgl016.js +70 -0
- package/dist/lint/post-synth/wgl017.js +38 -0
- package/dist/lint/post-synth/wgl018.js +34 -0
- package/dist/lint/post-synth/wgl019.js +43 -0
- package/dist/lint/post-synth/wgl020.js +47 -0
- package/dist/lint/post-synth/wgl021.js +52 -0
- package/dist/lint/post-synth/wgl022.js +38 -0
- package/dist/lint/post-synth/wgl023.js +44 -0
- package/dist/lint/post-synth/wgl024.js +41 -0
- package/dist/lint/post-synth/wgl025.js +42 -0
- package/dist/lint/post-synth/wgl026.js +58 -0
- package/dist/lint/post-synth/wgl027.js +49 -0
- package/dist/lint/post-synth/wgl028.js +58 -0
- package/dist/lint/post-synth/wgl029.js +55 -0
- package/dist/lint/post-synth/wgl030.js +43 -0
- package/dist/lint/post-synth/wgl031.js +33 -0
- package/dist/lint/post-synth/wgl032.js +64 -0
- package/dist/lint/post-synth/wgl033.js +32 -0
- package/dist/lint/post-synth/wgl034.js +37 -0
- package/dist/lint/post-synth/wgl035.js +40 -0
- package/dist/lint/post-synth/wgl036.js +38 -0
- package/dist/lint/post-synth/wgl037.js +41 -0
- package/dist/lint/post-synth/wgl038.js +43 -0
- package/dist/lint/post-synth/wgl039.js +34 -0
- package/dist/lint/post-synth/wgl040.js +35 -0
- package/dist/lint/post-synth/wgl041.js +53 -0
- package/dist/lint/post-synth/wgl042.js +41 -0
- package/dist/lint/post-synth/wgl043.js +39 -0
- package/dist/lint/post-synth/wgl044.js +35 -0
- package/dist/lint/post-synth/wgl045.js +54 -0
- package/dist/lint/post-synth/wgl046.js +40 -0
- package/dist/lint/post-synth/wgl047.js +34 -0
- package/dist/lint/post-synth/wgl048.js +32 -0
- package/dist/lint/post-synth/wgl049.js +53 -0
- package/dist/lint/post-synth/wgl050.js +42 -0
- package/dist/lint/post-synth/yaml-helpers.js +432 -0
- package/dist/lint/rules/artifact-no-expiry.js +53 -0
- package/dist/lint/rules/data/known-include-sources.js +44 -0
- package/dist/lint/rules/data/untrusted-variables.js +30 -0
- package/dist/lint/rules/deprecated-only-except.js +46 -0
- package/dist/lint/rules/import-source.js +61 -0
- package/dist/lint/rules/index.js +7 -0
- package/dist/lint/rules/missing-script.js +67 -0
- package/dist/lint/rules/missing-stage.js +60 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +33 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/context-tools.js +262 -0
- package/dist/migrate/from-github/actions/index.js +17 -0
- package/dist/migrate/from-github/actions/registry.js +61 -0
- package/dist/migrate/from-github/actions/tier-1.js +298 -0
- package/dist/migrate/from-github/actions/tier-2.js +241 -0
- package/dist/migrate/from-github/actions/tier-3.js +96 -0
- package/dist/migrate/from-github/composites/patterns.js +151 -0
- package/dist/migrate/from-github/composites/rewriter.js +22 -0
- package/dist/migrate/from-github/diagnostics.js +53 -0
- package/dist/migrate/from-github/emit-yaml.js +121 -0
- package/dist/migrate/from-github/expressions.js +275 -0
- package/dist/migrate/from-github/index.js +96 -0
- package/dist/migrate/from-github/provenance.js +35 -0
- package/dist/migrate/from-github/rules.js +74 -0
- package/dist/migrate/from-github/security.js +189 -0
- package/dist/migrate/from-github/stages.js +156 -0
- package/dist/migrate/from-github/transformer.js +633 -0
- package/dist/op/activities/gitlab.js +38 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +26 -0
- package/dist/package-cli.js +21 -0
- package/dist/plugin.js +536 -0
- package/dist/rules/wgl012.ts +2 -2
- package/dist/serializer.js +197 -0
- package/dist/testdata/create-fixture.js +40 -0
- package/dist/testdata/load-fixtures.js +20 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +29 -0
- package/dist/variables.js +30 -0
- package/package.json +8 -8
- package/src/codegen/fetch.ts +2 -2
- package/src/codegen/package.ts +2 -2
- package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -3
- package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +101 -0
- package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
- package/src/components/generate-pipeline.ts +60 -27
- package/src/components/generate-pr-pipeline.test.ts +14 -9
- package/src/components/generate-pr-waves-pipeline.test.ts +88 -0
- package/src/lint/post-synth/wgl012.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions expression substitution.
|
|
3
|
+
*
|
|
4
|
+
* Translates `${{ github.X }}`, `${{ runner.X }}`, `${{ secrets.X }}`,
|
|
5
|
+
* `${{ env.X }}`, `${{ vars.X }}`, `${{ inputs.X }}`, `${{ matrix.X }}`,
|
|
6
|
+
* and a small set of expression functions (`always()`, `failure()`, etc.)
|
|
7
|
+
* to their GitLab CI predefined-variable equivalents.
|
|
8
|
+
*
|
|
9
|
+
* Source: the upstream `github-actions-to-gitlab-ci` skill's
|
|
10
|
+
* `references/syntax-mapping.md` "github.* context expressions" tables.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Direct 1:1 mappings from GitHub context identifiers to GitLab predefined
|
|
14
|
+
* variables. Keys are the dotted identifier *without* the `${{ }}` wrapper.
|
|
15
|
+
*/
|
|
16
|
+
const GITHUB_CONTEXT_MAP = {
|
|
17
|
+
"github.sha": "$CI_COMMIT_SHA",
|
|
18
|
+
"github.ref": "$CI_COMMIT_REF_NAME",
|
|
19
|
+
"github.ref_name": "$CI_COMMIT_REF_NAME",
|
|
20
|
+
"github.repository": "$CI_PROJECT_PATH",
|
|
21
|
+
"github.repository_owner": "$CI_PROJECT_NAMESPACE",
|
|
22
|
+
"github.actor": "$GITLAB_USER_LOGIN",
|
|
23
|
+
"github.triggering_actor": "$GITLAB_USER_LOGIN",
|
|
24
|
+
"github.event_name": "$CI_PIPELINE_SOURCE",
|
|
25
|
+
"github.workflow": "$CI_PIPELINE_NAME",
|
|
26
|
+
"github.run_id": "$CI_PIPELINE_ID",
|
|
27
|
+
"github.run_number": "$CI_PIPELINE_IID",
|
|
28
|
+
"github.job": "$CI_JOB_NAME",
|
|
29
|
+
"github.head_ref": "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME",
|
|
30
|
+
"github.base_ref": "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME",
|
|
31
|
+
"github.workspace": "$CI_PROJECT_DIR",
|
|
32
|
+
"github.server_url": "$CI_SERVER_URL",
|
|
33
|
+
"runner.workspace": "$CI_PROJECT_DIR",
|
|
34
|
+
"job.status": "$CI_JOB_STATUS",
|
|
35
|
+
"strategy.job-index": "$CI_NODE_INDEX",
|
|
36
|
+
"strategy.job-total": "$CI_NODE_TOTAL",
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* GitHub expressions with no GitLab equivalent. When detected, the
|
|
40
|
+
* substitution leaves a placeholder and registers a needs-review record.
|
|
41
|
+
*/
|
|
42
|
+
const NO_EQUIVALENT = new Set([
|
|
43
|
+
"github.run_attempt",
|
|
44
|
+
"runner.os",
|
|
45
|
+
"runner.arch",
|
|
46
|
+
"runner.temp",
|
|
47
|
+
"job.container.id",
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Map a GitHub `if:` boolean function to a GitLab `when:` value or rule.
|
|
51
|
+
*
|
|
52
|
+
* Returned tuple is `[gitlabExpression, isWhenClause]` — when `isWhenClause`
|
|
53
|
+
* is true, the caller should emit `when: <expr>` rather than `if: <expr>`.
|
|
54
|
+
*/
|
|
55
|
+
export function translateIfFunction(expr) {
|
|
56
|
+
const trimmed = expr.trim();
|
|
57
|
+
if (trimmed === "always()")
|
|
58
|
+
return { expression: "true", whenClause: "always" };
|
|
59
|
+
if (trimmed === "success()")
|
|
60
|
+
return { expression: "true", whenClause: "on_success" };
|
|
61
|
+
if (trimmed === "failure()")
|
|
62
|
+
return { expression: "true", whenClause: "on_failure" };
|
|
63
|
+
if (trimmed === "cancelled()" || trimmed === "canceled()") {
|
|
64
|
+
return { expression: "true", needsReview: true };
|
|
65
|
+
}
|
|
66
|
+
return { expression: trimmed };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Substitute all `${{ ... }}` template expressions in a string with their
|
|
70
|
+
* GitLab equivalents. Returns the substituted string plus any provenance
|
|
71
|
+
* records describing which substitutions happened (or which need review).
|
|
72
|
+
*/
|
|
73
|
+
export function substituteExpressions(input, ctx) {
|
|
74
|
+
const records = [];
|
|
75
|
+
// GitHub expressions are wrapped in ${{ ... }}.
|
|
76
|
+
const output = input.replace(/\$\{\{\s*([^}]+?)\s*\}\}/g, (match, expr) => {
|
|
77
|
+
const trimmed = expr.trim();
|
|
78
|
+
// Direct context lookup
|
|
79
|
+
if (Object.prototype.hasOwnProperty.call(GITHUB_CONTEXT_MAP, trimmed)) {
|
|
80
|
+
records.push({
|
|
81
|
+
gitlabPath: ctx.gitlabPath,
|
|
82
|
+
sourceKey: ctx.sourceKey,
|
|
83
|
+
sourceFile: ctx.sourceFile,
|
|
84
|
+
category: "rewrite",
|
|
85
|
+
rule: "MIG-EXPR-CONTEXT",
|
|
86
|
+
note: `${trimmed} → ${GITHUB_CONTEXT_MAP[trimmed]}`,
|
|
87
|
+
});
|
|
88
|
+
return GITHUB_CONTEXT_MAP[trimmed];
|
|
89
|
+
}
|
|
90
|
+
// env.NAME / vars.NAME / secrets.NAME → $NAME
|
|
91
|
+
const userVarMatch = /^(env|vars|secrets|inputs|matrix)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
92
|
+
if (userVarMatch) {
|
|
93
|
+
const [, , name] = userVarMatch;
|
|
94
|
+
records.push({
|
|
95
|
+
gitlabPath: ctx.gitlabPath,
|
|
96
|
+
sourceKey: ctx.sourceKey,
|
|
97
|
+
sourceFile: ctx.sourceFile,
|
|
98
|
+
category: "rewrite",
|
|
99
|
+
rule: "MIG-EXPR-USERVAR",
|
|
100
|
+
note: `${trimmed} → $${name}`,
|
|
101
|
+
});
|
|
102
|
+
return `$${name}`;
|
|
103
|
+
}
|
|
104
|
+
// steps.<id>.outputs.<name> → $name with needs-review note (requires dotenv)
|
|
105
|
+
const stepsOutputMatch = /^steps\.([A-Za-z_][A-Za-z0-9_-]*)\.outputs\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
106
|
+
if (stepsOutputMatch) {
|
|
107
|
+
const [, , name] = stepsOutputMatch;
|
|
108
|
+
records.push({
|
|
109
|
+
gitlabPath: ctx.gitlabPath,
|
|
110
|
+
sourceKey: ctx.sourceKey,
|
|
111
|
+
sourceFile: ctx.sourceFile,
|
|
112
|
+
category: "needs-review",
|
|
113
|
+
rule: "MIG-NEEDS-OUTPUTS-001",
|
|
114
|
+
note: `${trimmed} requires artifacts:reports:dotenv pattern in GitLab; emitting $${name} placeholder`,
|
|
115
|
+
});
|
|
116
|
+
return `$${name}`;
|
|
117
|
+
}
|
|
118
|
+
// needs.<job>.outputs.<name> → $name with needs-review note
|
|
119
|
+
const needsOutputMatch = /^needs\.([A-Za-z_][A-Za-z0-9_-]*)\.outputs\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
120
|
+
if (needsOutputMatch) {
|
|
121
|
+
const [, , name] = needsOutputMatch;
|
|
122
|
+
records.push({
|
|
123
|
+
gitlabPath: ctx.gitlabPath,
|
|
124
|
+
sourceKey: ctx.sourceKey,
|
|
125
|
+
sourceFile: ctx.sourceFile,
|
|
126
|
+
category: "needs-review",
|
|
127
|
+
rule: "MIG-NEEDS-OUTPUTS-001",
|
|
128
|
+
note: `${trimmed} requires artifacts:reports:dotenv pattern in GitLab; emitting $${name} placeholder`,
|
|
129
|
+
});
|
|
130
|
+
return `$${name}`;
|
|
131
|
+
}
|
|
132
|
+
// Boolean expression functions: always(), success(), failure(), cancelled()
|
|
133
|
+
const fnResult = translateIfFunction(trimmed);
|
|
134
|
+
if (fnResult.whenClause) {
|
|
135
|
+
records.push({
|
|
136
|
+
gitlabPath: ctx.gitlabPath,
|
|
137
|
+
sourceKey: ctx.sourceKey,
|
|
138
|
+
sourceFile: ctx.sourceFile,
|
|
139
|
+
category: "rewrite",
|
|
140
|
+
rule: "MIG-EXPR-FUNCTION",
|
|
141
|
+
note: `${trimmed} → when: ${fnResult.whenClause}`,
|
|
142
|
+
});
|
|
143
|
+
return fnResult.expression;
|
|
144
|
+
}
|
|
145
|
+
// No-equivalent context expressions
|
|
146
|
+
if (NO_EQUIVALENT.has(trimmed)) {
|
|
147
|
+
records.push({
|
|
148
|
+
gitlabPath: ctx.gitlabPath,
|
|
149
|
+
sourceKey: ctx.sourceKey,
|
|
150
|
+
sourceFile: ctx.sourceFile,
|
|
151
|
+
category: "needs-review",
|
|
152
|
+
rule: "MIG-EXPR-NO-EQUIV",
|
|
153
|
+
note: `${trimmed} has no GitLab predefined variable equivalent`,
|
|
154
|
+
});
|
|
155
|
+
return `# TODO: ${trimmed} has no GitLab equivalent`;
|
|
156
|
+
}
|
|
157
|
+
// Unknown expression — leave the original and flag
|
|
158
|
+
records.push({
|
|
159
|
+
gitlabPath: ctx.gitlabPath,
|
|
160
|
+
sourceKey: ctx.sourceKey,
|
|
161
|
+
sourceFile: ctx.sourceFile,
|
|
162
|
+
category: "needs-review",
|
|
163
|
+
rule: "MIG-EXPR-UNKNOWN",
|
|
164
|
+
note: `Could not translate expression: ${match}`,
|
|
165
|
+
});
|
|
166
|
+
return match;
|
|
167
|
+
});
|
|
168
|
+
return { output, provenance: records };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Substitute GitHub identifiers (e.g. `github.ref`) appearing anywhere
|
|
172
|
+
* in a string with their GitLab predefined-variable equivalents.
|
|
173
|
+
* Used for if-conditions, concurrency.group values, and other places
|
|
174
|
+
* where identifiers appear without `${{ }}` wrapping.
|
|
175
|
+
*/
|
|
176
|
+
export function substituteIdentifiers(input, ctx) {
|
|
177
|
+
const records = [];
|
|
178
|
+
// Match dotted identifiers like `github.ref_name`, `runner.os`,
|
|
179
|
+
// `steps.foo.outputs.bar`, `env.NAME`, etc. — but not when they are
|
|
180
|
+
// already prefixed by `$` (already substituted).
|
|
181
|
+
const output = input.replace(/(?<![\w$])([a-z]+(?:\.[A-Za-z_][A-Za-z0-9_-]*)+)/g, (match) => {
|
|
182
|
+
if (Object.prototype.hasOwnProperty.call(GITHUB_CONTEXT_MAP, match)) {
|
|
183
|
+
records.push({
|
|
184
|
+
gitlabPath: ctx.gitlabPath,
|
|
185
|
+
sourceKey: ctx.sourceKey,
|
|
186
|
+
sourceFile: ctx.sourceFile,
|
|
187
|
+
category: "rewrite",
|
|
188
|
+
rule: "MIG-EXPR-CONTEXT",
|
|
189
|
+
note: `${match} → ${GITHUB_CONTEXT_MAP[match]}`,
|
|
190
|
+
});
|
|
191
|
+
return GITHUB_CONTEXT_MAP[match];
|
|
192
|
+
}
|
|
193
|
+
const userVar = /^(env|vars|secrets|inputs|matrix)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(match);
|
|
194
|
+
if (userVar) {
|
|
195
|
+
records.push({
|
|
196
|
+
gitlabPath: ctx.gitlabPath,
|
|
197
|
+
sourceKey: ctx.sourceKey,
|
|
198
|
+
sourceFile: ctx.sourceFile,
|
|
199
|
+
category: "rewrite",
|
|
200
|
+
rule: "MIG-EXPR-USERVAR",
|
|
201
|
+
note: `${match} → $${userVar[2]}`,
|
|
202
|
+
});
|
|
203
|
+
return `$${userVar[2]}`;
|
|
204
|
+
}
|
|
205
|
+
const stepsOutput = /^steps\.[A-Za-z_][A-Za-z0-9_-]*\.outputs\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(match);
|
|
206
|
+
if (stepsOutput) {
|
|
207
|
+
records.push({
|
|
208
|
+
gitlabPath: ctx.gitlabPath,
|
|
209
|
+
sourceKey: ctx.sourceKey,
|
|
210
|
+
sourceFile: ctx.sourceFile,
|
|
211
|
+
category: "needs-review",
|
|
212
|
+
rule: "MIG-NEEDS-OUTPUTS-001",
|
|
213
|
+
note: `${match} requires artifacts:reports:dotenv pattern; emitting $${stepsOutput[1]} placeholder`,
|
|
214
|
+
});
|
|
215
|
+
return `$${stepsOutput[1]}`;
|
|
216
|
+
}
|
|
217
|
+
const needsOutput = /^needs\.[A-Za-z_][A-Za-z0-9_-]*\.outputs\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(match);
|
|
218
|
+
if (needsOutput) {
|
|
219
|
+
records.push({
|
|
220
|
+
gitlabPath: ctx.gitlabPath,
|
|
221
|
+
sourceKey: ctx.sourceKey,
|
|
222
|
+
sourceFile: ctx.sourceFile,
|
|
223
|
+
category: "needs-review",
|
|
224
|
+
rule: "MIG-NEEDS-OUTPUTS-001",
|
|
225
|
+
note: `${match} requires artifacts:reports:dotenv pattern; emitting $${needsOutput[1]} placeholder`,
|
|
226
|
+
});
|
|
227
|
+
return `$${needsOutput[1]}`;
|
|
228
|
+
}
|
|
229
|
+
if (NO_EQUIVALENT.has(match)) {
|
|
230
|
+
records.push({
|
|
231
|
+
gitlabPath: ctx.gitlabPath,
|
|
232
|
+
sourceKey: ctx.sourceKey,
|
|
233
|
+
sourceFile: ctx.sourceFile,
|
|
234
|
+
category: "needs-review",
|
|
235
|
+
rule: "MIG-EXPR-NO-EQUIV",
|
|
236
|
+
note: `${match} has no GitLab predefined variable equivalent`,
|
|
237
|
+
});
|
|
238
|
+
return match;
|
|
239
|
+
}
|
|
240
|
+
return match;
|
|
241
|
+
});
|
|
242
|
+
return { output, provenance: records };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Translate a GitHub `if:` condition expression (without `${{ }}` wrapping)
|
|
246
|
+
* into a GitLab `rules:if:` expression. Returns substituted expression and
|
|
247
|
+
* any whenClause hint extracted from boolean function calls.
|
|
248
|
+
*/
|
|
249
|
+
export function translateIfCondition(rawIf, ctx) {
|
|
250
|
+
const records = [];
|
|
251
|
+
let whenClause;
|
|
252
|
+
// If the whole expression is a single boolean function, translate to when:
|
|
253
|
+
const fnAlone = /^\s*([a-z]+\(\))\s*$/i.exec(rawIf);
|
|
254
|
+
if (fnAlone) {
|
|
255
|
+
const fnResult = translateIfFunction(fnAlone[1]);
|
|
256
|
+
if (fnResult.whenClause) {
|
|
257
|
+
records.push({
|
|
258
|
+
gitlabPath: ctx.gitlabPath,
|
|
259
|
+
sourceKey: ctx.sourceKey,
|
|
260
|
+
sourceFile: ctx.sourceFile,
|
|
261
|
+
category: "rewrite",
|
|
262
|
+
rule: "MIG-IF-WHEN",
|
|
263
|
+
note: `if: ${fnAlone[1]} → when: ${fnResult.whenClause}`,
|
|
264
|
+
});
|
|
265
|
+
whenClause = fnResult.whenClause;
|
|
266
|
+
return { ifExpression: "", whenClause, provenance: records };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Strip any `${{ }}` wrappers first
|
|
270
|
+
const unwrapped = rawIf.replace(/\$\{\{\s*/g, "").replace(/\s*\}\}/g, "");
|
|
271
|
+
// Then substitute identifiers inline
|
|
272
|
+
const subbed = substituteIdentifiers(unwrapped, ctx);
|
|
273
|
+
records.push(...subbed.provenance);
|
|
274
|
+
return { ifExpression: subbed.output, whenClause, provenance: records };
|
|
275
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for the GitHub Actions → GitLab CI migration tool.
|
|
3
|
+
*
|
|
4
|
+
* Lazy-imports `@intentius/chant-lexicon-github` so users who don't need
|
|
5
|
+
* migration don't pay the install cost (the github lexicon is an optional
|
|
6
|
+
* peer dependency of `@intentius/chant-lexicon-gitlab`).
|
|
7
|
+
*/
|
|
8
|
+
import { ProvenanceAccumulator } from "./provenance.js";
|
|
9
|
+
import { transformIR } from "./transformer.js";
|
|
10
|
+
import { emitGitlabYaml } from "./emit-yaml.js";
|
|
11
|
+
import { provenanceToDiagnostics } from "./diagnostics.js";
|
|
12
|
+
import { analyzeSecurity, runSecurityChecks, renderSecurityPosture } from "./security.js";
|
|
13
|
+
import { GitLabGenerator } from "../../import/generator.js";
|
|
14
|
+
import { applyComposites } from "./composites/rewriter.js";
|
|
15
|
+
// Importing `./actions/index` triggers auto-registration of Tier 1
|
|
16
|
+
// marketplace action mappings into the default registry. This is the
|
|
17
|
+
// single chokepoint where the registry is wired up.
|
|
18
|
+
import "./actions/index.js";
|
|
19
|
+
/**
|
|
20
|
+
* Migrate a GitHub Actions workflow YAML into GitLab CI.
|
|
21
|
+
*
|
|
22
|
+
* @param yamlContent raw .github/workflows/*.yml content
|
|
23
|
+
* @param opts migration options
|
|
24
|
+
*/
|
|
25
|
+
export async function transform(yamlContent, opts = {}) {
|
|
26
|
+
// Lazy-import the GitHub parser to keep the github lexicon dep optional.
|
|
27
|
+
let GitHubActionsParser;
|
|
28
|
+
try {
|
|
29
|
+
({ GitHubActionsParser } = await import("@intentius/chant-lexicon-github/import/parser"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error("chant migrate from github requires @intentius/chant-lexicon-github. " +
|
|
33
|
+
"Install it: npm install --save-dev @intentius/chant-lexicon-github");
|
|
34
|
+
}
|
|
35
|
+
const ghIR = new GitHubActionsParser().parse(yamlContent);
|
|
36
|
+
const provAcc = new ProvenanceAccumulator();
|
|
37
|
+
const transformed = await transformIR(ghIR, {
|
|
38
|
+
sourceFile: opts.sourceFile,
|
|
39
|
+
registry: opts.registry,
|
|
40
|
+
provenance: provAcc,
|
|
41
|
+
});
|
|
42
|
+
let { ir } = transformed;
|
|
43
|
+
const stages = transformed.stages;
|
|
44
|
+
// --use-composites: opt-in IR rewrite that turns recognised shapes
|
|
45
|
+
// (NodePipeline / NodeCI) into composite calls.
|
|
46
|
+
if (opts.useComposites) {
|
|
47
|
+
const r = applyComposites(ir);
|
|
48
|
+
ir = r.ir;
|
|
49
|
+
provAcc.pushAll(r.provenance);
|
|
50
|
+
}
|
|
51
|
+
let output;
|
|
52
|
+
if (opts.emit === "ts") {
|
|
53
|
+
const generator = new GitLabGenerator();
|
|
54
|
+
const files = generator.generate(ir);
|
|
55
|
+
// For single-file emit (default), concatenate. The migration banner
|
|
56
|
+
// + per-resource provenance comments are interleaved.
|
|
57
|
+
const banner = `// Migrated from ${opts.sourceFile ?? "(stdin)"} by chant migrate.
|
|
58
|
+
// Source tool: github-actions. Edit freely — chant build will pick this up.\n\n`;
|
|
59
|
+
output = banner + files.map((f) => f.content).join("\n");
|
|
60
|
+
// Append NeedsReview TODOs at the bottom for visibility.
|
|
61
|
+
const todos = provAcc.byCategory("needs-review");
|
|
62
|
+
if (todos.length > 0) {
|
|
63
|
+
output += "\n// TODO(migration): items needing manual review:\n";
|
|
64
|
+
for (const t of todos) {
|
|
65
|
+
output += `// - ${t.rule}: ${t.note ?? ""}\n`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
output = emitGitlabYaml(ir);
|
|
71
|
+
}
|
|
72
|
+
// Security-aware migration (#306): classify each security property's fate
|
|
73
|
+
// across the edge, and run the GitLab security post-synth checks against the
|
|
74
|
+
// migrated YAML so weaknesses lost in translation surface on the target side.
|
|
75
|
+
if (opts.security) {
|
|
76
|
+
const yamlForSecurity = opts.emit === "ts" ? emitGitlabYaml(ir) : output;
|
|
77
|
+
provAcc.pushAll(analyzeSecurity(yamlContent, { sourceFile: opts.sourceFile }));
|
|
78
|
+
provAcc.pushAll(await runSecurityChecks(yamlForSecurity, { sourceFile: opts.sourceFile }));
|
|
79
|
+
}
|
|
80
|
+
const provenance = provAcc.all();
|
|
81
|
+
const diagnostics = provenanceToDiagnostics(provenance, { strict: opts.strict });
|
|
82
|
+
const securityPosture = renderSecurityPosture(provenance);
|
|
83
|
+
return { ir, output, provenance, diagnostics, stages, securityPosture };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Lightweight detector: does this YAML look like a GitHub Actions workflow?
|
|
87
|
+
* Used by the plugin's `migrationSource("github")` extension.
|
|
88
|
+
*/
|
|
89
|
+
export function detectGitHubWorkflow(content) {
|
|
90
|
+
// Cheap detection: top-level `jobs:` + `on:` or `runs-on:` appearing nested.
|
|
91
|
+
if (!/^\s*jobs\s*:/m.test(content))
|
|
92
|
+
return false;
|
|
93
|
+
return /^\s*on\s*:/m.test(content) || /^\s*runs-on\s*:/m.test(content);
|
|
94
|
+
}
|
|
95
|
+
export { ProvenanceAccumulator } from "./provenance.js";
|
|
96
|
+
export { createRegistry, getDefaultRegistry, lookupAction } from "./actions/registry.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-node provenance for GitHub Actions → GitLab CI migration.
|
|
3
|
+
*
|
|
4
|
+
* Returned as a side channel from `transform()` so the IR stays clean
|
|
5
|
+
* (re-usable by `chant import`) while still capturing per-key migration
|
|
6
|
+
* history for SARIF/Markdown reporting.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Accumulator passed through the transformer pipeline so any step can
|
|
10
|
+
* append to the same provenance list without threading return values.
|
|
11
|
+
*/
|
|
12
|
+
export class ProvenanceAccumulator {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.records = [];
|
|
15
|
+
}
|
|
16
|
+
push(record) {
|
|
17
|
+
this.records.push(record);
|
|
18
|
+
}
|
|
19
|
+
pushAll(records) {
|
|
20
|
+
this.records.push(...records);
|
|
21
|
+
}
|
|
22
|
+
all() {
|
|
23
|
+
return [...this.records];
|
|
24
|
+
}
|
|
25
|
+
byCategory(category) {
|
|
26
|
+
return this.records.filter((r) => r.category === category);
|
|
27
|
+
}
|
|
28
|
+
needsReviewCount() {
|
|
29
|
+
return this.records.filter((r) => r.category === "needs-review").length;
|
|
30
|
+
}
|
|
31
|
+
/** Records carrying a security classification. */
|
|
32
|
+
securityRecords() {
|
|
33
|
+
return this.records.filter((r) => r.security !== undefined);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static LintRule metadata for SARIF enrichment.
|
|
3
|
+
*
|
|
4
|
+
* Each rule fired by the transformer (provenance.rule field) gets a
|
|
5
|
+
* corresponding entry here so SARIF output carries description + helpUri.
|
|
6
|
+
*
|
|
7
|
+
* These rules don't actually run a `check()` — the work has already been
|
|
8
|
+
* done by the transformer. They're metadata-only.
|
|
9
|
+
*/
|
|
10
|
+
const noopCheck = () => [];
|
|
11
|
+
function rule(id, severity, description) {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
severity,
|
|
15
|
+
category: "correctness",
|
|
16
|
+
description,
|
|
17
|
+
helpUri: `https://intentius.io/chant/lexicons/gitlab/migration#${id.toLowerCase()}`,
|
|
18
|
+
check: noopCheck,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export const MIGRATION_RULES = [
|
|
22
|
+
// Trigger translations
|
|
23
|
+
rule("MIG-ON-PUSH", "info", "GitHub on: push → workflow.rules push"),
|
|
24
|
+
rule("MIG-ON-PR", "info", "GitHub on: pull_request → workflow.rules merge_request_event"),
|
|
25
|
+
rule("MIG-ON-SCHEDULE", "warning", "GitHub on: schedule requires GitLab CI/CD > Schedules UI configuration"),
|
|
26
|
+
rule("MIG-ON-DISPATCH", "warning", "GitHub on: workflow_dispatch inputs require spec:inputs (GitLab 17+) with defaults"),
|
|
27
|
+
rule("MIG-ON-NON-GIT", "warning", "GitHub event has no GitLab equivalent — GitLab pipelines run on git events only"),
|
|
28
|
+
rule("MIG-ON-UNKNOWN", "warning", "Unknown GitHub trigger event"),
|
|
29
|
+
// Job-level translations
|
|
30
|
+
rule("MIG-WORKFLOW-NAME", "info", "name → workflow.name"),
|
|
31
|
+
rule("MIG-WORKFLOW-ENV", "info", "Workflow env → top-level variables"),
|
|
32
|
+
rule("MIG-JOB-ENV", "info", "Job env → variables"),
|
|
33
|
+
rule("MIG-TIMEOUT", "info", "timeout-minutes → timeout"),
|
|
34
|
+
rule("MIG-ALLOW-FAILURE", "info", "continue-on-error → allow_failure"),
|
|
35
|
+
rule("MIG-NEEDS", "info", "needs: passthrough"),
|
|
36
|
+
rule("MIG-CONCURRENCY", "info", "concurrency.group/cancel-in-progress → resource_group/interruptible"),
|
|
37
|
+
rule("MIG-SERVICES", "info", "services: passthrough"),
|
|
38
|
+
rule("MIG-CONTAINER", "info", "container.image → image"),
|
|
39
|
+
rule("MIG-MATRIX", "info", "strategy.matrix → parallel.matrix"),
|
|
40
|
+
// runs-on
|
|
41
|
+
rule("MIG-RUNS-ON-001", "info", "runs-on: linux runner → Docker image"),
|
|
42
|
+
rule("MIG-RUNS-ON-NON-LINUX", "warning", "Non-Linux runs-on requires self-hosted runner with tag"),
|
|
43
|
+
rule("MIG-RUNS-ON-TAG", "info", "Custom runs-on label → tags:"),
|
|
44
|
+
// Expressions
|
|
45
|
+
rule("MIG-EXPR-CONTEXT", "info", "github.*/runner.*/job.* → predefined $CI_* variable"),
|
|
46
|
+
rule("MIG-EXPR-USERVAR", "info", "env.*/vars.*/secrets.*/inputs.*/matrix.* → $NAME"),
|
|
47
|
+
rule("MIG-EXPR-FUNCTION", "info", "Boolean function → when: clause"),
|
|
48
|
+
rule("MIG-EXPR-NO-EQUIV", "warning", "GitHub expression has no GitLab equivalent"),
|
|
49
|
+
rule("MIG-EXPR-UNKNOWN", "warning", "Could not translate expression"),
|
|
50
|
+
// Rule conversions
|
|
51
|
+
rule("MIG-IF-WHEN", "info", "if: boolean_function() → when:"),
|
|
52
|
+
// Permissions and outputs
|
|
53
|
+
rule("MIG-PERMISSIONS-001", "warning", "GitHub permissions: has no per-job equivalent — configure CI/CD token at project level"),
|
|
54
|
+
rule("MIG-JOB-OUTPUTS", "warning", "GitHub job outputs require artifacts:reports:dotenv pattern in GitLab"),
|
|
55
|
+
rule("MIG-NEEDS-OUTPUTS-001", "warning", "steps/needs outputs require artifacts:reports:dotenv pattern"),
|
|
56
|
+
// Matrix
|
|
57
|
+
rule("MIG-MATRIX-INCLUDE-001", "warning", "matrix.include/exclude has no direct GitLab equivalent"),
|
|
58
|
+
rule("MIG-FAIL-FAST", "warning", "strategy.fail-fast has no GitLab equivalent (default behaviour is non-fail-fast)"),
|
|
59
|
+
// Reusable workflow + step env
|
|
60
|
+
rule("MIG-REUSABLE-WORKFLOW", "warning", "GitHub reusable workflow → GitLab include: with variable substitution (no typed inputs)"),
|
|
61
|
+
rule("MIG-STEP-ENV-CONFLICT", "warning", "Step-level env var conflict; using last value"),
|
|
62
|
+
// Stage inference
|
|
63
|
+
rule("MIG-STAGE-TOPO", "info", "Stage assigned by needs: depth"),
|
|
64
|
+
rule("MIG-STAGE-HEURISTIC", "info", "Stage assigned by job name heuristic"),
|
|
65
|
+
rule("MIG-NEEDS-CYCLE-001", "warning", "needs: cycle detected; each cycle member in its own stage"),
|
|
66
|
+
// Action mapping fallback
|
|
67
|
+
rule("MIG-ACTION-UNKNOWN", "warning", "Marketplace action has no registered mapping"),
|
|
68
|
+
// ── Security posture across the migration edge (#306) ──────────────────
|
|
69
|
+
// Each cross-references the endpoint issue that re-establishes the property.
|
|
70
|
+
rule("MIG-PIN-LOST", "warning", "A pinned action SHA could not be carried to the GitLab include/image — re-pin on the GitLab side (#297)"),
|
|
71
|
+
rule("MIG-SECRET-UNSCOPED", "warning", "Migrated secret reference assumes a masked + protected GitLab variable (#300)"),
|
|
72
|
+
rule("MIG-INJECTION-CARRIED", "warning", "An untrusted-input injection site was translated verbatim across the edge (#299)"),
|
|
73
|
+
rule("MIG-TRUST-BOUNDARY", "warning", "Trigger/fork semantics shifted the trust boundary (pull_request_target → MR/fork pipeline) (#299)"),
|
|
74
|
+
];
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security-aware migration (#306).
|
|
3
|
+
*
|
|
4
|
+
* Migration is the edge between the two security endpoints (#296 ↔ #305).
|
|
5
|
+
* Security properties don't translate 1:1 — some are silently preserved, some
|
|
6
|
+
* lost, some need re-establishing on the GitLab side. This module classifies
|
|
7
|
+
* each security-relevant property's fate as it crosses the boundary and runs
|
|
8
|
+
* the GitLab security post-synth checks against the migrated output so anything
|
|
9
|
+
* lost in translation is caught on the target side.
|
|
10
|
+
*/
|
|
11
|
+
const UNTRUSTED_CONTEXTS = [
|
|
12
|
+
"github.event.issue.title",
|
|
13
|
+
"github.event.issue.body",
|
|
14
|
+
"github.event.pull_request.title",
|
|
15
|
+
"github.event.pull_request.body",
|
|
16
|
+
"github.event.pull_request.head.ref",
|
|
17
|
+
"github.event.comment.body",
|
|
18
|
+
"github.event.review.body",
|
|
19
|
+
"github.event.head_commit.message",
|
|
20
|
+
"github.head_ref",
|
|
21
|
+
];
|
|
22
|
+
const SHA_PINNED_USES = /uses:\s*['"]?([\w.-]+\/[\w.-]+(?:\/[\w.-]+)*)@([0-9a-f]{40})['"]?/g;
|
|
23
|
+
const SECRET_REF = /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
24
|
+
/**
|
|
25
|
+
* Classify the fate of security-relevant properties as they migrate from a
|
|
26
|
+
* GitHub Actions workflow to GitLab CI. Returns provenance records carrying a
|
|
27
|
+
* `security` classification (which flow into diagnostics, SARIF, and the report
|
|
28
|
+
* like any other provenance record).
|
|
29
|
+
*/
|
|
30
|
+
export function analyzeSecurity(sourceYaml, opts = {}) {
|
|
31
|
+
const records = [];
|
|
32
|
+
const file = opts.sourceFile;
|
|
33
|
+
// MIG-PIN-LOST — a GitHub SHA pin has no meaning once the action maps to a
|
|
34
|
+
// GitLab include/image (or becomes a hand-written script). Re-pin on GitLab.
|
|
35
|
+
const seenPins = new Set();
|
|
36
|
+
let m;
|
|
37
|
+
SHA_PINNED_USES.lastIndex = 0;
|
|
38
|
+
while ((m = SHA_PINNED_USES.exec(sourceYaml)) !== null) {
|
|
39
|
+
const slug = m[1];
|
|
40
|
+
if (seenPins.has(slug))
|
|
41
|
+
continue;
|
|
42
|
+
seenPins.add(slug);
|
|
43
|
+
records.push({
|
|
44
|
+
gitlabPath: "(target)",
|
|
45
|
+
sourceKey: `uses: ${slug}`,
|
|
46
|
+
sourceFile: file,
|
|
47
|
+
category: "synthesis",
|
|
48
|
+
rule: "MIG-PIN-LOST",
|
|
49
|
+
note: `The SHA pin on "${slug}" does not carry to its GitLab include/image — re-pin the GitLab target to a tag or digest (WGL029/WGL031).`,
|
|
50
|
+
security: { property: "Pinned action SHA", fate: "lost", severity: "warning", reestablish: "#297" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// MIG-SECRET-UNSCOPED — ${{ secrets.X }} → $X, but masking/protection is set
|
|
54
|
+
// outside the YAML; the reference assumes a masked + protected variable.
|
|
55
|
+
const seenSecrets = new Set();
|
|
56
|
+
SECRET_REF.lastIndex = 0;
|
|
57
|
+
while ((m = SECRET_REF.exec(sourceYaml)) !== null) {
|
|
58
|
+
const name = m[1];
|
|
59
|
+
if (seenSecrets.has(name))
|
|
60
|
+
continue;
|
|
61
|
+
seenSecrets.add(name);
|
|
62
|
+
records.push({
|
|
63
|
+
gitlabPath: "(target)",
|
|
64
|
+
sourceKey: `secrets.${name}`,
|
|
65
|
+
sourceFile: file,
|
|
66
|
+
category: "needs-review",
|
|
67
|
+
rule: "MIG-SECRET-UNSCOPED",
|
|
68
|
+
note: `secrets.${name} → $${name}, which assumes a masked + protected GitLab CI/CD variable. Mark it masked and protected (WGL038).`,
|
|
69
|
+
security: { property: "Secret reference", fate: "needs-review", severity: "warning", reestablish: "#300" },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// MIG-INJECTION-CARRIED — an untrusted-context interpolation translates
|
|
73
|
+
// verbatim, carrying the injection straight across.
|
|
74
|
+
const seenInjection = new Set();
|
|
75
|
+
for (const ctx of UNTRUSTED_CONTEXTS) {
|
|
76
|
+
if (sourceYaml.includes(ctx) && !seenInjection.has(ctx)) {
|
|
77
|
+
seenInjection.add(ctx);
|
|
78
|
+
records.push({
|
|
79
|
+
gitlabPath: "(target)",
|
|
80
|
+
sourceKey: ctx,
|
|
81
|
+
sourceFile: file,
|
|
82
|
+
category: "needs-review",
|
|
83
|
+
rule: "MIG-INJECTION-CARRIED",
|
|
84
|
+
note: `Untrusted input \${{ ${ctx} }} translates to a $CI_* reference verbatim — the script-injection risk carries across (WGL035).`,
|
|
85
|
+
security: { property: "Template injection site", fate: "translated", severity: "warning", reestablish: "#299" },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// MIG-TRUST-BOUNDARY — pull_request_target maps to merge-request pipelines,
|
|
90
|
+
// which fork contributors can trigger; the trust boundary shifts.
|
|
91
|
+
if (/pull_request_target/.test(sourceYaml)) {
|
|
92
|
+
records.push({
|
|
93
|
+
gitlabPath: "(target)",
|
|
94
|
+
sourceKey: "on.pull_request_target",
|
|
95
|
+
sourceFile: file,
|
|
96
|
+
category: "needs-review",
|
|
97
|
+
rule: "MIG-TRUST-BOUNDARY",
|
|
98
|
+
note: `pull_request_target → merge-request pipelines, which fork contributors can trigger. Re-establish the trust boundary (protected refs, approvals) on GitLab (WGL034/WGL036).`,
|
|
99
|
+
security: { property: "Trigger trust boundary", fate: "needs-review", severity: "warning", reestablish: "#299" },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// MIG-PERMISSIONS-001 (reframed as least-privilege) — permissions: has no
|
|
103
|
+
// per-job GitLab YAML equivalent; least privilege lives in project settings.
|
|
104
|
+
if (/^permissions:/m.test(sourceYaml) || /^\s+permissions:/m.test(sourceYaml)) {
|
|
105
|
+
records.push({
|
|
106
|
+
gitlabPath: "(target)",
|
|
107
|
+
sourceKey: "permissions",
|
|
108
|
+
sourceFile: file,
|
|
109
|
+
category: "needs-review",
|
|
110
|
+
rule: "MIG-PERMISSIONS-001",
|
|
111
|
+
note: `Least-privilege permissions: have no per-job GitLab YAML equivalent — re-establish via project CI/CD token settings and id_tokens scoping (WGL033).`,
|
|
112
|
+
security: { property: "Least-privilege permissions", fate: "needs-review", severity: "warning", reestablish: "#298" },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return records;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run the GitLab security post-synth checks against the migrated `.gitlab-ci.yml`
|
|
119
|
+
* and return their findings as provenance records — *migrate, then prove the
|
|
120
|
+
* output clears the GitLab security bar*. Anything lost in translation that
|
|
121
|
+
* lands as a concrete weakness on the target is caught here.
|
|
122
|
+
*/
|
|
123
|
+
export async function runSecurityChecks(gitlabYaml, opts = {}) {
|
|
124
|
+
const { gitlabPlugin } = await import("../../plugin.js");
|
|
125
|
+
const checks = gitlabPlugin.postSynthChecks?.() ?? [];
|
|
126
|
+
const ctx = {
|
|
127
|
+
outputs: new Map([["gitlab", gitlabYaml]]),
|
|
128
|
+
entities: new Map(),
|
|
129
|
+
buildResult: {
|
|
130
|
+
outputs: new Map([["gitlab", gitlabYaml]]),
|
|
131
|
+
entities: new Map(),
|
|
132
|
+
warnings: [],
|
|
133
|
+
errors: [],
|
|
134
|
+
sourceFileCount: 1,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const records = [];
|
|
138
|
+
for (const check of checks) {
|
|
139
|
+
let diags;
|
|
140
|
+
try {
|
|
141
|
+
diags = check.check(ctx);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue; // a check that needs entities/other context simply yields nothing here
|
|
145
|
+
}
|
|
146
|
+
for (const d of diags) {
|
|
147
|
+
records.push({
|
|
148
|
+
gitlabPath: "(target)",
|
|
149
|
+
sourceKey: d.entity ?? d.checkId,
|
|
150
|
+
sourceFile: opts.sourceFile,
|
|
151
|
+
category: "synthesis",
|
|
152
|
+
rule: d.checkId,
|
|
153
|
+
note: `Migrated output triggers ${d.checkId}: ${d.message}`,
|
|
154
|
+
security: {
|
|
155
|
+
property: "GitLab security check",
|
|
156
|
+
fate: "needs-review",
|
|
157
|
+
severity: d.severity === "error" ? "error" : d.severity === "info" ? "info" : "warning",
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return records;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Render a "Security posture" Markdown section from the security provenance
|
|
166
|
+
* records (those carrying a `security` classification).
|
|
167
|
+
*/
|
|
168
|
+
export function renderSecurityPosture(records) {
|
|
169
|
+
const security = records.filter((r) => r.security);
|
|
170
|
+
if (security.length === 0) {
|
|
171
|
+
return "## Security posture\n\nNo security-relevant properties detected at the migration edge.\n";
|
|
172
|
+
}
|
|
173
|
+
const byRule = new Map();
|
|
174
|
+
for (const r of security) {
|
|
175
|
+
const s = r.security;
|
|
176
|
+
const existing = byRule.get(r.rule);
|
|
177
|
+
if (existing)
|
|
178
|
+
existing.count += 1;
|
|
179
|
+
else
|
|
180
|
+
byRule.set(r.rule, { property: s.property, fate: s.fate, rule: r.rule, count: 1 });
|
|
181
|
+
}
|
|
182
|
+
let out = "## Security posture\n\n";
|
|
183
|
+
out += "| Property | Fate | Rule | Count |\n|---|---|---|---|\n";
|
|
184
|
+
for (const line of byRule.values()) {
|
|
185
|
+
out += `| ${line.property} | ${line.fate} | ${line.rule} | ${line.count} |\n`;
|
|
186
|
+
}
|
|
187
|
+
out += "\nFates: **translated** (weakness carried as-is) · **approximated** · **needs-review** (re-establish on GitLab) · **lost** (no GitLab equivalent for the property).\n";
|
|
188
|
+
return out;
|
|
189
|
+
}
|