@intentius/chant-lexicon-gitlab 0.79.0 → 0.80.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAChD,MAAM,0BAA0B,CAAC;AAElC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAS1E;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAqFtB"}
1
+ {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAEjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAChD,MAAM,0BAA0B,CAAC;AAElC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAS1E;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAyHtB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "77df7f883d9d0a9adf5d80b4e936dd5a076473afaffba5b70d0606f3ac927343",
4
+ "manifest.json": "073de2644bb1121ba60a3afcbffb9c821b972968c75e1b966446fe5a1833bd29",
5
5
  "meta.json": "931fc3246a55645b1493080bbeb160e5d205e42349e8bdca96ca243adb5f0da3",
6
6
  "types/index.d.ts": "5cd2e99f135a929b72bdd822d00d780d39b1cd407ba0cfb3d511c7ac9d667b58",
7
7
  "rules/artifact-no-expiry.ts": "3f3cabf9792cbf8207e53a25f506715466b19ec25e9c3b4d0d77fed6b2eb4542",
@@ -55,5 +55,5 @@
55
55
  "skills/chant-gitlab-migrate.md": "4853d04980560b379e5b0f7267dcb0a1e5d5879cb9e9931681ce76b2a5fe69d8",
56
56
  "skills/chant-gitlab-patterns.md": "6d9a44e9e8de4c3820be9d65381b0d2ede4cb3626aaaf0011e21838a9fbf556e"
57
57
  },
58
- "composite": "fe857e0abc3900a63ccea9a656ec8bd996cd4330bb4efc8c8fdc2ae2cfc1072e"
58
+ "composite": "163341752b7f5181bd0682992b19dd2242df3f5b977dd5c6c64303b4ebc11f7e"
59
59
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitLab",
6
6
  "intrinsics": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gitlab",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
4
4
  "description": "GitLab CI lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -67,8 +67,8 @@
67
67
  "typescript": "^5.9.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@intentius/chant": "^0.79.0",
71
- "@intentius/chant-lexicon-github": "^0.79.0",
70
+ "@intentius/chant": "^0.80.0",
71
+ "@intentius/chant-lexicon-github": "^0.80.0",
72
72
  "typescript": "^5.9.3"
73
73
  },
74
74
  "peerDependenciesMeta": {
@@ -257,3 +257,40 @@ describe("generateGitlabPipeline: options", () => {
257
257
  expect((job.script as string[])[0]).toContain("--env production");
258
258
  });
259
259
  });
260
+
261
+ describe("generateGitlabPipeline: a promote job (#2575)", () => {
262
+ const components = (): DriverComponent[] => [
263
+ { name: "shared-alb", dependsOn: [], deploy: [] },
264
+ {
265
+ name: "api",
266
+ dependsOn: ["shared-alb"],
267
+ deploy: [
268
+ { phase: "Build", steps: [{ kind: "docker-build", context: ".", into: "dist/api.tar" }] },
269
+ { phase: "Publish", steps: [{ kind: "publish-image", from: "archive:dist/api.tar" }] },
270
+ ],
271
+ },
272
+ ];
273
+
274
+ test("without promoteTo the pipeline is unchanged", () => {
275
+ const yaml = generateGitlabPipeline(components(), { env: "staging" }).yaml;
276
+ expect(yaml).not.toContain("promote");
277
+ expect(yaml).not.toContain("dist/api.tar");
278
+ });
279
+
280
+ test("building jobs keep their archive as artifacts, and a promote stage job needs every component job", () => {
281
+ const result = generateGitlabPipeline(components(), { env: "staging", promoteTo: "prod" });
282
+ const parsed = parseYAML(result.yaml);
283
+
284
+ expect((parsed["api"] as Record<string, unknown>).artifacts).toEqual({ paths: ["dist/api.tar"] });
285
+ expect((parsed["shared-alb"] as Record<string, unknown>).artifacts).toEqual({ paths: ["shared-alb.outputs.json"] });
286
+
287
+ expect(parsed.stages).toEqual(["wave-1", "wave-2", "promote"]);
288
+ expect(result.stages).toEqual(["wave-1", "wave-2"]);
289
+ expect(parsed["promote-prod"]).toEqual({
290
+ stage: "promote",
291
+ image: "node:22-slim",
292
+ script: ["chant components promote --from staging --to prod"],
293
+ needs: ["api", "shared-alb"],
294
+ });
295
+ });
296
+ });
@@ -34,6 +34,7 @@
34
34
 
