@intentius/chant-lexicon-gitlab 0.0.10 → 0.0.12
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/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/rules/yaml-helpers.ts +1 -8
- package/package.json +2 -2
- package/src/codegen/docs.ts +107 -2
- package/src/import/parser.ts +1 -167
- package/src/lint/post-synth/yaml-helpers.ts +1 -8
- package/src/serializer.ts +1 -105
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "xxhash64",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "6bd0f9deb20b8705",
|
|
5
5
|
"meta.json": "c663c6c63748a9d0",
|
|
6
6
|
"types/index.d.ts": "64e65524615be023",
|
|
7
7
|
"rules/missing-stage.ts": "6d5379e74209a735",
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
"rules/wgl011.ts": "b6b97e5104d91267",
|
|
12
12
|
"rules/wgl015.ts": "d7e9e080994f985",
|
|
13
13
|
"rules/wgl012.ts": "3d188d13fb2236c0",
|
|
14
|
-
"rules/yaml-helpers.ts": "
|
|
14
|
+
"rules/yaml-helpers.ts": "b5416b80369484f2",
|
|
15
15
|
"rules/wgl010.ts": "1548cad287cdf286",
|
|
16
16
|
"rules/wgl014.ts": "6248a852888e8028",
|
|
17
17
|
"rules/wgl013.ts": "3519c933e23fc605",
|
|
18
18
|
"skills/chant-gitlab.md": "4393eb63e0b84b7f"
|
|
19
19
|
},
|
|
20
|
-
"composite": "
|
|
20
|
+
"composite": "95f5813f9d4b37c7"
|
|
21
21
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -7,14 +7,7 @@
|
|
|
7
7
|
* without a full YAML parser dependency.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
}
|
|
10
|
+
export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
18
11
|
|
|
19
12
|
/**
|
|
20
13
|
* Parse a serialized GitLab CI YAML into a structured object.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gitlab",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": ["src/", "dist/"],
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"prepack": "bun run bundle && bun run validate"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@intentius/chant": "0.0.
|
|
24
|
+
"@intentius/chant": "0.0.11"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"typescript": "^5.9.3"
|
package/src/codegen/docs.ts
CHANGED
|
@@ -460,8 +460,8 @@ export default {
|
|
|
460
460
|
{
|
|
461
461
|
slug: "examples",
|
|
462
462
|
title: "Examples",
|
|
463
|
-
description: "Walkthrough of
|
|
464
|
-
content: `
|
|
463
|
+
description: "Walkthrough of GitLab CI/CD examples — pipelines, composites, and cross-lexicon patterns",
|
|
464
|
+
content: `Runnable examples live in the lexicon's \`examples/\` directory. Clone the repo and try them:
|
|
465
465
|
|
|
466
466
|
\`\`\`bash
|
|
467
467
|
cd examples/getting-started
|
|
@@ -559,6 +559,111 @@ deploy:
|
|
|
559
559
|
2. **Conditional execution** — merge request and branch rules control when jobs run
|
|
560
560
|
3. **Manual deployment** — deploy requires manual trigger on the default branch
|
|
561
561
|
4. **JUnit reports** — test artifacts include JUnit XML for GitLab MR display
|
|
562
|
+
|
|
563
|
+
## Docker Build
|
|
564
|
+
|
|
565
|
+
\`examples/docker-build/\` — builds and pushes a Docker image using the \`DockerBuild\` composite.
|
|
566
|
+
|
|
567
|
+
{{file:docker-build/src/pipeline.ts}}
|
|
568
|
+
|
|
569
|
+
The \`DockerBuild\` composite expands to a job with Docker-in-Docker service, registry login, build, and push steps.
|
|
570
|
+
|
|
571
|
+
## Node Pipeline
|
|
572
|
+
|
|
573
|
+
\`examples/node-pipeline/\` — a full Node.js CI pipeline using the \`NodePipeline\` composite.
|
|
574
|
+
|
|
575
|
+
{{file:node-pipeline/src/pipeline.ts}}
|
|
576
|
+
|
|
577
|
+
## Python Pipeline
|
|
578
|
+
|
|
579
|
+
\`examples/python-pipeline/\` — a Python CI pipeline using the \`PythonPipeline\` composite.
|
|
580
|
+
|
|
581
|
+
{{file:python-pipeline/src/pipeline.ts}}
|
|
582
|
+
|
|
583
|
+
## Review App
|
|
584
|
+
|
|
585
|
+
\`examples/review-app/\` — deploys a review environment per merge request using the \`ReviewApp\` composite.
|
|
586
|
+
|
|
587
|
+
{{file:review-app/src/pipeline.ts}}
|
|
588
|
+
|
|
589
|
+
## AWS ALB Deployment
|
|
590
|
+
|
|
591
|
+
A cross-lexicon example showing how to deploy AWS CloudFormation stacks from GitLab CI. Three separate pipelines mirror the separate-project AWS ALB pattern:
|
|
592
|
+
|
|
593
|
+
### Infra pipeline
|
|
594
|
+
|
|
595
|
+
Deploys the shared ALB stack (VPC, ALB, ECS cluster, ECR repos):
|
|
596
|
+
|
|
597
|
+
\`\`\`typescript
|
|
598
|
+
import { Job, Image, Rule } from "@intentius/chant-lexicon-gitlab";
|
|
599
|
+
|
|
600
|
+
const awsImage = new Image({ name: "amazon/aws-cli:latest" });
|
|
601
|
+
|
|
602
|
+
export const deployInfra = new Job({
|
|
603
|
+
stage: "deploy",
|
|
604
|
+
image: awsImage,
|
|
605
|
+
script: [
|
|
606
|
+
"aws cloudformation deploy --template-file templates/template.json --stack-name shared-alb --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset",
|
|
607
|
+
],
|
|
608
|
+
rules: [
|
|
609
|
+
new Rule({ if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH" }),
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
\`\`\`
|
|
613
|
+
|
|
614
|
+
### Service pipeline (API)
|
|
615
|
+
|
|
616
|
+
Builds a Docker image, pushes to ECR, and deploys the API service stack with cross-stack parameter passing:
|
|
617
|
+
|
|
618
|
+
\`\`\`typescript
|
|
619
|
+
import { Job, Image, Service, Need, Rule } from "@intentius/chant-lexicon-gitlab";
|
|
620
|
+
import { CI } from "@intentius/chant-lexicon-gitlab";
|
|
621
|
+
|
|
622
|
+
const ECR_URL = "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com";
|
|
623
|
+
const ECR_REPO = "alb-api";
|
|
624
|
+
const fullImage = \\\`\\\${ECR_URL}/\\\${ECR_REPO}\\\`;
|
|
625
|
+
|
|
626
|
+
const dockerImage = new Image({ name: "docker:27-cli" });
|
|
627
|
+
const dind = new Service({ name: "docker:27-dind", alias: "docker" });
|
|
628
|
+
|
|
629
|
+
export const buildImage = new Job({
|
|
630
|
+
stage: "build",
|
|
631
|
+
image: dockerImage,
|
|
632
|
+
services: [dind],
|
|
633
|
+
variables: { DOCKER_TLS_CERTDIR: "/certs" },
|
|
634
|
+
before_script: [
|
|
635
|
+
"apk add --no-cache aws-cli",
|
|
636
|
+
\\\`aws ecr get-login-password | docker login --username AWS --password-stdin \\\${ECR_URL}\\\`,
|
|
637
|
+
],
|
|
638
|
+
script: [
|
|
639
|
+
\\\`docker build -t \\\${fullImage}:\\\${CI.CommitRefSlug} .\\\`,
|
|
640
|
+
\\\`docker push \\\${fullImage}:\\\${CI.CommitRefSlug}\\\`,
|
|
641
|
+
],
|
|
642
|
+
rules: [new Rule({ if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH" })],
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
export const deployService = new Job({
|
|
646
|
+
stage: "deploy",
|
|
647
|
+
image: new Image({ name: "amazon/aws-cli:latest" }),
|
|
648
|
+
needs: [new Need({ job: "build-image" })],
|
|
649
|
+
script: [
|
|
650
|
+
// Fetch shared ALB outputs and map to CF parameter overrides
|
|
651
|
+
"OUTPUTS=$(aws cloudformation describe-stacks --stack-name shared-alb --query 'Stacks[0].Outputs' --output json)",
|
|
652
|
+
'PARAMS=$(echo "$OUTPUTS" | jq -r \\'[...output-to-param mapping...] | join(" ")\\')',
|
|
653
|
+
"aws cloudformation deploy --template-file templates/template.json --stack-name shared-alb-api --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset --parameter-overrides $PARAMS",
|
|
654
|
+
],
|
|
655
|
+
rules: [new Rule({ if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH" })],
|
|
656
|
+
});
|
|
657
|
+
\`\`\`
|
|
658
|
+
|
|
659
|
+
**Key patterns:**
|
|
660
|
+
|
|
661
|
+
1. **ECR login** — uses \`aws ecr get-login-password\` instead of GitLab registry credentials
|
|
662
|
+
2. **Cross-stack parameter passing** — \`describe-stacks\` fetches outputs from the infra stack, \`jq\` maps them to \`--parameter-overrides\`
|
|
663
|
+
3. **Job naming** — \`buildImage\` serializes to \`build-image\` in YAML; \`Need\` references must use kebab-case
|
|
664
|
+
4. **Docker-in-Docker** — \`docker:27-cli\` image with \`docker:27-dind\` service for container builds
|
|
665
|
+
|
|
666
|
+
The full examples live in \`examples/gitlab-aws-alb-infra/\`, \`examples/gitlab-aws-alb-api/\`, and \`examples/gitlab-aws-alb-ui/\`.
|
|
562
667
|
`,
|
|
563
668
|
},
|
|
564
669
|
{
|
package/src/import/parser.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TemplateParser, TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
|
|
9
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Reserved top-level keys in .gitlab-ci.yml that are NOT job definitions.
|
|
@@ -33,173 +34,6 @@ const RESERVED_KEYS = new Set([
|
|
|
33
34
|
]);
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
* Parse a YAML document into a plain object.
|
|
38
|
-
* Uses a simple YAML parser approach — GitLab CI YAML is straightforward
|
|
39
|
-
* enough that we can parse it without a full YAML library by parsing JSON
|
|
40
|
-
* or using Bun's built-in YAML support if available.
|
|
41
|
-
*/
|
|
42
|
-
function parseYAML(content: string): Record<string, unknown> {
|
|
43
|
-
// Try JSON first (some CI files may be JSON)
|
|
44
|
-
try {
|
|
45
|
-
return JSON.parse(content);
|
|
46
|
-
} catch {
|
|
47
|
-
// Fall through to YAML parsing
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Simple YAML parser for GitLab CI files
|
|
51
|
-
// This handles the common cases: scalars, arrays, objects, block scalars
|
|
52
|
-
const lines = content.split("\n");
|
|
53
|
-
return parseYAMLLines(lines, 0, 0).value as Record<string, unknown>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface ParseResult {
|
|
57
|
-
value: unknown;
|
|
58
|
-
endIndex: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseYAMLLines(lines: string[], startIndex: number, baseIndent: number): ParseResult {
|
|
62
|
-
const result: Record<string, unknown> = {};
|
|
63
|
-
let i = startIndex;
|
|
64
|
-
|
|
65
|
-
while (i < lines.length) {
|
|
66
|
-
const line = lines[i];
|
|
67
|
-
// Skip empty lines and comments
|
|
68
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
69
|
-
i++;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const indent = line.search(/\S/);
|
|
74
|
-
if (indent < baseIndent) break; // Dedented — done with this block
|
|
75
|
-
if (indent > baseIndent && startIndex > 0) break; // Unexpected indent
|
|
76
|
-
|
|
77
|
-
const keyMatch = line.match(/^(\s*)([^\s:][^:]*?):\s*(.*)$/);
|
|
78
|
-
if (keyMatch) {
|
|
79
|
-
const key = keyMatch[2].trim();
|
|
80
|
-
const inlineValue = keyMatch[3].trim();
|
|
81
|
-
|
|
82
|
-
if (inlineValue === "" || inlineValue.startsWith("#")) {
|
|
83
|
-
// Check next line for array or nested object
|
|
84
|
-
if (i + 1 < lines.length) {
|
|
85
|
-
const nextLine = lines[i + 1];
|
|
86
|
-
const nextIndent = nextLine.search(/\S/);
|
|
87
|
-
if (nextIndent > indent && nextLine.trimStart().startsWith("- ")) {
|
|
88
|
-
// Array
|
|
89
|
-
const arr = parseYAMLArray(lines, i + 1, nextIndent);
|
|
90
|
-
result[key] = arr.value;
|
|
91
|
-
i = arr.endIndex;
|
|
92
|
-
continue;
|
|
93
|
-
} else if (nextIndent > indent) {
|
|
94
|
-
// Nested object
|
|
95
|
-
const nested = parseYAMLLines(lines, i + 1, nextIndent);
|
|
96
|
-
result[key] = nested.value;
|
|
97
|
-
i = nested.endIndex;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
result[key] = null;
|
|
102
|
-
i++;
|
|
103
|
-
} else if (inlineValue.startsWith("[")) {
|
|
104
|
-
// Inline array
|
|
105
|
-
try {
|
|
106
|
-
result[key] = JSON.parse(inlineValue);
|
|
107
|
-
} catch {
|
|
108
|
-
result[key] = inlineValue;
|
|
109
|
-
}
|
|
110
|
-
i++;
|
|
111
|
-
} else if (inlineValue.startsWith("{")) {
|
|
112
|
-
// Inline object
|
|
113
|
-
try {
|
|
114
|
-
result[key] = JSON.parse(inlineValue);
|
|
115
|
-
} catch {
|
|
116
|
-
result[key] = inlineValue;
|
|
117
|
-
}
|
|
118
|
-
i++;
|
|
119
|
-
} else {
|
|
120
|
-
result[key] = parseScalar(inlineValue);
|
|
121
|
-
i++;
|
|
122
|
-
}
|
|
123
|
-
} else if (line.trimStart().startsWith("- ")) {
|
|
124
|
-
// We hit an array at the top level — shouldn't happen normally
|
|
125
|
-
break;
|
|
126
|
-
} else {
|
|
127
|
-
i++;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { value: result, endIndex: i };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function parseYAMLArray(lines: string[], startIndex: number, baseIndent: number): ParseResult {
|
|
135
|
-
const result: unknown[] = [];
|
|
136
|
-
let i = startIndex;
|
|
137
|
-
|
|
138
|
-
while (i < lines.length) {
|
|
139
|
-
const line = lines[i];
|
|
140
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
141
|
-
i++;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const indent = line.search(/\S/);
|
|
146
|
-
if (indent < baseIndent) break;
|
|
147
|
-
|
|
148
|
-
const itemMatch = line.match(/^(\s*)- (.*)$/);
|
|
149
|
-
if (itemMatch && indent === baseIndent) {
|
|
150
|
-
const itemValue = itemMatch[2].trim();
|
|
151
|
-
// Check if it's a key-value pair (object item in array)
|
|
152
|
-
const kvMatch = itemValue.match(/^([^\s:][^:]*?):\s*(.*)$/);
|
|
153
|
-
if (kvMatch) {
|
|
154
|
-
const obj: Record<string, unknown> = {};
|
|
155
|
-
obj[kvMatch[1].trim()] = parseScalar(kvMatch[2].trim());
|
|
156
|
-
// Check for more keys at indent+2
|
|
157
|
-
const nextIndent = indent + 2;
|
|
158
|
-
let j = i + 1;
|
|
159
|
-
while (j < lines.length) {
|
|
160
|
-
const nextLine = lines[j];
|
|
161
|
-
if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
|
|
162
|
-
j++;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const ni = nextLine.search(/\S/);
|
|
166
|
-
if (ni !== nextIndent) break;
|
|
167
|
-
const nextKV = nextLine.match(/^(\s*)([^\s:][^:]*?):\s*(.*)$/);
|
|
168
|
-
if (nextKV) {
|
|
169
|
-
obj[nextKV[2].trim()] = parseScalar(nextKV[3].trim());
|
|
170
|
-
j++;
|
|
171
|
-
} else {
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
result.push(obj);
|
|
176
|
-
i = j;
|
|
177
|
-
} else {
|
|
178
|
-
result.push(parseScalar(itemValue));
|
|
179
|
-
i++;
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return { value: result, endIndex: i };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function parseScalar(value: string): unknown {
|
|
190
|
-
if (value === "" || value === "~" || value === "null") return null;
|
|
191
|
-
if (value === "true" || value === "yes") return true;
|
|
192
|
-
if (value === "false" || value === "no") return false;
|
|
193
|
-
// Strip quotes
|
|
194
|
-
if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
|
|
195
|
-
return value.slice(1, -1);
|
|
196
|
-
}
|
|
197
|
-
// Number
|
|
198
|
-
const num = Number(value);
|
|
199
|
-
if (!isNaN(num) && value !== "") return num;
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
37
|
/**
|
|
204
38
|
* GitLab CI YAML parser implementation.
|
|
205
39
|
*/
|
|
@@ -7,14 +7,7 @@
|
|
|
7
7
|
* without a full YAML parser dependency.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
}
|
|
10
|
+
export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
18
11
|
|
|
19
12
|
/**
|
|
20
13
|
* Parse a serialized GitLab CI YAML into a structured object.
|
package/src/serializer.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { Serializer } from "@intentius/chant/serializer";
|
|
|
13
13
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
14
14
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
15
15
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
16
|
+
import { emitYAML } from "@intentius/chant/yaml";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* GitLab CI visitor for the generic serializer walker.
|
|
@@ -82,111 +83,6 @@ function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unkn
|
|
|
82
83
|
return walkValue(preprocessed, entityNames, gitlabVisitor(entityNames));
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
/**
|
|
86
|
-
* Emit a YAML value with proper indentation.
|
|
87
|
-
*/
|
|
88
|
-
function emitYAML(value: unknown, indent: number): string {
|
|
89
|
-
const prefix = " ".repeat(indent);
|
|
90
|
-
|
|
91
|
-
if (value === null || value === undefined) {
|
|
92
|
-
return "null";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (typeof value === "boolean") {
|
|
96
|
-
return value ? "true" : "false";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (typeof value === "number") {
|
|
100
|
-
return String(value);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (typeof value === "string") {
|
|
104
|
-
// Quote strings that could be misinterpreted
|
|
105
|
-
if (
|
|
106
|
-
value === "" ||
|
|
107
|
-
value === "true" ||
|
|
108
|
-
value === "false" ||
|
|
109
|
-
value === "null" ||
|
|
110
|
-
value === "yes" ||
|
|
111
|
-
value === "no" ||
|
|
112
|
-
value.includes(": ") ||
|
|
113
|
-
value.includes("#") ||
|
|
114
|
-
value.startsWith("*") ||
|
|
115
|
-
value.startsWith("&") ||
|
|
116
|
-
value.startsWith("!") ||
|
|
117
|
-
value.startsWith("{") ||
|
|
118
|
-
value.startsWith("[") ||
|
|
119
|
-
value.startsWith("'") ||
|
|
120
|
-
value.startsWith('"') ||
|
|
121
|
-
value.startsWith("$") ||
|
|
122
|
-
/^\d/.test(value)
|
|
123
|
-
) {
|
|
124
|
-
// Use single quotes, escaping internal single quotes
|
|
125
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
126
|
-
}
|
|
127
|
-
return value;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (Array.isArray(value)) {
|
|
131
|
-
if (value.length === 0) return "[]";
|
|
132
|
-
const lines: string[] = [];
|
|
133
|
-
for (const item of value) {
|
|
134
|
-
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
135
|
-
// Object items in arrays
|
|
136
|
-
const entries = Object.entries(item as Record<string, unknown>);
|
|
137
|
-
if (entries.length > 0) {
|
|
138
|
-
const [firstKey, firstVal] = entries[0];
|
|
139
|
-
const firstEmitted = emitYAML(firstVal, indent + 2);
|
|
140
|
-
if (firstEmitted.startsWith("\n")) {
|
|
141
|
-
// Multi-line value: put on next line, indented under the key
|
|
142
|
-
lines.push(`${prefix}- ${firstKey}:${firstEmitted}`);
|
|
143
|
-
} else {
|
|
144
|
-
lines.push(`${prefix}- ${firstKey}: ${firstEmitted}`);
|
|
145
|
-
}
|
|
146
|
-
for (let i = 1; i < entries.length; i++) {
|
|
147
|
-
const [key, val] = entries[i];
|
|
148
|
-
const emitted = emitYAML(val, indent + 2);
|
|
149
|
-
if (emitted.startsWith("\n")) {
|
|
150
|
-
lines.push(`${prefix} ${key}:${emitted}`);
|
|
151
|
-
} else {
|
|
152
|
-
lines.push(`${prefix} ${key}: ${emitted}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
lines.push(`${prefix}- ${emitYAML(item, indent + 1).trimStart()}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return "\n" + lines.join("\n");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (typeof value === "object") {
|
|
164
|
-
const obj = value as Record<string, unknown>;
|
|
165
|
-
|
|
166
|
-
// Handle tagged values (intrinsics like !reference)
|
|
167
|
-
if ("tag" in obj && "value" in obj && typeof obj.tag === "string") {
|
|
168
|
-
if (obj.tag === "!reference" && Array.isArray(obj.value)) {
|
|
169
|
-
return `!reference [${(obj.value as string[]).join(", ")}]`;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const entries = Object.entries(obj);
|
|
174
|
-
if (entries.length === 0) return "{}";
|
|
175
|
-
const lines: string[] = [];
|
|
176
|
-
for (const [key, val] of entries) {
|
|
177
|
-
const emitted = emitYAML(val, indent + 1);
|
|
178
|
-
if (emitted.startsWith("\n")) {
|
|
179
|
-
lines.push(`${prefix}${key}:${emitted}`);
|
|
180
|
-
} else {
|
|
181
|
-
lines.push(`${prefix}${key}: ${emitted}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return "\n" + lines.join("\n");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return String(value);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
86
|
/**
|
|
191
87
|
* GitLab CI YAML serializer implementation.
|
|
192
88
|
*/
|