@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.
- 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,24 @@
|
|
|
1
|
+
# Generated by chant migrate from input.yml
|
|
2
|
+
|
|
3
|
+
stages:
|
|
4
|
+
- build
|
|
5
|
+
- deploy
|
|
6
|
+
workflow:
|
|
7
|
+
rules:
|
|
8
|
+
- if: '$CI_PIPELINE_SOURCE == "push"'
|
|
9
|
+
|
|
10
|
+
deploy:
|
|
11
|
+
image: ubuntu:24.04
|
|
12
|
+
rules:
|
|
13
|
+
- if: '$CI_COMMIT_REF_NAME == ''refs/heads/main'' && $CI_PIPELINE_SOURCE == ''push'''
|
|
14
|
+
script:
|
|
15
|
+
- make deploy
|
|
16
|
+
stage: deploy
|
|
17
|
+
|
|
18
|
+
notify:
|
|
19
|
+
image: ubuntu:24.04
|
|
20
|
+
rules:
|
|
21
|
+
- when: on_failure
|
|
22
|
+
script:
|
|
23
|
+
- ./notify-slack.sh
|
|
24
|
+
stage: build
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by chant migrate from input.yml
|
|
2
|
+
|
|
3
|
+
stages:
|
|
4
|
+
- test
|
|
5
|
+
workflow:
|
|
6
|
+
rules:
|
|
7
|
+
- if: '$CI_PIPELINE_SOURCE == "push"'
|
|
8
|
+
|
|
9
|
+
test:
|
|
10
|
+
image: node:20-alpine
|
|
11
|
+
services:
|
|
12
|
+
- name: postgres:15
|
|
13
|
+
alias: postgres
|
|
14
|
+
variables:
|
|
15
|
+
POSTGRES_PASSWORD: secret
|
|
16
|
+
script:
|
|
17
|
+
- npm test
|
|
18
|
+
stage: test
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"totals": {
|
|
3
|
+
"error": 0,
|
|
4
|
+
"warning": 1,
|
|
5
|
+
"info": 0
|
|
6
|
+
},
|
|
7
|
+
"ruleIds": [
|
|
8
|
+
"MIG-PERMISSIONS-001"
|
|
9
|
+
],
|
|
10
|
+
"needsReview": [
|
|
11
|
+
{
|
|
12
|
+
"line": 1,
|
|
13
|
+
"ruleId": "MIG-PERMISSIONS-001"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"provenance": {
|
|
17
|
+
"literalRewrites": 6,
|
|
18
|
+
"actionMappings": []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by chant migrate from input.yml
|
|
2
|
+
|
|
3
|
+
stages:
|
|
4
|
+
- build
|
|
5
|
+
workflow:
|
|
6
|
+
rules:
|
|
7
|
+
- if: '$CI_PIPELINE_SOURCE == "push"'
|
|
8
|
+
|
|
9
|
+
flaky-test:
|
|
10
|
+
image: ubuntu:24.04
|
|
11
|
+
timeout: '10 minutes'
|
|
12
|
+
allow_failure: true
|
|
13
|
+
resource_group: deploy-$CI_COMMIT_REF_NAME
|
|
14
|
+
interruptible: true
|
|
15
|
+
script:
|
|
16
|
+
- make test
|
|
17
|
+
stage: build
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture-driven migration tests.
|
|
3
|
+
*
|
|
4
|
+
* Each fixture directory under `fixtures/` contains:
|
|
5
|
+
* - input.yml (GitHub Actions workflow)
|
|
6
|
+
* - expected.gitlab-ci.yml (expected GitLab CI output)
|
|
7
|
+
* - expected-report.json (expected diagnostic + provenance shape)
|
|
8
|
+
*
|
|
9
|
+
* The test asserts canonical-YAML equality on the output and shape
|
|
10
|
+
* equality on the report. Adding a new fixture requires no manifest
|
|
11
|
+
* edit — just drop the three files into a new directory.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, test, expect } from "vitest";
|
|
15
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
18
|
+
import { transform } from "./index";
|
|
19
|
+
|
|
20
|
+
const FIXTURE_ROOT = join(__dirname, "fixtures");
|
|
21
|
+
|
|
22
|
+
interface Fixture { name: string; dir: string }
|
|
23
|
+
|
|
24
|
+
function* walkFixtures(root: string, prefix = ""): Generator<Fixture> {
|
|
25
|
+
for (const entry of readdirSync(root)) {
|
|
26
|
+
const p = join(root, entry);
|
|
27
|
+
if (!statSync(p).isDirectory()) continue;
|
|
28
|
+
const name = prefix ? `${prefix}/${entry}` : entry;
|
|
29
|
+
const files = readdirSync(p);
|
|
30
|
+
if (files.includes("input.yml") && files.includes("expected.gitlab-ci.yml")) {
|
|
31
|
+
yield { name, dir: p };
|
|
32
|
+
} else {
|
|
33
|
+
yield* walkFixtures(p, name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function canonicalYaml(text: string): unknown {
|
|
39
|
+
// Strip leading comments so the "Generated by chant migrate from ..."
|
|
40
|
+
// banner doesn't have to match byte-for-byte when sourceFile differs.
|
|
41
|
+
const stripped = text.replace(/^#[^\n]*\n+/g, "");
|
|
42
|
+
if (!stripped.trim()) return {};
|
|
43
|
+
return parseYAML(stripped);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ReportShape {
|
|
47
|
+
totals: { error: number; warning: number; info: number };
|
|
48
|
+
ruleIds: string[];
|
|
49
|
+
needsReview: Array<{ line: number; ruleId: string }>;
|
|
50
|
+
provenance: { literalRewrites: number; actionMappings: Array<{ action: string; tier: number | undefined }> };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function reportShape(diagnostics: Array<Record<string, unknown>>, provenance: Array<Record<string, unknown>>): ReportShape {
|
|
54
|
+
const totals = { error: 0, warning: 0, info: 0 };
|
|
55
|
+
for (const d of diagnostics) {
|
|
56
|
+
const sev = d.severity as string;
|
|
57
|
+
if (sev in totals) totals[sev as "error" | "warning" | "info"]++;
|
|
58
|
+
}
|
|
59
|
+
const ruleIds = Array.from(new Set(diagnostics.map((d) => d.ruleId as string))).sort();
|
|
60
|
+
const needsReview = diagnostics
|
|
61
|
+
.filter((d) => d.severity === "warning" || d.severity === "error")
|
|
62
|
+
.map((d) => ({ line: d.line as number, ruleId: d.ruleId as string }));
|
|
63
|
+
const literalRewrites = provenance.filter((p) => p.category === "literal" || p.category === "rewrite").length;
|
|
64
|
+
const actionMappings = provenance
|
|
65
|
+
.filter((p) => p.category === "action-map")
|
|
66
|
+
.map((p) => ({ action: p.actionRef as string, tier: p.mappingTier as number | undefined }));
|
|
67
|
+
return { totals, ruleIds, needsReview, provenance: { literalRewrites, actionMappings } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("github → gitlab fixtures", () => {
|
|
71
|
+
for (const f of walkFixtures(FIXTURE_ROOT)) {
|
|
72
|
+
test(f.name, async () => {
|
|
73
|
+
const input = readFileSync(join(f.dir, "input.yml"), "utf-8");
|
|
74
|
+
const expectedYaml = readFileSync(join(f.dir, "expected.gitlab-ci.yml"), "utf-8");
|
|
75
|
+
const expectedReport: ReportShape = JSON.parse(
|
|
76
|
+
readFileSync(join(f.dir, "expected-report.json"), "utf-8"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const result = await transform(input, { sourceFile: "input.yml" });
|
|
80
|
+
|
|
81
|
+
// Canonical YAML compare (parse + re-stringify absorbs whitespace/key order)
|
|
82
|
+
expect(canonicalYaml(result.output)).toEqual(canonicalYaml(expectedYaml));
|
|
83
|
+
|
|
84
|
+
// Shape compare
|
|
85
|
+
const actualShape = reportShape(
|
|
86
|
+
result.diagnostics as unknown as Array<Record<string, unknown>>,
|
|
87
|
+
result.provenance as unknown as Array<Record<string, unknown>>,
|
|
88
|
+
);
|
|
89
|
+
expect(actualShape).toEqual(expectedReport);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
|
|
9
|
+
import type { TemplateIR } from "@intentius/chant/import/parser";
|
|
10
|
+
import type { LintDiagnostic } from "@intentius/chant/lint/rule";
|
|
11
|
+
import { ProvenanceAccumulator, type ProvenanceRecord } from "./provenance";
|
|
12
|
+
import { transformIR } from "./transformer";
|
|
13
|
+
import { emitGitlabYaml } from "./emit-yaml";
|
|
14
|
+
import { provenanceToDiagnostics } from "./diagnostics";
|
|
15
|
+
import { GitLabGenerator } from "../../import/generator";
|
|
16
|
+
import { applyComposites } from "./composites/rewriter";
|
|
17
|
+
import type { ActionMappingRegistry } from "./actions/registry";
|
|
18
|
+
// Importing `./actions/index` triggers auto-registration of Tier 1
|
|
19
|
+
// marketplace action mappings into the default registry. This is the
|
|
20
|
+
// single chokepoint where the registry is wired up.
|
|
21
|
+
import "./actions/index";
|
|
22
|
+
|
|
23
|
+
export interface MigrateOptions {
|
|
24
|
+
/** Output format. */
|
|
25
|
+
emit?: "yaml" | "ts";
|
|
26
|
+
/** Enable composite-pattern recognition (Node patterns in v1). */
|
|
27
|
+
useComposites?: boolean;
|
|
28
|
+
/** Source file path for provenance (display only). */
|
|
29
|
+
sourceFile?: string;
|
|
30
|
+
/** Inject a custom action mapping registry for testing. */
|
|
31
|
+
registry?: ActionMappingRegistry;
|
|
32
|
+
/** Escalate needs-review diagnostics to errors. */
|
|
33
|
+
strict?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MigrationResult {
|
|
37
|
+
/** GitLab IR (consumed by `chant import`-style tools downstream). */
|
|
38
|
+
ir: TemplateIR;
|
|
39
|
+
/** Rendered output (YAML by default, TS when emit: "ts"). */
|
|
40
|
+
output: string;
|
|
41
|
+
/** Per-key provenance records. */
|
|
42
|
+
provenance: ProvenanceRecord[];
|
|
43
|
+
/** SARIF-shaped diagnostics derived from provenance. */
|
|
44
|
+
diagnostics: LintDiagnostic[];
|
|
45
|
+
/** Inferred stage list. */
|
|
46
|
+
stages: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Migrate a GitHub Actions workflow YAML into GitLab CI.
|
|
51
|
+
*
|
|
52
|
+
* @param yamlContent raw .github/workflows/*.yml content
|
|
53
|
+
* @param opts migration options
|
|
54
|
+
*/
|
|
55
|
+
export async function transform(
|
|
56
|
+
yamlContent: string,
|
|
57
|
+
opts: MigrateOptions = {},
|
|
58
|
+
): Promise<MigrationResult> {
|
|
59
|
+
// Lazy-import the GitHub parser to keep the github lexicon dep optional.
|
|
60
|
+
let GitHubActionsParser: typeof import("@intentius/chant-lexicon-github/import/parser").GitHubActionsParser;
|
|
61
|
+
try {
|
|
62
|
+
({ GitHubActionsParser } = await import("@intentius/chant-lexicon-github/import/parser"));
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"chant migrate from github requires @intentius/chant-lexicon-github. " +
|
|
66
|
+
"Install it: npm install --save-dev @intentius/chant-lexicon-github",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ghIR = new GitHubActionsParser().parse(yamlContent);
|
|
71
|
+
const provAcc = new ProvenanceAccumulator();
|
|
72
|
+
const transformed = await transformIR(ghIR, {
|
|
73
|
+
sourceFile: opts.sourceFile,
|
|
74
|
+
registry: opts.registry,
|
|
75
|
+
provenance: provAcc,
|
|
76
|
+
});
|
|
77
|
+
let { ir } = transformed;
|
|
78
|
+
const stages = transformed.stages;
|
|
79
|
+
|
|
80
|
+
// --use-composites: opt-in IR rewrite that turns recognised shapes
|
|
81
|
+
// (NodePipeline / NodeCI) into composite calls.
|
|
82
|
+
if (opts.useComposites) {
|
|
83
|
+
const r = applyComposites(ir);
|
|
84
|
+
ir = r.ir;
|
|
85
|
+
provAcc.pushAll(r.provenance);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let output: string;
|
|
89
|
+
if (opts.emit === "ts") {
|
|
90
|
+
const generator = new GitLabGenerator();
|
|
91
|
+
const files = generator.generate(ir);
|
|
92
|
+
// For single-file emit (default), concatenate. The migration banner
|
|
93
|
+
// + per-resource provenance comments are interleaved.
|
|
94
|
+
const banner = `// Migrated from ${opts.sourceFile ?? "(stdin)"} by chant migrate.
|
|
95
|
+
// Source tool: github-actions. Edit freely — chant build will pick this up.\n\n`;
|
|
96
|
+
output = banner + files.map((f) => f.content).join("\n");
|
|
97
|
+
// Append NeedsReview TODOs at the bottom for visibility.
|
|
98
|
+
const todos = provAcc.byCategory("needs-review");
|
|
99
|
+
if (todos.length > 0) {
|
|
100
|
+
output += "\n// TODO(migration): items needing manual review:\n";
|
|
101
|
+
for (const t of todos) {
|
|
102
|
+
output += `// - ${t.rule}: ${t.note ?? ""}\n`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
output = emitGitlabYaml(ir);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const provenance = provAcc.all();
|
|
110
|
+
const diagnostics = provenanceToDiagnostics(provenance, { strict: opts.strict });
|
|
111
|
+
|
|
112
|
+
return { ir, output, provenance, diagnostics, stages };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Lightweight detector: does this YAML look like a GitHub Actions workflow?
|
|
117
|
+
* Used by the plugin's `migrationSource("github")` extension.
|
|
118
|
+
*/
|
|
119
|
+
export function detectGitHubWorkflow(content: string): boolean {
|
|
120
|
+
// Cheap detection: top-level `jobs:` + `on:` or `runs-on:` appearing nested.
|
|
121
|
+
if (!/^\s*jobs\s*:/m.test(content)) return false;
|
|
122
|
+
return /^\s*on\s*:/m.test(content) || /^\s*runs-on\s*:/m.test(content);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { ProvenanceAccumulator } from "./provenance";
|
|
126
|
+
export type { ProvenanceRecord, ProvenanceCategory } from "./provenance";
|
|
127
|
+
export type { ActionMapping, ActionMapCtx, ActionMappedResult, ActionMappingRegistry } from "./actions/registry";
|
|
128
|
+
export { createRegistry, getDefaultRegistry, lookupAction } from "./actions/registry";
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
export type ProvenanceCategory =
|
|
10
|
+
| "literal" // direct YAML key rename (env → variables, runs-on → image)
|
|
11
|
+
| "rewrite" // expression/identifier substitution (github.ref → $CI_COMMIT_REF_NAME)
|
|
12
|
+
| "synthesis" // emitted GitLab construct with no GH original (inferred stage)
|
|
13
|
+
| "needs-review" // could not translate; emitted comment + diagnostic
|
|
14
|
+
| "skipped" // intentionally dropped
|
|
15
|
+
| "action-map"; // a marketplace action mapping fired
|
|
16
|
+
|
|
17
|
+
export interface ProvenanceRecord {
|
|
18
|
+
/** Where this lives in the output (GitLab IR) — e.g. "jobs.test.script[2]" */
|
|
19
|
+
gitlabPath: string;
|
|
20
|
+
/** Resource logicalId in the GitLab IR, if applicable */
|
|
21
|
+
gitlabLogicalId?: string;
|
|
22
|
+
/** Source file name (for SARIF) */
|
|
23
|
+
sourceFile?: string;
|
|
24
|
+
/** 1-based source line (best effort) */
|
|
25
|
+
sourceLine?: number;
|
|
26
|
+
/** 1-based source column (best effort) */
|
|
27
|
+
sourceColumn?: number;
|
|
28
|
+
/** YAML path in the source (e.g. "jobs.test.steps[1].uses") */
|
|
29
|
+
sourceKey?: string;
|
|
30
|
+
/** What category of translation happened */
|
|
31
|
+
category: ProvenanceCategory;
|
|
32
|
+
/** Rule ID (e.g. "MIG-TRIGGER-001", "ACT-actions-checkout") */
|
|
33
|
+
rule: string;
|
|
34
|
+
/** Free-form explanation for human readers */
|
|
35
|
+
note?: string;
|
|
36
|
+
/** Original action reference (e.g. "actions/checkout@v4") for action-map records */
|
|
37
|
+
actionRef?: string;
|
|
38
|
+
/** Tier 1/2/3 for action-map records */
|
|
39
|
+
mappingTier?: 1 | 2 | 3;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Accumulator passed through the transformer pipeline so any step can
|
|
44
|
+
* append to the same provenance list without threading return values.
|
|
45
|
+
*/
|
|
46
|
+
export class ProvenanceAccumulator {
|
|
47
|
+
private records: ProvenanceRecord[] = [];
|
|
48
|
+
|
|
49
|
+
push(record: ProvenanceRecord): void {
|
|
50
|
+
this.records.push(record);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pushAll(records: ProvenanceRecord[]): void {
|
|
54
|
+
this.records.push(...records);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
all(): ProvenanceRecord[] {
|
|
58
|
+
return [...this.records];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
byCategory(category: ProvenanceCategory): ProvenanceRecord[] {
|
|
62
|
+
return this.records.filter((r) => r.category === category);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
needsReviewCount(): number {
|
|
66
|
+
return this.records.filter((r) => r.category === "needs-review").length;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
|
|
11
|
+
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
12
|
+
|
|
13
|
+
const noopCheck = () => [];
|
|
14
|
+
|
|
15
|
+
function rule(id: string, severity: "error" | "warning" | "info", description: string): LintRule {
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
severity,
|
|
19
|
+
category: "correctness",
|
|
20
|
+
description,
|
|
21
|
+
helpUri: `https://intentius.io/chant/lexicons/gitlab/migration#${id.toLowerCase()}`,
|
|
22
|
+
check: noopCheck,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MIGRATION_RULES: LintRule[] = [
|
|
27
|
+
// Trigger translations
|
|
28
|
+
rule("MIG-ON-PUSH", "info", "GitHub on: push → workflow.rules push"),
|
|
29
|
+
rule("MIG-ON-PR", "info", "GitHub on: pull_request → workflow.rules merge_request_event"),
|
|
30
|
+
rule("MIG-ON-SCHEDULE", "warning", "GitHub on: schedule requires GitLab CI/CD > Schedules UI configuration"),
|
|
31
|
+
rule("MIG-ON-DISPATCH", "warning", "GitHub on: workflow_dispatch inputs require spec:inputs (GitLab 17+) with defaults"),
|
|
32
|
+
rule("MIG-ON-NON-GIT", "warning", "GitHub event has no GitLab equivalent — GitLab pipelines run on git events only"),
|
|
33
|
+
rule("MIG-ON-UNKNOWN", "warning", "Unknown GitHub trigger event"),
|
|
34
|
+
|
|
35
|
+
// Job-level translations
|
|
36
|
+
rule("MIG-WORKFLOW-NAME", "info", "name → workflow.name"),
|
|
37
|
+
rule("MIG-WORKFLOW-ENV", "info", "Workflow env → top-level variables"),
|
|
38
|
+
rule("MIG-JOB-ENV", "info", "Job env → variables"),
|
|
39
|
+
rule("MIG-TIMEOUT", "info", "timeout-minutes → timeout"),
|
|
40
|
+
rule("MIG-ALLOW-FAILURE", "info", "continue-on-error → allow_failure"),
|
|
41
|
+
rule("MIG-NEEDS", "info", "needs: passthrough"),
|
|
42
|
+
rule("MIG-CONCURRENCY", "info", "concurrency.group/cancel-in-progress → resource_group/interruptible"),
|
|
43
|
+
rule("MIG-SERVICES", "info", "services: passthrough"),
|
|
44
|
+
rule("MIG-CONTAINER", "info", "container.image → image"),
|
|
45
|
+
rule("MIG-MATRIX", "info", "strategy.matrix → parallel.matrix"),
|
|
46
|
+
|
|
47
|
+
// runs-on
|
|
48
|
+
rule("MIG-RUNS-ON-001", "info", "runs-on: linux runner → Docker image"),
|
|
49
|
+
rule("MIG-RUNS-ON-NON-LINUX", "warning", "Non-Linux runs-on requires self-hosted runner with tag"),
|
|
50
|
+
rule("MIG-RUNS-ON-TAG", "info", "Custom runs-on label → tags:"),
|
|
51
|
+
|
|
52
|
+
// Expressions
|
|
53
|
+
rule("MIG-EXPR-CONTEXT", "info", "github.*/runner.*/job.* → predefined $CI_* variable"),
|
|
54
|
+
rule("MIG-EXPR-USERVAR", "info", "env.*/vars.*/secrets.*/inputs.*/matrix.* → $NAME"),
|
|
55
|
+
rule("MIG-EXPR-FUNCTION", "info", "Boolean function → when: clause"),
|
|
56
|
+
rule("MIG-EXPR-NO-EQUIV", "warning", "GitHub expression has no GitLab equivalent"),
|
|
57
|
+
rule("MIG-EXPR-UNKNOWN", "warning", "Could not translate expression"),
|
|
58
|
+
|
|
59
|
+
// Rule conversions
|
|
60
|
+
rule("MIG-IF-WHEN", "info", "if: boolean_function() → when:"),
|
|
61
|
+
|
|
62
|
+
// Permissions and outputs
|
|
63
|
+
rule("MIG-PERMISSIONS-001", "warning", "GitHub permissions: has no per-job equivalent — configure CI/CD token at project level"),
|
|
64
|
+
rule("MIG-JOB-OUTPUTS", "warning", "GitHub job outputs require artifacts:reports:dotenv pattern in GitLab"),
|
|
65
|
+
rule("MIG-NEEDS-OUTPUTS-001", "warning", "steps/needs outputs require artifacts:reports:dotenv pattern"),
|
|
66
|
+
|
|
67
|
+
// Matrix
|
|
68
|
+
rule("MIG-MATRIX-INCLUDE-001", "warning", "matrix.include/exclude has no direct GitLab equivalent"),
|
|
69
|
+
rule("MIG-FAIL-FAST", "warning", "strategy.fail-fast has no GitLab equivalent (default behaviour is non-fail-fast)"),
|
|
70
|
+
|
|
71
|
+
// Reusable workflow + step env
|
|
72
|
+
rule("MIG-REUSABLE-WORKFLOW", "warning", "GitHub reusable workflow → GitLab include: with variable substitution (no typed inputs)"),
|
|
73
|
+
rule("MIG-STEP-ENV-CONFLICT", "warning", "Step-level env var conflict; using last value"),
|
|
74
|
+
|
|
75
|
+
// Stage inference
|
|
76
|
+
rule("MIG-STAGE-TOPO", "info", "Stage assigned by needs: depth"),
|
|
77
|
+
rule("MIG-STAGE-HEURISTIC", "info", "Stage assigned by job name heuristic"),
|
|
78
|
+
rule("MIG-NEEDS-CYCLE-001", "warning", "needs: cycle detected; each cycle member in its own stage"),
|
|
79
|
+
|
|
80
|
+
// Action mapping fallback
|
|
81
|
+
rule("MIG-ACTION-UNKNOWN", "warning", "Marketplace action has no registered mapping"),
|
|
82
|
+
];
|