@intentius/chant-lexicon-gitlab 0.0.1
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/package.json +27 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +33 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +962 -0
- package/src/codegen/fetch.ts +73 -0
- package/src/codegen/generate-cli.ts +41 -0
- package/src/codegen/generate-lexicon.ts +53 -0
- package/src/codegen/generate-typescript.ts +144 -0
- package/src/codegen/generate.ts +166 -0
- package/src/codegen/naming.ts +52 -0
- package/src/codegen/package.ts +64 -0
- package/src/codegen/parse.test.ts +195 -0
- package/src/codegen/parse.ts +531 -0
- package/src/codegen/patches.test.ts +99 -0
- package/src/codegen/patches.ts +100 -0
- package/src/codegen/rollback.ts +26 -0
- package/src/codegen/snapshot.test.ts +109 -0
- package/src/coverage.test.ts +39 -0
- package/src/coverage.ts +52 -0
- package/src/generated/index.d.ts +248 -0
- package/src/generated/index.ts +23 -0
- package/src/generated/lexicon-gitlab.json +77 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +151 -0
- package/src/import/generator.ts +173 -0
- package/src/import/parser.test.ts +160 -0
- package/src/import/parser.ts +282 -0
- package/src/import/roundtrip.test.ts +89 -0
- package/src/index.ts +25 -0
- package/src/intrinsics.test.ts +42 -0
- package/src/intrinsics.ts +40 -0
- package/src/lint/post-synth/post-synth.test.ts +155 -0
- package/src/lint/post-synth/wgl010.ts +41 -0
- package/src/lint/post-synth/wgl011.ts +54 -0
- package/src/lint/post-synth/yaml-helpers.ts +88 -0
- package/src/lint/rules/artifact-no-expiry.ts +62 -0
- package/src/lint/rules/deprecated-only-except.ts +53 -0
- package/src/lint/rules/index.ts +8 -0
- package/src/lint/rules/missing-script.ts +65 -0
- package/src/lint/rules/missing-stage.ts +62 -0
- package/src/lint/rules/rules.test.ts +146 -0
- package/src/lsp/completions.test.ts +85 -0
- package/src/lsp/completions.ts +18 -0
- package/src/lsp/hover.test.ts +60 -0
- package/src/lsp/hover.ts +36 -0
- package/src/plugin.test.ts +228 -0
- package/src/plugin.ts +380 -0
- package/src/serializer.test.ts +309 -0
- package/src/serializer.ts +226 -0
- package/src/testdata/ci-schema-fixture.json +2184 -0
- package/src/testdata/create-fixture.ts +46 -0
- package/src/testdata/load-fixtures.ts +23 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.test.ts +43 -0
- package/src/validate.ts +125 -0
- package/src/variables.ts +27 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Script to create a minimal CI schema fixture for tests.
|
|
4
|
+
* Run once: bun run src/testdata/create-fixture.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
|
|
11
|
+
const schema = JSON.parse(readFileSync(join(homedir(), ".chant", "gitlab-ci-schema.json"), "utf-8"));
|
|
12
|
+
|
|
13
|
+
const neededDefs = [
|
|
14
|
+
"job_template", "image", "services", "artifacts", "cache_item",
|
|
15
|
+
"rules", "retry", "allow_failure", "parallel", "include_item",
|
|
16
|
+
"when", "retry_errors", "workflowAutoCancel",
|
|
17
|
+
"before_script", "after_script", "script", "optional_script",
|
|
18
|
+
"globalVariables", "jobVariables", "rulesVariables",
|
|
19
|
+
"id_tokens", "secrets", "timeout", "start_in",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const defs: Record<string, unknown> = {};
|
|
23
|
+
for (const name of neededDefs) {
|
|
24
|
+
if (schema.definitions?.[name]) {
|
|
25
|
+
defs[name] = schema.definitions[name];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const keepProps = ["default", "workflow", "stages", "variables", "include"];
|
|
30
|
+
const filteredProps: Record<string, unknown> = {};
|
|
31
|
+
for (const key of keepProps) {
|
|
32
|
+
if (schema.properties?.[key]) filteredProps[key] = schema.properties[key];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const minimal = {
|
|
36
|
+
definitions: defs,
|
|
37
|
+
properties: filteredProps,
|
|
38
|
+
patternProperties: schema.patternProperties || {},
|
|
39
|
+
required: schema.required || [],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const outPath = join(import.meta.dir, "ci-schema-fixture.json");
|
|
43
|
+
writeFileSync(outPath, JSON.stringify(minimal, null, 2));
|
|
44
|
+
console.log(`Fixture written to ${outPath}`);
|
|
45
|
+
console.log(`Definitions: ${Object.keys(defs).length}`);
|
|
46
|
+
console.log(`Missing: ${neededDefs.filter(n => !defs[n]).join(", ") || "none"}`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test fixture loader for the GitLab CI lexicon.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load the minimal CI schema fixture for offline testing.
|
|
10
|
+
*/
|
|
11
|
+
export function loadSchemaFixture(): Buffer {
|
|
12
|
+
const fixturePath = join(import.meta.dir, "ci-schema-fixture.json");
|
|
13
|
+
return Buffer.from(readFileSync(fixturePath));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load the schema fixture as a Map compatible with generatePipeline.
|
|
18
|
+
*/
|
|
19
|
+
export function loadSchemaFixtureMap(): Map<string, Buffer> {
|
|
20
|
+
const schemas = new Map<string, Buffer>();
|
|
21
|
+
schemas.set("GitLab::CI::Pipeline", loadSchemaFixture());
|
|
22
|
+
return schemas;
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Thin entry point for `bun run validate` in lexicon-gitlab.
|
|
4
|
+
*/
|
|
5
|
+
import { validate } from "./validate";
|
|
6
|
+
|
|
7
|
+
const result = await validate();
|
|
8
|
+
|
|
9
|
+
for (const check of result.checks) {
|
|
10
|
+
const status = check.ok ? "OK" : "FAIL";
|
|
11
|
+
const msg = check.error ? ` — ${check.error}` : "";
|
|
12
|
+
console.error(` [${status}] ${check.name}${msg}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!result.success) {
|
|
16
|
+
console.error("Validation failed");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
console.error("All validation checks passed.");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { validate } from "./validate";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const basePath = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
+
|
|
8
|
+
describe("validate", () => {
|
|
9
|
+
test("passes validation for current generated artifacts", async () => {
|
|
10
|
+
const result = await validate({ basePath });
|
|
11
|
+
expect(result.success).toBe(true);
|
|
12
|
+
expect(result.checks.length).toBeGreaterThan(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("checks all expected entities are present", async () => {
|
|
16
|
+
const result = await validate({ basePath });
|
|
17
|
+
const checkNames = result.checks.map((c) => c.name);
|
|
18
|
+
expect(checkNames).toContain("resource Job present");
|
|
19
|
+
expect(checkNames).toContain("resource Default present");
|
|
20
|
+
expect(checkNames).toContain("resource Workflow present");
|
|
21
|
+
expect(checkNames).toContain("property Artifacts present");
|
|
22
|
+
expect(checkNames).toContain("property Cache present");
|
|
23
|
+
expect(checkNames).toContain("property Image present");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("checks file existence", async () => {
|
|
27
|
+
const result = await validate({ basePath });
|
|
28
|
+
const fileChecks = result.checks.filter((c) => c.name.endsWith("exists"));
|
|
29
|
+
expect(fileChecks.length).toBeGreaterThan(0);
|
|
30
|
+
for (const check of fileChecks) {
|
|
31
|
+
expect(check.ok).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("checks index.d.ts class declarations", async () => {
|
|
36
|
+
const result = await validate({ basePath });
|
|
37
|
+
const dtsChecks = result.checks.filter((c) => c.name.startsWith("index.d.ts declares"));
|
|
38
|
+
expect(dtsChecks.length).toBeGreaterThan(0);
|
|
39
|
+
for (const check of dtsChecks) {
|
|
40
|
+
expect(check.ok).toBe(true);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic validation for GitLab CI lexicon artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Checks that generated files exist, contain expected entities,
|
|
5
|
+
* and pass basic structural validation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { join, dirname } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
export interface ValidateCheck {
|
|
13
|
+
name: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ValidateResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
checks: ValidateCheck[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const EXPECTED_RESOURCES = ["Job", "Default", "Workflow"];
|
|
24
|
+
const EXPECTED_PROPERTIES = [
|
|
25
|
+
"Artifacts", "Cache", "Image", "Rule", "Retry",
|
|
26
|
+
"AllowFailure", "Parallel", "Include", "Release",
|
|
27
|
+
"Environment", "Trigger", "AutoCancel",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate lexicon artifacts.
|
|
32
|
+
*/
|
|
33
|
+
export async function validate(opts?: { basePath?: string }): Promise<ValidateResult> {
|
|
34
|
+
const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
35
|
+
const generatedDir = join(basePath, "src", "generated");
|
|
36
|
+
const checks: ValidateCheck[] = [];
|
|
37
|
+
|
|
38
|
+
// Check files exist
|
|
39
|
+
for (const file of ["lexicon-gitlab.json", "index.d.ts", "index.ts", "runtime.ts"]) {
|
|
40
|
+
const path = join(generatedDir, file);
|
|
41
|
+
checks.push({
|
|
42
|
+
name: `${file} exists`,
|
|
43
|
+
ok: existsSync(path),
|
|
44
|
+
error: existsSync(path) ? undefined : `File not found: ${path}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate lexicon JSON structure
|
|
49
|
+
const lexiconPath = join(generatedDir, "lexicon-gitlab.json");
|
|
50
|
+
if (existsSync(lexiconPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const content = readFileSync(lexiconPath, "utf-8");
|
|
53
|
+
const registry = JSON.parse(content);
|
|
54
|
+
const entries = Object.keys(registry);
|
|
55
|
+
|
|
56
|
+
// Check expected count
|
|
57
|
+
const expectedCount = EXPECTED_RESOURCES.length + EXPECTED_PROPERTIES.length;
|
|
58
|
+
checks.push({
|
|
59
|
+
name: `lexicon-gitlab.json has ${expectedCount} entries`,
|
|
60
|
+
ok: entries.length === expectedCount,
|
|
61
|
+
error: entries.length !== expectedCount
|
|
62
|
+
? `Expected ${expectedCount} entries, found ${entries.length}`
|
|
63
|
+
: undefined,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Check resource entities present
|
|
67
|
+
for (const name of EXPECTED_RESOURCES) {
|
|
68
|
+
const entry = registry[name];
|
|
69
|
+
const ok = entry !== undefined && entry.kind === "resource";
|
|
70
|
+
checks.push({
|
|
71
|
+
name: `resource ${name} present`,
|
|
72
|
+
ok,
|
|
73
|
+
error: ok ? undefined : `Missing or invalid resource entry: ${name}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check property entities present
|
|
78
|
+
for (const name of EXPECTED_PROPERTIES) {
|
|
79
|
+
const entry = registry[name];
|
|
80
|
+
const ok = entry !== undefined && entry.kind === "property";
|
|
81
|
+
checks.push({
|
|
82
|
+
name: `property ${name} present`,
|
|
83
|
+
ok,
|
|
84
|
+
error: ok ? undefined : `Missing or invalid property entry: ${name}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check all entries have required fields
|
|
89
|
+
for (const [name, entry] of Object.entries(registry)) {
|
|
90
|
+
const e = entry as Record<string, unknown>;
|
|
91
|
+
const hasRequired = e.resourceType && e.kind && e.lexicon === "gitlab";
|
|
92
|
+
checks.push({
|
|
93
|
+
name: `${name} has required fields`,
|
|
94
|
+
ok: !!hasRequired,
|
|
95
|
+
error: hasRequired ? undefined : `Entry ${name} missing required fields`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
checks.push({
|
|
100
|
+
name: "lexicon-gitlab.json is valid JSON",
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `Parse error: ${err}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate index.d.ts has class declarations
|
|
108
|
+
const dtsPath = join(generatedDir, "index.d.ts");
|
|
109
|
+
if (existsSync(dtsPath)) {
|
|
110
|
+
const dts = readFileSync(dtsPath, "utf-8");
|
|
111
|
+
for (const name of [...EXPECTED_RESOURCES, ...EXPECTED_PROPERTIES]) {
|
|
112
|
+
const has = dts.includes(`export declare class ${name}`);
|
|
113
|
+
checks.push({
|
|
114
|
+
name: `index.d.ts declares ${name}`,
|
|
115
|
+
ok: has,
|
|
116
|
+
error: has ? undefined : `Missing class declaration: ${name}`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
success: checks.every((c) => c.ok),
|
|
123
|
+
checks,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/variables.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab CI/CD predefined variable references.
|
|
3
|
+
*
|
|
4
|
+
* These expand to $CI_* variable strings in the serialized YAML.
|
|
5
|
+
* Provided for type-safe access and autocompletion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CI = {
|
|
9
|
+
CommitBranch: "$CI_COMMIT_BRANCH",
|
|
10
|
+
CommitRef: "$CI_COMMIT_REF_NAME",
|
|
11
|
+
CommitSha: "$CI_COMMIT_SHA",
|
|
12
|
+
CommitTag: "$CI_COMMIT_TAG",
|
|
13
|
+
DefaultBranch: "$CI_DEFAULT_BRANCH",
|
|
14
|
+
Environment: "$CI_ENVIRONMENT_NAME",
|
|
15
|
+
JobId: "$CI_JOB_ID",
|
|
16
|
+
JobName: "$CI_JOB_NAME",
|
|
17
|
+
JobStage: "$CI_JOB_STAGE",
|
|
18
|
+
MergeRequestIid: "$CI_MERGE_REQUEST_IID",
|
|
19
|
+
PipelineId: "$CI_PIPELINE_ID",
|
|
20
|
+
PipelineSource: "$CI_PIPELINE_SOURCE",
|
|
21
|
+
ProjectDir: "$CI_PROJECT_DIR",
|
|
22
|
+
ProjectId: "$CI_PROJECT_ID",
|
|
23
|
+
ProjectName: "$CI_PROJECT_NAME",
|
|
24
|
+
ProjectPath: "$CI_PROJECT_PATH",
|
|
25
|
+
Registry: "$CI_REGISTRY",
|
|
26
|
+
RegistryImage: "$CI_REGISTRY_IMAGE",
|
|
27
|
+
} as const;
|