@intentius/chant-lexicon-gitlab 0.124.31 → 0.124.33
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.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +294 -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 +376 -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-record.gitlab.golden.yml +46 -0
- 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-op-pipeline.test.ts +10 -0
- package/src/components/generate-op-pipeline.ts +8 -1
- package/src/components/generate-pipeline.ts +71 -30
- package/src/components/generate-pr-pipeline.test.ts +61 -18
- 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,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored snapshot of well-known GitLab CI include/component source projects,
|
|
3
|
+
* used by the look-alike check (WGL032) as the reference set: an `include` or
|
|
4
|
+
* `component` source that is a near-miss (edit distance 1–2) of one of these —
|
|
5
|
+
* but not an exact match — is likely a typo or an impersonation.
|
|
6
|
+
*
|
|
7
|
+
* Advisory only. Refresh source: GitLab-maintained CI templates and the popular
|
|
8
|
+
* CI/CD Catalog components. Accept staleness — a stale entry only weakens
|
|
9
|
+
* look-alike detection, it never blocks a build.
|
|
10
|
+
*/
|
|
11
|
+
export const KNOWN_INCLUDE_SOURCES = [
|
|
12
|
+
"gitlab-org/gitlab",
|
|
13
|
+
"gitlab-org/security-products/ci-templates",
|
|
14
|
+
"components/sast",
|
|
15
|
+
"components/secret-detection",
|
|
16
|
+
"components/dependency-scanning",
|
|
17
|
+
"components/container-scanning",
|
|
18
|
+
"components/code-quality",
|
|
19
|
+
"gitlab-org/components/sast",
|
|
20
|
+
"gitlab-org/components/secret-detection",
|
|
21
|
+
];
|
|
22
|
+
/** Levenshtein edit distance, capped: returns early once it exceeds `max`. */
|
|
23
|
+
export function editDistance(a, b, max) {
|
|
24
|
+
if (a === b)
|
|
25
|
+
return 0;
|
|
26
|
+
if (Math.abs(a.length - b.length) > max)
|
|
27
|
+
return max + 1;
|
|
28
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
29
|
+
for (let i = 1; i <= a.length; i++) {
|
|
30
|
+
const curr = [i];
|
|
31
|
+
let rowMin = i;
|
|
32
|
+
for (let j = 1; j <= b.length; j++) {
|
|
33
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
34
|
+
const val = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
35
|
+
curr.push(val);
|
|
36
|
+
if (val < rowMin)
|
|
37
|
+
rowMin = val;
|
|
38
|
+
}
|
|
39
|
+
if (rowMin > max)
|
|
40
|
+
return max + 1;
|
|
41
|
+
prev = curr;
|
|
42
|
+
}
|
|
43
|
+
return prev[b.length];
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored snapshot of GitLab CI predefined variables whose values are
|
|
3
|
+
* attacker-controllable — branch/tag names, commit and merge-request titles and
|
|
4
|
+
* descriptions, author identity. Interpolating these into a `script:` (or using
|
|
5
|
+
* them as a gate) lets a crafted branch name or commit title influence what runs.
|
|
6
|
+
*
|
|
7
|
+
* Source: GitLab "Predefined CI/CD variables" reference, filtered to the values
|
|
8
|
+
* an outside contributor can set on a fork or merge request. Advisory and
|
|
9
|
+
* necessarily incomplete; editing this list is the refresh mechanism.
|
|
10
|
+
*/
|
|
11
|
+
export const UNTRUSTED_CI_VARIABLES = [
|
|
12
|
+
"CI_COMMIT_TITLE",
|
|
13
|
+
"CI_COMMIT_MESSAGE",
|
|
14
|
+
"CI_COMMIT_DESCRIPTION",
|
|
15
|
+
"CI_COMMIT_REF_NAME",
|
|
16
|
+
"CI_COMMIT_REF_SLUG",
|
|
17
|
+
"CI_COMMIT_BRANCH",
|
|
18
|
+
"CI_COMMIT_TAG",
|
|
19
|
+
"CI_COMMIT_AUTHOR",
|
|
20
|
+
"CI_MERGE_REQUEST_TITLE",
|
|
21
|
+
"CI_MERGE_REQUEST_DESCRIPTION",
|
|
22
|
+
"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME",
|
|
23
|
+
"CI_MERGE_REQUEST_SOURCE_BRANCH_SHA",
|
|
24
|
+
"CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME",
|
|
25
|
+
];
|
|
26
|
+
/** Reference forms a variable can take in a script: `$VAR` or `${VAR}`. */
|
|
27
|
+
export function variableReferenced(text, name) {
|
|
28
|
+
const re = new RegExp(`\\$\\{?${name}\\}?(?![A-Z0-9_])`);
|
|
29
|
+
return re.test(text);
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL001: Deprecated only/except keywords
|
|
3
|
+
*
|
|
4
|
+
* Flags usage of `only:` and `except:` in GitLab CI jobs.
|
|
5
|
+
* These keywords are deprecated in favor of `rules:` which provides
|
|
6
|
+
* more flexible conditional execution.
|
|
7
|
+
*/
|
|
8
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
9
|
+
export const deprecatedOnlyExceptRule = {
|
|
10
|
+
id: "WGL001",
|
|
11
|
+
severity: "warning",
|
|
12
|
+
category: "style",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
// Look for property assignments named "only" or "except"
|
|
18
|
+
// inside new Job(...) or similar constructor calls
|
|
19
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
|
|
20
|
+
const propName = node.name.text;
|
|
21
|
+
if (propName === "only" || propName === "except") {
|
|
22
|
+
// Check if this is inside a new expression (Job constructor)
|
|
23
|
+
let parent = node.parent;
|
|
24
|
+
while (parent) {
|
|
25
|
+
if (ts.isNewExpression(parent)) {
|
|
26
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
file: sourceFile.fileName,
|
|
29
|
+
line: line + 1,
|
|
30
|
+
column: character + 1,
|
|
31
|
+
ruleId: "WGL001",
|
|
32
|
+
severity: "warning",
|
|
33
|
+
message: `"${propName}" is deprecated. Use "rules" for conditional job execution instead.`,
|
|
34
|
+
});
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
parent = parent.parent;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
ts.forEachChild(node, visit);
|
|
42
|
+
}
|
|
43
|
+
visit(sourceFile);
|
|
44
|
+
return diagnostics;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for gitlab lint rules that match a bare TypeScript
|
|
3
|
+
* identifier (`Job`, ...) and need to know whether it actually came from
|
|
4
|
+
* this lexicon before flagging it (chant #1544 — cross-lexicon rule bleed:
|
|
5
|
+
* `new Job(...)` matches ANY lexicon's `Job` class by name alone, so a
|
|
6
|
+
* multi-lexicon project got gitlab-only rules applied to github/forgejo
|
|
7
|
+
* jobs that were never missing anything).
|
|
8
|
+
*/
|
|
9
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
10
|
+
/**
|
|
11
|
+
* The module specifier a top-level import bound `name` to, whether via a
|
|
12
|
+
* named import (`import { Job } from "..."`, matched on its local —
|
|
13
|
+
* possibly aliased — binding) or a namespace import (`import * as gl from
|
|
14
|
+
* "..."`, matched on the namespace's own name). Undefined when `name` isn't
|
|
15
|
+
* bound by any top-level import in this file (no import at all, a
|
|
16
|
+
* re-export chain, a dynamic `require`, …) — the caller should treat that
|
|
17
|
+
* as "can't tell", not "not gitlab", to stay conservative for the common
|
|
18
|
+
* single-lexicon-project case (and every pre-#1544 unit test fixture, which
|
|
19
|
+
* has no import statements at all).
|
|
20
|
+
*/
|
|
21
|
+
export function importSourceFor(sourceFile, name) {
|
|
22
|
+
for (const stmt of sourceFile.statements) {
|
|
23
|
+
if (!ts.isImportDeclaration(stmt))
|
|
24
|
+
continue;
|
|
25
|
+
const moduleSpecifier = stmt.moduleSpecifier;
|
|
26
|
+
if (!ts.isStringLiteral(moduleSpecifier))
|
|
27
|
+
continue;
|
|
28
|
+
const namedBindings = stmt.importClause?.namedBindings;
|
|
29
|
+
if (!namedBindings)
|
|
30
|
+
continue;
|
|
31
|
+
if (ts.isNamespaceImport(namedBindings)) {
|
|
32
|
+
if (namedBindings.name.text === name)
|
|
33
|
+
return moduleSpecifier.text;
|
|
34
|
+
}
|
|
35
|
+
else if (ts.isNamedImports(namedBindings)) {
|
|
36
|
+
for (const element of namedBindings.elements) {
|
|
37
|
+
if (element.name.text === name)
|
|
38
|
+
return moduleSpecifier.text;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* True when `expression` (a `new`-expression callee) resolves — via a
|
|
46
|
+
* traced import — to a lexicon OTHER than gitlab. False for an unresolved
|
|
47
|
+
* import (conservative: still checked) or one that does resolve to gitlab.
|
|
48
|
+
*/
|
|
49
|
+
export function isJobFromAnotherLexicon(sourceFile, expression) {
|
|
50
|
+
let bindingName;
|
|
51
|
+
if (ts.isIdentifier(expression)) {
|
|
52
|
+
bindingName = expression.text;
|
|
53
|
+
}
|
|
54
|
+
else if (ts.isPropertyAccessExpression(expression)) {
|
|
55
|
+
bindingName = ts.isIdentifier(expression.expression) ? expression.expression.text : undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!bindingName)
|
|
58
|
+
return false;
|
|
59
|
+
const source = importSourceFor(sourceFile, bindingName);
|
|
60
|
+
return source !== undefined && !source.includes("chant-lexicon-gitlab");
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab CI/CD lint rules
|
|
3
|
+
*/
|
|
4
|
+
export { deprecatedOnlyExceptRule } from "./deprecated-only-except.js";
|
|
5
|
+
export { missingScriptRule } from "./missing-script.js";
|
|
6
|
+
export { missingStageRule } from "./missing-stage.js";
|
|
7
|
+
export { artifactNoExpiryRule } from "./artifact-no-expiry.js";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL002: Missing script
|
|
3
|
+
*
|
|
4
|
+
* A GitLab CI job must have `script`, `trigger`, or `run` defined.
|
|
5
|
+
* Jobs without any of these will fail pipeline validation.
|
|
6
|
+
*/
|
|
7
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
8
|
+
import { isJobFromAnotherLexicon } from "./import-source.js";
|
|
9
|
+
const VALID_EXECUTION_PROPS = new Set(["script", "trigger", "run"]);
|
|
10
|
+
export const missingScriptRule = {
|
|
11
|
+
id: "WGL002",
|
|
12
|
+
severity: "error",
|
|
13
|
+
category: "correctness",
|
|
14
|
+
check(context) {
|
|
15
|
+
const { sourceFile } = context;
|
|
16
|
+
const diagnostics = [];
|
|
17
|
+
function visit(node) {
|
|
18
|
+
if (ts.isNewExpression(node)) {
|
|
19
|
+
// Check for `new Job(...)` or `new gl.Job(...)`
|
|
20
|
+
let isJob = false;
|
|
21
|
+
const expression = node.expression;
|
|
22
|
+
if (ts.isIdentifier(expression) && expression.text === "Job") {
|
|
23
|
+
isJob = true;
|
|
24
|
+
}
|
|
25
|
+
else if (ts.isPropertyAccessExpression(expression) && expression.name.text === "Job") {
|
|
26
|
+
isJob = true;
|
|
27
|
+
}
|
|
28
|
+
// chant #1544 — cross-lexicon rule bleed: `new Job(...)` matches
|
|
29
|
+
// ANY lexicon's `Job` class by bare name alone (github's, forgejo's,
|
|
30
|
+
// ...), so a multi-lexicon project got WGL002 (gitlab-only) applied
|
|
31
|
+
// to github/forgejo jobs that were never missing anything — they
|
|
32
|
+
// just don't use `script`/`trigger`/`run`. Skip when the import
|
|
33
|
+
// resolves to a lexicon other than gitlab; an unresolved import
|
|
34
|
+
// (no `import` statement — most unit-test fixtures, a re-export) is
|
|
35
|
+
// "can't tell", so it keeps the previous, conservative behavior.
|
|
36
|
+
if (isJob && isJobFromAnotherLexicon(sourceFile, expression)) {
|
|
37
|
+
isJob = false;
|
|
38
|
+
}
|
|
39
|
+
if (isJob && node.arguments && node.arguments.length > 0) {
|
|
40
|
+
const props = node.arguments[0];
|
|
41
|
+
if (ts.isObjectLiteralExpression(props)) {
|
|
42
|
+
const hasExecution = props.properties.some((prop) => {
|
|
43
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
44
|
+
return VALID_EXECUTION_PROPS.has(prop.name.text);
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
});
|
|
48
|
+
if (!hasExecution) {
|
|
49
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
file: sourceFile.fileName,
|
|
52
|
+
line: line + 1,
|
|
53
|
+
column: character + 1,
|
|
54
|
+
ruleId: "WGL002",
|
|
55
|
+
severity: "error",
|
|
56
|
+
message: 'Job must have "script", "trigger", or "run" defined.',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
ts.forEachChild(node, visit);
|
|
63
|
+
}
|
|
64
|
+
visit(sourceFile);
|
|
65
|
+
return diagnostics;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL003: Missing stage
|
|
3
|
+
*
|
|
4
|
+
* Jobs should declare a `stage` property. Without it, the job defaults
|
|
5
|
+
* to the "test" stage which may not be the intended behavior.
|
|
6
|
+
*/
|
|
7
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
8
|
+
import { isJobFromAnotherLexicon } from "./import-source.js";
|
|
9
|
+
export const missingStageRule = {
|
|
10
|
+
id: "WGL003",
|
|
11
|
+
severity: "info",
|
|
12
|
+
category: "style",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isNewExpression(node)) {
|
|
18
|
+
let isJob = false;
|
|
19
|
+
const expression = node.expression;
|
|
20
|
+
if (ts.isIdentifier(expression) && expression.text === "Job") {
|
|
21
|
+
isJob = true;
|
|
22
|
+
}
|
|
23
|
+
else if (ts.isPropertyAccessExpression(expression) && expression.name.text === "Job") {
|
|
24
|
+
isJob = true;
|
|
25
|
+
}
|
|
26
|
+
// chant #1544 — same cross-lexicon rule bleed as WGL002 (see
|
|
27
|
+
// missing-script.ts): a github/forgejo `Job` must not be judged
|
|
28
|
+
// against gitlab's `stage` convention.
|
|
29
|
+
if (isJob && isJobFromAnotherLexicon(sourceFile, expression)) {
|
|
30
|
+
isJob = false;
|
|
31
|
+
}
|
|
32
|
+
if (isJob && node.arguments && node.arguments.length > 0) {
|
|
33
|
+
const props = node.arguments[0];
|
|
34
|
+
if (ts.isObjectLiteralExpression(props)) {
|
|
35
|
+
const hasStage = props.properties.some((prop) => {
|
|
36
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
37
|
+
return prop.name.text === "stage";
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
});
|
|
41
|
+
if (!hasStage) {
|
|
42
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
file: sourceFile.fileName,
|
|
45
|
+
line: line + 1,
|
|
46
|
+
column: character + 1,
|
|
47
|
+
ruleId: "WGL003",
|
|
48
|
+
severity: "info",
|
|
49
|
+
message: 'Job does not declare a "stage". It will default to "test".',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
ts.forEachChild(node, visit);
|
|
56
|
+
}
|
|
57
|
+
visit(sourceFile);
|
|
58
|
+
return diagnostics;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAapF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAE1E"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-gitlab.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provide GitLab CI completions based on context.
|
|
15
|
+
*/
|
|
16
|
+
export function gitlabCompletions(ctx) {
|
|
17
|
+
return lexiconCompletions(ctx, getIndex(), "GitLab CI entity");
|
|
18
|
+
}
|
package/dist/lsp/hover.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAa1E;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAEpE"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-gitlab.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provide hover information for GitLab CI entity types.
|
|
15
|
+
*/
|
|
16
|
+
export function gitlabHover(ctx) {
|
|
17
|
+
return lexiconHover(ctx, getIndex(), resourceHover);
|
|
18
|
+
}
|
|
19
|
+
function resourceHover(className, entry) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
lines.push(`**${className}**`);
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push(`GitLab CI type: \`${entry.resourceType}\``);
|
|
24
|
+
if (entry.kind === "resource") {
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push("*Resource entity — serialized as a top-level CI key*");
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push("*Property entity — used as a nested value in resources*");
|
|
31
|
+
}
|
|
32
|
+
return { contents: lines.join("\n") };
|
|
33
|
+
}
|
package/dist/manifest.json
CHANGED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only MCP tools that expose what `chant build` already computes about a
|
|
3
|
+
* GitLab pipeline — its stages and jobs, what it pulls in from outside, its
|
|
4
|
+
* security findings — so an agent can ask about a change *before it runs or
|
|
5
|
+
* merges* (#327, #328).
|
|
6
|
+
*
|
|
7
|
+
* Every tool here builds from source and returns data. None of them touch the
|
|
8
|
+
* live GitLab instance, read run history, or write anything — that boundary is
|
|
9
|
+
* what keeps chant a context producer and not a copy of the live tooling.
|
|
10
|
+
*/
|
|
11
|
+
import { build } from "@intentius/chant/build";
|
|
12
|
+
import { runPostSynthChecks, getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
13
|
+
import { postSynthChecks } from "../lint/post-synth/index.js";
|
|
14
|
+
import { getProvenance } from "@intentius/chant/provenance";
|
|
15
|
+
import { relative, isAbsolute, resolve } from "path";
|
|
16
|
+
import { readFile } from "fs/promises";
|
|
17
|
+
import { gitlabSerializer } from "../serializer.js";
|
|
18
|
+
import { transform, detectGitHubWorkflow } from "../migrate/from-github/index.js";
|
|
19
|
+
import { extractStages, extractJobs, extractIncludes, extractImageRefs, isPinnedRef, } from "../lint/post-synth/yaml-helpers.js";
|
|
20
|
+
/** Build the project and return the emitted GitLab YAML plus the raw result. */
|
|
21
|
+
async function buildGitlab(path) {
|
|
22
|
+
const result = await build(path, [gitlabSerializer]);
|
|
23
|
+
const out = result.outputs.get("gitlab");
|
|
24
|
+
return { yaml: out ? getPrimaryOutput(out) : "", result };
|
|
25
|
+
}
|
|
26
|
+
/** Discover the lexicon's post-synth checks without depending on the plugin. */
|
|
27
|
+
function gitlabPostSynthChecks() {
|
|
28
|
+
return postSynthChecks;
|
|
29
|
+
}
|
|
30
|
+
/** Is a component address (`host/group/comp@version`) pinned to a fixed version? */
|
|
31
|
+
export function componentPinned(value) {
|
|
32
|
+
const at = value.lastIndexOf("@");
|
|
33
|
+
return at !== -1 && isPinnedRef(value.slice(at + 1));
|
|
34
|
+
}
|
|
35
|
+
/** The YAML job name a build entity serializes to (camelCase → kebab-case). */
|
|
36
|
+
export function jobNameOf(entityName) {
|
|
37
|
+
return entityName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
/** Find the build entity whose serialized job name matches `job`. */
|
|
40
|
+
function entityForJob(result, job) {
|
|
41
|
+
for (const [name, entity] of result.entities) {
|
|
42
|
+
if (name === job || jobNameOf(name) === job)
|
|
43
|
+
return { name, entity };
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Render a source-file path relative to the project root, when possible. */
|
|
48
|
+
function relSource(root, file) {
|
|
49
|
+
if (!file)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!isAbsolute(file))
|
|
52
|
+
return file;
|
|
53
|
+
const rel = relative(root, file);
|
|
54
|
+
return rel.startsWith("..") ? file : rel;
|
|
55
|
+
}
|
|
56
|
+
/** Jobs that would re-run because they depend (transitively) on `job`. */
|
|
57
|
+
export function downstreamJobs(yaml, job) {
|
|
58
|
+
const jobs = extractJobs(yaml);
|
|
59
|
+
// edge: upstream -> downstream (J depends on each of its needs)
|
|
60
|
+
const downstreamOf = new Map();
|
|
61
|
+
for (const [name, j] of jobs) {
|
|
62
|
+
for (const dep of j.needs ?? []) {
|
|
63
|
+
const arr = downstreamOf.get(dep) ?? [];
|
|
64
|
+
arr.push(name);
|
|
65
|
+
downstreamOf.set(dep, arr);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const out = new Set();
|
|
69
|
+
const queue = [job];
|
|
70
|
+
while (queue.length) {
|
|
71
|
+
const cur = queue.shift();
|
|
72
|
+
for (const next of downstreamOf.get(cur) ?? []) {
|
|
73
|
+
if (!out.has(next)) {
|
|
74
|
+
out.add(next);
|
|
75
|
+
queue.push(next);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [...out];
|
|
80
|
+
}
|
|
81
|
+
const PATH_INPUT = {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
/** The read-only context tools for the GitLab lexicon. */
|
|
88
|
+
export function gitlabContextTools() {
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
name: "gitlab:checks",
|
|
92
|
+
description: "Build the pipeline and return its security and correctness findings (the WGL checks) as JSON. Read-only — does not touch the live GitLab instance.",
|
|
93
|
+
inputSchema: PATH_INPUT,
|
|
94
|
+
async handler(params) {
|
|
95
|
+
const { yaml, result } = await buildGitlab(params.path ?? ".");
|
|
96
|
+
if (!yaml)
|
|
97
|
+
return { findings: [], note: "no GitLab pipeline produced from this project" };
|
|
98
|
+
const scoped = { ...result, outputs: new Map([["gitlab", result.outputs.get("gitlab")]]) };
|
|
99
|
+
const diags = runPostSynthChecks(gitlabPostSynthChecks(), scoped);
|
|
100
|
+
return {
|
|
101
|
+
findings: diags.map((d) => ({
|
|
102
|
+
id: d.checkId,
|
|
103
|
+
severity: d.severity,
|
|
104
|
+
job: d.entity ?? null,
|
|
105
|
+
message: d.message,
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "gitlab:pipeline",
|
|
112
|
+
description: "Build the pipeline and return its stages and jobs as written (name, stage, and what each job runs after) — before anything runs. Read-only.",
|
|
113
|
+
inputSchema: PATH_INPUT,
|
|
114
|
+
async handler(params) {
|
|
115
|
+
const { yaml } = await buildGitlab(params.path ?? ".");
|
|
116
|
+
const jobs = [...extractJobs(yaml).values()].map((j) => ({
|
|
117
|
+
name: j.name,
|
|
118
|
+
stage: j.stage ?? null,
|
|
119
|
+
runsAfter: j.needs ?? [],
|
|
120
|
+
}));
|
|
121
|
+
return { stages: extractStages(yaml), jobs };
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "gitlab:references",
|
|
126
|
+
description: "Build the pipeline and list everything it pulls in from outside (includes, components, images) and whether each is pinned to a fixed version. Read-only.",
|
|
127
|
+
inputSchema: PATH_INPUT,
|
|
128
|
+
async handler(params) {
|
|
129
|
+
const { yaml } = await buildGitlab(params.path ?? ".");
|
|
130
|
+
const includes = extractIncludes(yaml).map((e) => ({
|
|
131
|
+
kind: e.kind,
|
|
132
|
+
source: e.value,
|
|
133
|
+
ref: e.ref ?? null,
|
|
134
|
+
pinned: e.kind === "project" ? (e.ref ? isPinnedRef(e.ref) : false) : e.kind === "component" ? componentPinned(e.value) : null,
|
|
135
|
+
}));
|
|
136
|
+
const images = extractImageRefs(yaml).map((i) => ({
|
|
137
|
+
kind: "image",
|
|
138
|
+
source: i.image,
|
|
139
|
+
ref: null,
|
|
140
|
+
pinned: i.image.includes("@sha256:"),
|
|
141
|
+
}));
|
|
142
|
+
return [...includes, ...images];
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "gitlab:affected",
|
|
147
|
+
description: "Build the pipeline and, given a job name, list the jobs that would re-run because they depend on it (the needs chain). Read-only.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
152
|
+
job: { type: "string", description: "Job name to trace downstream from" },
|
|
153
|
+
},
|
|
154
|
+
required: ["job"],
|
|
155
|
+
},
|
|
156
|
+
async handler(params) {
|
|
157
|
+
const { yaml } = await buildGitlab(params.path ?? ".");
|
|
158
|
+
const job = params.job;
|
|
159
|
+
return { job, wouldRerun: downstreamJobs(yaml, job) };
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "gitlab:pipeline-yaml",
|
|
164
|
+
description: "Build the project and return the generated .gitlab-ci.yml as a string. Read-only.",
|
|
165
|
+
inputSchema: PATH_INPUT,
|
|
166
|
+
async handler(params) {
|
|
167
|
+
const { yaml } = await buildGitlab(params.path ?? ".");
|
|
168
|
+
return { yaml };
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "gitlab:source",
|
|
173
|
+
description: "Build the project and, given a job name, say where it came from in the TypeScript source — the file that declared it and the composite that expanded it, if any. Entity-level provenance (file + composite), not a YAML-line source map. Read-only.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
178
|
+
job: { type: "string", description: "Job name (as it appears in the generated YAML) to trace back to source" },
|
|
179
|
+
},
|
|
180
|
+
required: ["job"],
|
|
181
|
+
},
|
|
182
|
+
async handler(params) {
|
|
183
|
+
const root = params.path ?? ".";
|
|
184
|
+
const { result } = await buildGitlab(root);
|
|
185
|
+
const job = params.job;
|
|
186
|
+
const found = entityForJob(result, job);
|
|
187
|
+
if (!found)
|
|
188
|
+
return { job, found: false, note: "no build entity serializes to that job name" };
|
|
189
|
+
const prov = getProvenance(found.entity);
|
|
190
|
+
return {
|
|
191
|
+
job,
|
|
192
|
+
found: true,
|
|
193
|
+
entity: found.name,
|
|
194
|
+
from: relSource(root, prov?.sourceFile) ?? null,
|
|
195
|
+
via: prov?.composite ?? null,
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "gitlab:owns",
|
|
201
|
+
description: "Build the project and report whether a job is declared (owned) by chant in this project's source. For pipeline config, ownership means \"declared here\" — GitLab CI jobs are not taggable cloud resources, so there is no live ownership marker as there is for cloud lexicons. Read-only.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
206
|
+
job: { type: "string", description: "Job name (as it appears in the generated YAML) to check" },
|
|
207
|
+
},
|
|
208
|
+
required: ["job"],
|
|
209
|
+
},
|
|
210
|
+
async handler(params) {
|
|
211
|
+
const root = params.path ?? ".";
|
|
212
|
+
const { result } = await buildGitlab(root);
|
|
213
|
+
const job = params.job;
|
|
214
|
+
const found = entityForJob(result, job);
|
|
215
|
+
return {
|
|
216
|
+
job,
|
|
217
|
+
owned: Boolean(found),
|
|
218
|
+
basis: "declared-in-source",
|
|
219
|
+
note: "GitLab CI jobs are not taggable cloud resources; ownership here means the job is declared by chant in this project. Live ownership markers apply to cloud lexicons (aws/azure/k8s).",
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "gitlab:compare",
|
|
225
|
+
description: "Given a GitHub Actions workflow file, migrate it to GitLab CI and report which security properties survive the move and which weaken or are lost (the migration safety view). Returns a per-property fate (translated/approximated/needs-review/lost). Read-only — builds and analyzes, never writes.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
file: { type: "string", description: "Path to a .github/workflows/*.yml workflow file to migrate and compare" },
|
|
230
|
+
},
|
|
231
|
+
required: ["file"],
|
|
232
|
+
},
|
|
233
|
+
async handler(params) {
|
|
234
|
+
const file = resolve(params.file ?? "");
|
|
235
|
+
let content;
|
|
236
|
+
try {
|
|
237
|
+
content = await readFile(file, "utf8");
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return { file: params.file ?? null, found: false, note: "could not read the workflow file" };
|
|
241
|
+
}
|
|
242
|
+
if (!detectGitHubWorkflow(content)) {
|
|
243
|
+
return { file: params.file ?? null, found: false, note: "file does not look like a GitHub Actions workflow" };
|
|
244
|
+
}
|
|
245
|
+
const migration = await transform(content, { security: true, sourceFile: file });
|
|
246
|
+
const properties = migration.provenance
|
|
247
|
+
.filter((r) => r.security)
|
|
248
|
+
.map((r) => ({
|
|
249
|
+
property: r.security.property,
|
|
250
|
+
fate: r.security.fate,
|
|
251
|
+
severity: r.security.severity,
|
|
252
|
+
reestablish: r.security.reestablish ?? null,
|
|
253
|
+
note: r.note ?? null,
|
|
254
|
+
}));
|
|
255
|
+
const summary = { translated: 0, approximated: 0, "needs-review": 0, lost: 0 };
|
|
256
|
+
for (const p of properties)
|
|
257
|
+
summary[p.fate] = (summary[p.fate] ?? 0) + 1;
|
|
258
|
+
return { found: true, properties, summary };
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
}
|