@intentius/chant-lexicon-gitlab 0.0.1 → 0.0.3

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 ADDED
@@ -0,0 +1,59 @@
1
+ # @intentius/chant-lexicon-gitlab
2
+
3
+ > Part of the [chant](../../README.md) monorepo. Published as [`@intentius/chant-lexicon-gitlab`](https://www.npmjs.com/package/@intentius/chant-lexicon-gitlab) on npm.
4
+
5
+ GitLab CI lexicon for chant — declare CI/CD pipelines as flat, typed TypeScript that serializes to `.gitlab-ci.yml`.
6
+
7
+ ## Overview
8
+
9
+ This package provides:
10
+
11
+ - **GitLab CI serializer** — converts chant declarables to GitLab CI YAML
12
+ - **Resource types** — typed constructors for `Job`, `Default`, `Workflow`, and all GitLab CI keywords
13
+ - **Property types** — `Artifacts`, `Cache`, `Image`, `Rule`, `Retry`, `Environment`, `Trigger`, and more
14
+ - **Lint rules** — GitLab-specific validation (e.g. missing script, deprecated only/except)
15
+ - **Code generation** — generates TypeScript types from the GitLab CI JSON schema
16
+ - **LSP/MCP support** — completions and hover for GitLab CI keywords
17
+
18
+ ## Usage
19
+
20
+ ```typescript
21
+ import { Job, Artifacts, Image } from "@intentius/chant-lexicon-gitlab";
22
+
23
+ export const testJob = new Job({
24
+ stage: "test",
25
+ image: new Image({ name: "node:20" }),
26
+ script: ["npm ci", "npm test"],
27
+ artifacts: new Artifacts({
28
+ paths: ["coverage/"],
29
+ expireIn: "1 week",
30
+ }),
31
+ });
32
+ ```
33
+
34
+ ## Lint Rules
35
+
36
+ | Rule | Description |
37
+ |------|-------------|
38
+ | `missing-script` | Job must have a `script` keyword |
39
+ | `missing-stage` | Job should declare a `stage` |
40
+ | `deprecated-only-except` | Flags use of deprecated `only`/`except` keywords |
41
+ | `artifact-no-expiry` | Artifacts should have `expire_in` set |
42
+
43
+ ## Code Generation
44
+
45
+ The GitLab lexicon generates types from the [GitLab CI JSON schema](https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json):
46
+
47
+ - `codegen/generate.ts` — calls core `generatePipeline<GitLabParseResult>` with GitLab callbacks
48
+ - `codegen/naming.ts` — extends core `NamingStrategy` for GitLab CI keywords
49
+ - `codegen/package.ts` — calls core `packagePipeline` with GitLab manifest
50
+ - `codegen/parse.ts` — parses the GitLab CI JSON schema into typed entities
51
+
52
+ ## Related Packages
53
+
54
+ - `@intentius/chant` — core functionality, type system, and CLI
55
+ - `@intentius/chant-lexicon-aws` — AWS CloudFormation lexicon
56
+
57
+ ## License
58
+
59
+ See the main project LICENSE file.
@@ -0,0 +1,17 @@
1
+ {
2
+ "algorithm": "xxhash64",
3
+ "artifacts": {
4
+ "manifest.json": "ebbf7a6ac4cb48ce",
5
+ "meta.json": "3a60a5c15437a93b",
6
+ "types/index.d.ts": "2ab52d25cd1a5a14",
7
+ "rules/missing-stage.ts": "6d5379e74209a735",
8
+ "rules/missing-script.ts": "923dde9acb46cc28",
9
+ "rules/deprecated-only-except.ts": "1f5a8c785777fb03",
10
+ "rules/artifact-no-expiry.ts": "26874cb6adfbca26",
11
+ "rules/wgl011.ts": "b6b97e5104d91267",
12
+ "rules/wgl010.ts": "1548cad287cdf286",
13
+ "rules/yaml-helpers.ts": "a66cc193b4ef4f0a",
14
+ "skills/gitlab-ci.md": "f860e40c2643c327"
15
+ },
16
+ "composite": "93408e2ccfc0086a"
17
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "gitlab",
3
+ "version": "0.0.3",
4
+ "chantVersion": ">=0.1.0",
5
+ "namespace": "GitLab",
6
+ "intrinsics": [
7
+ {
8
+ "name": "reference",
9
+ "description": "!reference tag — reference another job's properties",
10
+ "outputKey": "!reference",
11
+ "isTag": true
12
+ }
13
+ ],
14
+ "pseudoParameters": {}
15
+ }
package/dist/meta.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "AllowFailure": {
3
+ "resourceType": "GitLab::CI::AllowFailure",
4
+ "kind": "property",
5
+ "lexicon": "gitlab"
6
+ },
7
+ "Artifacts": {
8
+ "resourceType": "GitLab::CI::Artifacts",
9
+ "kind": "property",
10
+ "lexicon": "gitlab"
11
+ },
12
+ "AutoCancel": {
13
+ "resourceType": "GitLab::CI::AutoCancel",
14
+ "kind": "property",
15
+ "lexicon": "gitlab"
16
+ },
17
+ "Cache": {
18
+ "resourceType": "GitLab::CI::Cache",
19
+ "kind": "property",
20
+ "lexicon": "gitlab"
21
+ },
22
+ "Default": {
23
+ "resourceType": "GitLab::CI::Default",
24
+ "kind": "resource",
25
+ "lexicon": "gitlab"
26
+ },
27
+ "Environment": {
28
+ "resourceType": "GitLab::CI::Environment",
29
+ "kind": "property",
30
+ "lexicon": "gitlab"
31
+ },
32
+ "Image": {
33
+ "resourceType": "GitLab::CI::Image",
34
+ "kind": "property",
35
+ "lexicon": "gitlab"
36
+ },
37
+ "Include": {
38
+ "resourceType": "GitLab::CI::Include",
39
+ "kind": "property",
40
+ "lexicon": "gitlab"
41
+ },
42
+ "Job": {
43
+ "resourceType": "GitLab::CI::Job",
44
+ "kind": "resource",
45
+ "lexicon": "gitlab"
46
+ },
47
+ "Parallel": {
48
+ "resourceType": "GitLab::CI::Parallel",
49
+ "kind": "property",
50
+ "lexicon": "gitlab"
51
+ },
52
+ "Release": {
53
+ "resourceType": "GitLab::CI::Release",
54
+ "kind": "property",
55
+ "lexicon": "gitlab"
56
+ },
57
+ "Retry": {
58
+ "resourceType": "GitLab::CI::Retry",
59
+ "kind": "property",
60
+ "lexicon": "gitlab"
61
+ },
62
+ "Rule": {
63
+ "resourceType": "GitLab::CI::Rule",
64
+ "kind": "property",
65
+ "lexicon": "gitlab"
66
+ },
67
+ "Trigger": {
68
+ "resourceType": "GitLab::CI::Trigger",
69
+ "kind": "property",
70
+ "lexicon": "gitlab"
71
+ },
72
+ "Workflow": {
73
+ "resourceType": "GitLab::CI::Workflow",
74
+ "kind": "resource",
75
+ "lexicon": "gitlab"
76
+ }
77
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WGL004: Artifacts without expiry
3
+ *
4
+ * Artifacts without `expireIn` (or `expire_in`) will be kept indefinitely,
5
+ * wasting storage. Always set an expiry for job artifacts.
6
+ */
7
+
8
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
+ import * as ts from "typescript";
10
+
11
+ export const artifactNoExpiryRule: LintRule = {
12
+ id: "WGL004",
13
+ severity: "warning",
14
+ category: "performance",
15
+
16
+ check(context: LintContext): LintDiagnostic[] {
17
+ const { sourceFile } = context;
18
+ const diagnostics: LintDiagnostic[] = [];
19
+
20
+ function visit(node: ts.Node): void {
21
+ if (ts.isNewExpression(node)) {
22
+ let isArtifacts = false;
23
+ const expression = node.expression;
24
+
25
+ if (ts.isIdentifier(expression) && expression.text === "Artifacts") {
26
+ isArtifacts = true;
27
+ } else if (ts.isPropertyAccessExpression(expression) && expression.name.text === "Artifacts") {
28
+ isArtifacts = true;
29
+ }
30
+
31
+ if (isArtifacts && node.arguments && node.arguments.length > 0) {
32
+ const props = node.arguments[0];
33
+ if (ts.isObjectLiteralExpression(props)) {
34
+ const hasExpiry = props.properties.some((prop) => {
35
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
36
+ return prop.name.text === "expireIn" || prop.name.text === "expire_in";
37
+ }
38
+ return false;
39
+ });
40
+
41
+ if (!hasExpiry) {
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: "WGL004",
48
+ severity: "warning",
49
+ message: 'Artifacts without "expireIn" will be kept indefinitely. Set an expiry to save storage.',
50
+ });
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ ts.forEachChild(node, visit);
57
+ }
58
+
59
+ visit(sourceFile);
60
+ return diagnostics;
61
+ },
62
+ };
@@ -0,0 +1,53 @@
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
+
9
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
10
+ import * as ts from "typescript";
11
+
12
+ export const deprecatedOnlyExceptRule: LintRule = {
13
+ id: "WGL001",
14
+ severity: "warning",
15
+ category: "style",
16
+
17
+ check(context: LintContext): LintDiagnostic[] {
18
+ const { sourceFile } = context;
19
+ const diagnostics: LintDiagnostic[] = [];
20
+
21
+ function visit(node: ts.Node): void {
22
+ // Look for property assignments named "only" or "except"
23
+ // inside new Job(...) or similar constructor calls
24
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
25
+ const propName = node.name.text;
26
+ if (propName === "only" || propName === "except") {
27
+ // Check if this is inside a new expression (Job constructor)
28
+ let parent: ts.Node | undefined = node.parent;
29
+ while (parent) {
30
+ if (ts.isNewExpression(parent)) {
31
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
32
+ diagnostics.push({
33
+ file: sourceFile.fileName,
34
+ line: line + 1,
35
+ column: character + 1,
36
+ ruleId: "WGL001",
37
+ severity: "warning",
38
+ message: `"${propName}" is deprecated. Use "rules" for conditional job execution instead.`,
39
+ });
40
+ break;
41
+ }
42
+ parent = parent.parent;
43
+ }
44
+ }
45
+ }
46
+
47
+ ts.forEachChild(node, visit);
48
+ }
49
+
50
+ visit(sourceFile);
51
+ return diagnostics;
52
+ },
53
+ };
@@ -0,0 +1,65 @@
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
+
8
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
+ import * as ts from "typescript";
10
+
11
+ const VALID_EXECUTION_PROPS = new Set(["script", "trigger", "run"]);
12
+
13
+ export const missingScriptRule: LintRule = {
14
+ id: "WGL002",
15
+ severity: "error",
16
+ category: "correctness",
17
+
18
+ check(context: LintContext): LintDiagnostic[] {
19
+ const { sourceFile } = context;
20
+ const diagnostics: LintDiagnostic[] = [];
21
+
22
+ function visit(node: ts.Node): void {
23
+ if (ts.isNewExpression(node)) {
24
+ // Check for `new Job(...)` or `new gl.Job(...)`
25
+ let isJob = false;
26
+ const expression = node.expression;
27
+
28
+ if (ts.isIdentifier(expression) && expression.text === "Job") {
29
+ isJob = true;
30
+ } else if (ts.isPropertyAccessExpression(expression) && expression.name.text === "Job") {
31
+ isJob = true;
32
+ }
33
+
34
+ if (isJob && node.arguments && node.arguments.length > 0) {
35
+ const props = node.arguments[0];
36
+ if (ts.isObjectLiteralExpression(props)) {
37
+ const hasExecution = props.properties.some((prop) => {
38
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
39
+ return VALID_EXECUTION_PROPS.has(prop.name.text);
40
+ }
41
+ return false;
42
+ });
43
+
44
+ if (!hasExecution) {
45
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
46
+ diagnostics.push({
47
+ file: sourceFile.fileName,
48
+ line: line + 1,
49
+ column: character + 1,
50
+ ruleId: "WGL002",
51
+ severity: "error",
52
+ message: 'Job must have "script", "trigger", or "run" defined.',
53
+ });
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ ts.forEachChild(node, visit);
60
+ }
61
+
62
+ visit(sourceFile);
63
+ return diagnostics;
64
+ },
65
+ };
@@ -0,0 +1,62 @@
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
+
8
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
+ import * as ts from "typescript";
10
+
11
+ export const missingStageRule: LintRule = {
12
+ id: "WGL003",
13
+ severity: "info",
14
+ category: "style",
15
+
16
+ check(context: LintContext): LintDiagnostic[] {
17
+ const { sourceFile } = context;
18
+ const diagnostics: LintDiagnostic[] = [];
19
+
20
+ function visit(node: ts.Node): void {
21
+ if (ts.isNewExpression(node)) {
22
+ let isJob = false;
23
+ const expression = node.expression;
24
+
25
+ if (ts.isIdentifier(expression) && expression.text === "Job") {
26
+ isJob = true;
27
+ } else if (ts.isPropertyAccessExpression(expression) && expression.name.text === "Job") {
28
+ isJob = true;
29
+ }
30
+
31
+ if (isJob && node.arguments && node.arguments.length > 0) {
32
+ const props = node.arguments[0];
33
+ if (ts.isObjectLiteralExpression(props)) {
34
+ const hasStage = props.properties.some((prop) => {
35
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
36
+ return prop.name.text === "stage";
37
+ }
38
+ return false;
39
+ });
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
+
56
+ ts.forEachChild(node, visit);
57
+ }
58
+
59
+ visit(sourceFile);
60
+ return diagnostics;
61
+ },
62
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * WGL010: Undefined stage
3
+ *
4
+ * Detects jobs that reference a stage not declared in the `stages:` list.
5
+ * This will cause a pipeline validation error in GitLab.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput, extractStages, extractJobs } from "./yaml-helpers";
10
+
11
+ export const wgl010: PostSynthCheck = {
12
+ id: "WGL010",
13
+ description: "Job references a stage not in the stages list",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ const diagnostics: PostSynthDiagnostic[] = [];
17
+
18
+ for (const [, output] of ctx.outputs) {
19
+ const yaml = getPrimaryOutput(output);
20
+ const stages = extractStages(yaml);
21
+ if (stages.length === 0) continue; // No explicit stages — GitLab uses defaults
22
+
23
+ const stageSet = new Set(stages);
24
+ const jobs = extractJobs(yaml);
25
+
26
+ for (const [jobName, job] of jobs) {
27
+ if (job.stage && !stageSet.has(job.stage)) {
28
+ diagnostics.push({
29
+ checkId: "WGL010",
30
+ severity: "error",
31
+ message: `Job "${jobName}" references undefined stage "${job.stage}". Add it to the stages list.`,
32
+ entity: jobName,
33
+ lexicon: "gitlab",
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WGL011: Unreachable job
3
+ *
4
+ * Detects jobs where all `rules:` entries evaluate to `when: never`,
5
+ * making the job unreachable. This usually indicates a configuration error.
6
+ *
7
+ * Note: This is a simple static check — it only catches the obvious case
8
+ * where every rule has `when: "never"` literally set. Complex conditions
9
+ * with `if:` expressions are not evaluated.
10
+ */
11
+
12
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
13
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
14
+
15
+ export const wgl011: PostSynthCheck = {
16
+ id: "WGL011",
17
+ description: "Job has rules that always evaluate to never (unreachable)",
18
+
19
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+
22
+ for (const [entityName, entity] of ctx.entities) {
23
+ if (isPropertyDeclarable(entity)) continue;
24
+ const entityType = (entity as Record<string, unknown>).entityType as string;
25
+ if (entityType !== "GitLab::CI::Job") continue;
26
+
27
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
28
+ if (!props?.rules || !Array.isArray(props.rules)) continue;
29
+
30
+ const rules = props.rules as Array<Record<string, unknown>>;
31
+ if (rules.length === 0) continue;
32
+
33
+ // Check if ALL rules have when: "never"
34
+ const allNever = rules.every((rule) => {
35
+ // If the rule is a declarable (e.g. new Rule({...})), check its props
36
+ const ruleProps = (rule as Record<string, unknown>).props as Record<string, unknown> | undefined;
37
+ const when = ruleProps?.when ?? (rule as Record<string, unknown>).when;
38
+ return when === "never";
39
+ });
40
+
41
+ if (allNever) {
42
+ diagnostics.push({
43
+ checkId: "WGL011",
44
+ severity: "warning",
45
+ message: `Job "${entityName}" has rules that all evaluate to "never" — this job will never run.`,
46
+ entity: entityName,
47
+ lexicon: "gitlab",
48
+ });
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Helpers for parsing serialized GitLab CI YAML in post-synth checks.
3
+ *
4
+ * Since GitLab CI output is YAML (not JSON like CloudFormation), we parse
5
+ * the YAML sections structurally using simple regex/string parsing. The
6
+ * serializer emits a predictable format so we can extract what we need
7
+ * without a full YAML parser dependency.
8
+ */
9
+
10
+ import type { SerializerResult } from "@intentius/chant/serializer";
11
+
12
+ /**
13
+ * Extract the primary output string from a serializer result.
14
+ */
15
+ export function getPrimaryOutput(output: string | SerializerResult): string {
16
+ return typeof output === "string" ? output : output.primary;
17
+ }
18
+
19
+ /**
20
+ * Parse a serialized GitLab CI YAML into a structured object.
21
+ * Returns null if the output can't be parsed.
22
+ */
23
+ export interface ParsedGitLabCI {
24
+ stages: string[];
25
+ jobs: Map<string, ParsedJob>;
26
+ }
27
+
28
+ export interface ParsedJob {
29
+ name: string;
30
+ stage?: string;
31
+ rules?: ParsedRule[];
32
+ }
33
+
34
+ export interface ParsedRule {
35
+ when?: string;
36
+ if?: string;
37
+ }
38
+
39
+ /**
40
+ * Extract stages list from serialized YAML.
41
+ */
42
+ export function extractStages(yaml: string): string[] {
43
+ const stages: string[] = [];
44
+ const stagesMatch = yaml.match(/^stages:\n((?:\s+- .+\n?)+)/m);
45
+ if (stagesMatch) {
46
+ for (const line of stagesMatch[1].split("\n")) {
47
+ const item = line.match(/^\s+- (.+)$/);
48
+ if (item) stages.push(item[1].trim().replace(/^'|'$/g, ""));
49
+ }
50
+ }
51
+ return stages;
52
+ }
53
+
54
+ /**
55
+ * Extract job names and their stage values from serialized YAML.
56
+ */
57
+ export function extractJobs(yaml: string): Map<string, ParsedJob> {
58
+ const jobs = new Map<string, ParsedJob>();
59
+
60
+ // Split into sections by double newlines
61
+ const sections = yaml.split("\n\n");
62
+ for (const section of sections) {
63
+ const lines = section.split("\n");
64
+ if (lines.length === 0) continue;
65
+
66
+ // Top-level key
67
+ const topMatch = lines[0].match(/^([a-z][a-z0-9_-]*):/);
68
+ if (!topMatch) continue;
69
+
70
+ const name = topMatch[1];
71
+ // Skip reserved keys
72
+ if (["stages", "default", "workflow", "variables", "include"].includes(name)) continue;
73
+
74
+ const job: ParsedJob = { name };
75
+
76
+ // Find stage within the section
77
+ for (const line of lines) {
78
+ const stageMatch = line.match(/^\s+stage:\s+(.+)$/);
79
+ if (stageMatch) {
80
+ job.stage = stageMatch[1].trim().replace(/^'|'$/g, "");
81
+ }
82
+ }
83
+
84
+ jobs.set(name, job);
85
+ }
86
+
87
+ return jobs;
88
+ }
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: gitlab-ci
3
+ description: GitLab CI/CD best practices and common patterns
4
+ ---
5
+
6
+ # GitLab CI/CD with Chant
7
+
8
+ ## Common Entity Types
9
+
10
+ - `Job` — Pipeline job definition
11
+ - `Default` — Default settings inherited by all jobs
12
+ - `Workflow` — Pipeline-level configuration
13
+ - `Artifacts` — Job artifact configuration
14
+ - `Cache` — Cache configuration
15
+ - `Image` — Docker image for a job
16
+ - `Rule` — Conditional execution rule
17
+ - `Environment` — Deployment environment
18
+ - `Trigger` — Trigger downstream pipeline
19
+ - `Include` — Include external CI configuration
20
+
21
+ ## Predefined Variables
22
+
23
+ - `CI.CommitBranch` — Current branch name
24
+ - `CI.CommitSha` — Current commit SHA
25
+ - `CI.PipelineSource` — What triggered the pipeline
26
+ - `CI.ProjectPath` — Project path (group/project)
27
+ - `CI.Registry` — Container registry URL
28
+ - `CI.MergeRequestIid` — MR internal ID
29
+
30
+ ## Best Practices
31
+
32
+ 1. **Use stages** — Organize jobs into logical stages (build, test, deploy)
33
+ 2. **Cache dependencies** — Cache node_modules, pip packages, etc.
34
+ 3. **Use rules** — Prefer `rules:` over `only:/except:` for conditional execution
35
+ 4. **Minimize artifacts** — Only preserve files needed by later stages
36
+ 5. **Use includes** — Share common configuration across projects
37
+ 6. **Set timeouts** — Prevent stuck jobs from blocking pipelines
@@ -0,0 +1,248 @@
1
+ // Code generated by chant gitlab generate. DO NOT EDIT.
2
+
3
+ // --- CI Entity classes ---
4
+
5
+ export declare class AllowFailure {
6
+ constructor(props: {
7
+ exit_codes: number;
8
+ });
9
+ }
10
+
11
+ export declare class Artifacts {
12
+ constructor(props: {
13
+ access?: "none" | "developer" | "all";
14
+ exclude?: string[];
15
+ expire_in?: string;
16
+ expose_as?: string;
17
+ name?: string;
18
+ paths?: string[];
19
+ reports?: Record<string, unknown>;
20
+ untracked?: boolean;
21
+ when?: "on_success" | "on_failure" | "always";
22
+ });
23
+ }
24
+
25
+ export declare class AutoCancel {
26
+ constructor(props: {
27
+ on_job_failure?: "none" | "all";
28
+ on_new_commit?: "conservative" | "interruptible" | "none";
29
+ });
30
+ }
31
+
32
+ export declare class Cache {
33
+ constructor(props: {
34
+ fallback_keys?: string[];
35
+ key?: string | Record<string, any>;
36
+ paths?: string[];
37
+ policy?: string;
38
+ unprotect?: boolean;
39
+ untracked?: boolean;
40
+ when?: "on_success" | "on_failure" | "always";
41
+ });
42
+ }
43
+
44
+ export declare class Default {
45
+ constructor(props: {
46
+ after_script?: string | string[];
47
+ artifacts?: Artifacts;
48
+ before_script?: string | string[];
49
+ cache?: Cache | Cache[];
50
+ hooks?: Record<string, unknown>;
51
+ id_tokens?: Record<string, unknown>;
52
+ identity?: "google_cloud";
53
+ image?: Image;
54
+ interruptible?: boolean;
55
+ retry?: Retry | number;
56
+ services?: Service[];
57
+ tags?: any[];
58
+ timeout?: string;
59
+ });
60
+ }
61
+
62
+ export declare class Environment {
63
+ constructor(props: {
64
+ /** The name of the environment, e.g. 'qa', 'staging', 'production'. */
65
+ name: string;
66
+ /** Specifies what this job will do. 'start' (default) indicates the job will start the deployment. 'prepare'/'verify'/'access' indicates this will not affect the deployment. 'stop' indicates this will stop the deployment. */
67
+ action?: "start" | "prepare" | "stop" | "verify" | "access";
68
+ /** The amount of time it should take before Gitlab will automatically stop the environment. Supports a wide variety of formats, e.g. '1 week', '3 mins 4 sec', '2 hrs 20 min', '2h20min', '6 mos 1 day', '47 yrs 6 mos and 4d', '3 weeks and 2 days'. */
69
+ auto_stop_in?: string;
70
+ /** Explicitly specifies the tier of the deployment environment if non-standard environment name is used. */
71
+ deployment_tier?: "production" | "staging" | "testing" | "development" | "other";
72
+ /** Used to configure the kubernetes deployment for this environment. This is currently not supported for kubernetes clusters that are managed by Gitlab. */
73
+ kubernetes?: Record<string, unknown>;
74
+ /** The name of a job to execute when the environment is about to be stopped. */
75
+ on_stop?: string;
76
+ /** When set, this will expose buttons in various places for the current environment in Gitlab, that will take you to the defined URL. */
77
+ url?: string;
78
+ });
79
+ }
80
+
81
+ export declare class Image {
82
+ constructor(props: {
83
+ /** Full name of the image that should be used. It should contain the Registry part if needed. */
84
+ name: string;
85
+ docker?: Record<string, unknown>;
86
+ /** Command or script that should be executed as the container's entrypoint. It will be translated to Docker's --entrypoint option while creating the container. The syntax is similar to Dockerfile's ENTRYPOINT directive, where each shell token is a separate string in the array. */
87
+ entrypoint?: any[];
88
+ pull_policy?: "always" | "never" | "if-not-present" | "always" | "never" | "if-not-present"[];
89
+ });
90
+ }
91
+
92
+ export declare class Include {
93
+ constructor(props: {
94
+ file: string | string[];
95
+ /** Path to the project, e.g. `group/project`, or `group/sub-group/project` [Learn more](https://docs.gitlab.com/ee/ci/yaml/index.html#includefile). */
96
+ project: string;
97
+ inputs?: Record<string, unknown>;
98
+ /** Branch/Tag/Commit-hash for the target project. */
99
+ ref?: string;
100
+ rules?: any[];
101
+ });
102
+ }
103
+
104
+ export declare class Job {
105
+ constructor(props: {
106
+ after_script?: string | string[];
107
+ allow_failure?: AllowFailure | boolean;
108
+ artifacts?: Artifacts;
109
+ before_script?: string | string[];
110
+ cache?: Cache | Cache[];
111
+ /** Must be a regular expression, optionally but recommended to be quoted, and must be surrounded with '/'. Example: '/Code coverage: \d+\.\d+/' */
112
+ coverage?: string;
113
+ /** Specify a list of job names from earlier stages from which artifacts should be loaded. By default, all previous artifacts are passed. Use an empty array to skip downloading artifacts. */
114
+ dependencies?: string[];
115
+ /** Used to associate environment metadata with a deploy. Environment can have a name and URL attached to it, and will be displayed under /environments under the project. */
116
+ environment?: string | Record<string, any>;
117
+ /** Job will run *except* for when these filtering options match. */
118
+ except?: any;
119
+ /** The name of one or more jobs to inherit configuration from. */
120
+ extends?: string | string[];
121
+ hooks?: Record<string, unknown>;
122
+ id_tokens?: Record<string, unknown>;
123
+ identity?: "google_cloud";
124
+ image?: Image;
125
+ inherit?: Record<string, unknown>;
126
+ interruptible?: boolean;
127
+ manual_confirmation?: string;
128
+ /** The list of jobs in previous stages whose sole completion is needed to start the current job. */
129
+ needs?: string | Record<string, any> | any[][];
130
+ /** Job will run *only* when these filtering options match. */
131
+ only?: any;
132
+ pages?: Record<string, any> | boolean;
133
+ parallel?: Parallel | number;
134
+ /** A path to a directory that contains the files to be published with Pages */
135
+ publish?: string;
136
+ /** Indicates that the job creates a Release. */
137
+ release?: Record<string, unknown>;
138
+ /** Limit job concurrency. Can be used to ensure that the Runner will not run certain jobs simultaneously. */
139
+ resource_group?: string;
140
+ retry?: Retry | number;
141
+ rules?: Rule[];
142
+ run?: any[];
143
+ script?: string | string[];
144
+ secrets?: Record<string, unknown>;
145
+ services?: Service[];
146
+ /** Define what stage the job will run in. */
147
+ stage?: string | string[];
148
+ start_in?: string;
149
+ tags?: any[];
150
+ timeout?: string;
151
+ trigger?: Record<string, any> | string;
152
+ variables?: Record<string, unknown>;
153
+ when?: "on_success" | "on_failure" | "always" | "never" | "manual" | "delayed";
154
+ });
155
+ }
156
+
157
+ export declare class Parallel {
158
+ constructor(props: {
159
+ /** Defines different variables for jobs that are running in parallel. */
160
+ matrix: Record<string, unknown>[];
161
+ });
162
+ }
163
+
164
+ export declare class Release {
165
+ constructor(props: {
166
+ /** Specifies the longer description of the Release. */
167
+ description: string;
168
+ /** The tag_name must be specified. It can refer to an existing Git tag or can be specified by the user. */
169
+ tag_name: string;
170
+ assets?: Record<string, unknown>;
171
+ /** The title of each milestone the release is associated with. */
172
+ milestones?: string[];
173
+ /** The Release name. If omitted, it is populated with the value of release: tag_name. */
174
+ name?: string;
175
+ /** If the release: tag_name doesn’t exist yet, the release is created from ref. ref can be a commit SHA, another tag name, or a branch name. */
176
+ ref?: string;
177
+ /** The date and time when the release is ready. Defaults to the current date and time if not defined. Should be enclosed in quotes and expressed in ISO 8601 format. */
178
+ released_at?: string;
179
+ /** Message to use if creating a new annotated tag. */
180
+ tag_message?: string;
181
+ });
182
+ }
183
+
184
+ export declare class Retry {
185
+ constructor(props: {
186
+ exit_codes?: number[] | number;
187
+ max?: number;
188
+ when?: any[];
189
+ });
190
+ }
191
+
192
+ export declare class Rule {
193
+ constructor(props: {
194
+ allow_failure?: AllowFailure | boolean;
195
+ changes?: any;
196
+ exists?: any;
197
+ if?: string;
198
+ interruptible?: boolean;
199
+ needs?: any[];
200
+ start_in?: string;
201
+ variables?: Record<string, unknown>;
202
+ when?: "on_success" | "on_failure" | "always" | "never" | "manual" | "delayed";
203
+ });
204
+ }
205
+
206
+ export declare class Trigger {
207
+ constructor(props: {
208
+ /** Path to the project, e.g. `group/project`, or `group/sub-group/project`. */
209
+ project: string;
210
+ /** The branch name that a downstream pipeline will use */
211
+ branch?: string;
212
+ /** Specify what to forward to the downstream pipeline. */
213
+ forward?: Record<string, unknown>;
214
+ /** You can mirror the pipeline status from the triggered pipeline to the source bridge job by using strategy: depend */
215
+ strategy?: "depend";
216
+ });
217
+ }
218
+
219
+ export declare class Workflow {
220
+ constructor(props: {
221
+ auto_cancel?: AutoCancel;
222
+ name?: string;
223
+ rules?: Record<string, any> | string[][];
224
+ });
225
+ }
226
+
227
+ // --- CI/CD Variables ---
228
+
229
+ export declare const CI: {
230
+ readonly CommitBranch: string;
231
+ readonly CommitRef: string;
232
+ readonly CommitSha: string;
233
+ readonly CommitTag: string;
234
+ readonly DefaultBranch: string;
235
+ readonly Environment: string;
236
+ readonly JobId: string;
237
+ readonly JobName: string;
238
+ readonly JobStage: string;
239
+ readonly MergeRequestIid: string;
240
+ readonly PipelineId: string;
241
+ readonly PipelineSource: string;
242
+ readonly ProjectDir: string;
243
+ readonly ProjectId: string;
244
+ readonly ProjectName: string;
245
+ readonly ProjectPath: string;
246
+ readonly Registry: string;
247
+ readonly RegistryImage: string;
248
+ };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gitlab",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
+ "license": "Apache-2.0",
4
5
  "type": "module",
5
6
  "files": ["src/", "dist/"],
6
7
  "publishConfig": {
@@ -15,8 +16,9 @@
15
16
  },
16
17
  "scripts": {
17
18
  "generate": "bun run src/codegen/generate-cli.ts",
19
+ "bundle": "bun run src/package-cli.ts",
18
20
  "validate": "bun run src/validate-cli.ts",
19
- "prepack": "bun run generate && bun run validate"
21
+ "prepack": "bun run bundle && bun run validate"
20
22
  },
21
23
  "dependencies": {
22
24
  "@intentius/chant": "0.0.1"
@@ -948,7 +948,7 @@ deploy:
948
948
  `,
949
949
  },
950
950
  ],
951
- basePath: "/lexicons/gitlab/",
951
+ basePath: "/chant/lexicons/gitlab/",
952
952
  };
953
953
 
954
954
  const result = await docsPipeline(config);
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Thin entry point for `bun run bundle` in lexicon-gitlab.
4
+ * Generates src/generated/ files and writes dist/ bundle.
5
+ */
6
+ import { generate, writeGeneratedFiles } from "./codegen/generate";
7
+ import { packageLexicon } from "./codegen/package";
8
+ import { writeFileSync, mkdirSync } from "fs";
9
+ import { join, dirname } from "path";
10
+ import { fileURLToPath } from "url";
11
+
12
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
13
+
14
+ // 1. Generate src/generated/ files
15
+ const genResult = await generate({ verbose: true });
16
+ writeGeneratedFiles(genResult, pkgDir);
17
+ console.error(`Generated ${genResult.resources} entities, ${genResult.properties} property types, ${genResult.enums} enums`);
18
+
19
+ // 2. Run package pipeline and write dist/
20
+ const { spec, stats } = await packageLexicon({ verbose: true });
21
+
22
+ const distDir = join(pkgDir, "dist");
23
+ mkdirSync(join(distDir, "types"), { recursive: true });
24
+ mkdirSync(join(distDir, "rules"), { recursive: true });
25
+ mkdirSync(join(distDir, "skills"), { recursive: true });
26
+
27
+ writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
28
+ writeFileSync(join(distDir, "meta.json"), spec.registry);
29
+ writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
30
+
31
+ for (const [name, content] of spec.rules) {
32
+ writeFileSync(join(distDir, "rules", name), content);
33
+ }
34
+ for (const [name, content] of spec.skills) {
35
+ writeFileSync(join(distDir, "skills", name), content);
36
+ }
37
+
38
+ if (spec.integrity) {
39
+ writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
40
+ }
41
+
42
+ console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
43
+ console.error(`dist/ written to ${distDir}`);