35
35
  import { emitYAML } from "@intentius/chant/yaml";
36
36
  import { resolveComponentGraph, type DriverComponent } from "@intentius/chant/components/driver";
37
+ import { promoteArchivePaths } from "@intentius/chant/components/promote";
37
38
  import type {
38
39
  ComponentPipelineJob as GeneratedJob,
39
40
  ComponentPipelineOptions as GenerateGitlabOptions,
@@ -82,6 +83,19 @@ export function generateGitlabPipeline(
82
83
  for (const c of components) for (const dep of c.dependsOn ?? []) dependedUpon.add(dep);
83
84
  const outputsFile = (name: string) => `${name}.outputs.json`;
84
85
 
86
+ // A promote job (#2575) runs apart from the deploy jobs, and a promote
87
+ // publishes from the build archive on disk, so each component job keeps the
88
+ // files its build steps wrote as artifacts. GitLab hands them to the promote
89
+ // job across its `needs:` edges.
90
+ const promoteTo = options.promoteTo;
91
+ const archives = new Map<string, string[]>();
92
+ if (promoteTo !== undefined) {
93
+ for (const c of components) {
94
+ const paths = promoteArchivePaths(c);
95
+ if (paths.length > 0) archives.set(c.name, paths);
96
+ }
97
+ }
98
+
85
99
  const stages = waves.map((_, i) => `wave-${i + 1}`);
86
100
  const jobs: GeneratedJob[] = [];
87
101
  const jobNameByComponent = new Map<string, string>();
@@ -125,11 +139,30 @@ export function generateGitlabPipeline(
125
139
  };
126
140
  if (needs.length > 0) jobProps.needs = needs;
127
141
  // Publish this component's dumped outputs so dependent jobs receive it.
128
- if (dependedUpon.has(name)) jobProps.artifacts = { paths: [outputsFile(name)] };
142
+ const artifactPaths = [
143
+ ...(dependedUpon.has(name) ? [outputsFile(name)] : []),
144
+ ...(archives.get(name) ?? []),
145
+ ];
146
+ if (artifactPaths.length > 0) jobProps.artifacts = { paths: artifactPaths };
129
147
  doc[jobName] = jobProps;
130
148
  }
131
149
  });
132
150
 
151
+ let promoteJob: string | undefined;
152
+ if (promoteTo !== undefined) {
153
+ promoteJob = `promote-${toJobName(promoteTo)}`;
154
+ if (promoteJob in doc) {
155
+ throw new Error(`the promote job "${promoteJob}" has the same name as a component job; rename the component`);
156
+ }
157
+ const command = options.promoteCommand ?? ["chant", "components", "promote", "--from", env, "--to", promoteTo];
158
+ doc[promoteJob] = {
159
+ stage: "promote",
160
+ image,
161
+ script: [...beforeScript, command.join(" "), ...extraScript],
162
+ needs: [...jobNameByComponent.values()].sort(),
163
+ };
164
+ }
165
+
133
166
  const sections: string[] = [];
134
167
  // `emitYAML` returns a `\n`-led block for a non-empty sequence and an inline
135
168
  // `[]` for an empty one, so the header needs a space in the second case.
@@ -137,12 +170,16 @@ export function generateGitlabPipeline(
137
170
  // when whitespace or the line's end follows it — which is how GitLab's own
138
171
  // reader takes it, and now how `parseYAML` does too (chant #2013).
139
172
  sections.push("workflow:" + emitYAML(doc.workflow, 1));
140
- sections.push(stages.length > 0 ? "stages:" + emitYAML(stages, 1) : "stages: []");
173
+ // The promote stage is YAML only: `stages` in the result stays one entry
174
+ // per graph wave.
175
+ const yamlStages = promoteJob ? [...stages, "promote"] : stages;
176
+ sections.push(yamlStages.length > 0 ? "stages:" + emitYAML(yamlStages, 1) : "stages: []");
141
177
  if (doc.variables) sections.push("variables:" + emitYAML(doc.variables, 1));
142
178
  for (const job of jobs) {
143
179
  const props = doc[job.jobName] as Record<string, unknown>;
144
180
  sections.push(`${job.jobName}:` + emitYAML(props, 1));
145
181
  }
182
+ if (promoteJob) sections.push(`${promoteJob}:` + emitYAML(doc[promoteJob], 1));
146
183
 
147
184
  return { yaml: sections.join("\n\n") + "\n", stages, jobs, env };
148
185
  }