@intentius/chant-lexicon-gitlab 0.124.22 → 0.124.24

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.
@@ -18,6 +18,12 @@
18
18
  *
19
19
  * A `pr-review` wave (#3684) adds `<name>-record-plans`, run in merge request
20
20
  * pipelines only, which records the digests the merge request's head plans.
21
+ *
22
+ * `runner` (on the spec, a wave or a job group, #3914) is the job's `tags:`; a
23
+ * GitHub runner group is a spec error here. A wave's `jobs` renders its
24
+ * deciding job with the wave's own settings and a job per group (per share of
25
+ * a group) with the group's: its tags, environment, variables over the
26
+ * wave's, setup after the wave's, and `id_tokens:` from its `idTokens`.
21
27
  */
22
28
  import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
23
29
  import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAeH,OAAO,EAAoC,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAClG,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,gHAAgH;AAChH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAqEvH"}
1
+ {"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AACjH,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,gHAAgH;AAChH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAyFvH"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "3b1663372f319b0cf8b562a7df67830af85630e394b55a82a049d34e74db578b",
4
+ "manifest.json": "a373d4af5482711a1b501907a60d10deb1e50ea9dcbf70941eb4b1e54126ace0",
5
5
  "meta.json": "db082fbf90c5941c498b2bee0f106103449c1a4c257716ca2220f0dd490450b2",
6
6
  "types/index.d.ts": "a048a66ebe20da0cb5fc889054cac69044d9bf882e2c56b5c5bf113d1f3ba57c",
7
7
  "rules/artifact-no-expiry.ts": "0ca5bb972d4a542126e8a0229d43b55658394cb7dd9b9923b2c590bec1c2ad7d",
@@ -55,5 +55,5 @@
55
55
  "skills/chant-gitlab-migrate.md": "4853d04980560b379e5b0f7267dcb0a1e5d5879cb9e9931681ce76b2a5fe69d8",
56
56
  "skills/chant-gitlab-patterns.md": "6d9a44e9e8de4c3820be9d65381b0d2ede4cb3626aaaf0011e21838a9fbf556e"
57
57
  },
58
- "composite": "e1ab249bf95d943da409aae407c3eff3dc89319a438a222e2159a285209e05a1"
58
+ "composite": "3342dfc812c8e79be300ba1c27098440d0f91be83a87ec2f36e6d355caafe1d8"
59
59
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.124.22",
3
+ "version": "0.124.24",
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.124.22",
3
+ "version": "0.124.24",
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.124.22",
71
- "@intentius/chant-lexicon-github": "^0.124.22"
70
+ "@intentius/chant": "^0.124.24",
71
+ "@intentius/chant-lexicon-github": "^0.124.24"
72
72
  },
73
73
  "peerDependenciesMeta": {
74
74
  "@intentius/chant-lexicon-github": {
@@ -0,0 +1,86 @@
1
+ # chant Op waves "migrations": one stage per wave, each needing the one before.
2
+ # A wave that waits at its gate exits 3 and stops the waves after it; approve the
3
+ # digest it prints, then retry the job.
4
+
5
+ stages:
6
+ - wave-1-dev
7
+ - wave-2-prod
8
+
9
+ wave-1-dev:
10
+ stage: wave-1-dev
11
+ image: node:22
12
+ tags:
13
+ - docker
14
+ - dev
15
+ resource_group: migrations-dev
16
+ variables:
17
+ GIT_DEPTH: '0'
18
+ rules:
19
+ - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
20
+ script:
21
+ - chant run wave --spec waves.json --wave 1
22
+
23
+ wave-2-prod-decide:
24
+ stage: wave-2-prod
25
+ image: node:22
26
+ tags:
27
+ - plan
28
+ resource_group: migrations-prod
29
+ id_tokens:
30
+ PLAN_TOKEN:
31
+ aud: plan.example
32
+ variables:
33
+ GIT_DEPTH: '0'
34
+ rules:
35
+ - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
36
+ needs:
37
+ - wave-1-dev
38
+ script:
39
+ - chant run wave --spec waves.json --wave 2 --decide
40
+ artifacts:
41
+ when: always
42
+ paths:
43
+ - .chant/op-waves/migrations/wave-2.json
44
+ expire_in: '30 days'
45
+
46
+ wave-2-prod-us:
47
+ stage: wave-2-prod
48
+ image: node:22
49
+ tags:
50
+ - us-apply
51
+ resource_group: migrations-prod
52
+ environment:
53
+ name: prod-us
54
+ id_tokens:
55
+ CLOUD_TOKEN:
56
+ aud: us.example
57
+ variables:
58
+ GIT_DEPTH: '0'
59
+ REGION: us
60
+ rules:
61
+ - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
62
+ needs:
63
+ - wave-2-prod-decide
64
+ script:
65
+ - echo us
66
+ - chant run wave --spec waves.json --wave 2 --group us
67
+
68
+ wave-2-prod-eu:
69
+ stage: wave-2-prod
70
+ image: node:22
71
+ tags:
72
+ - eu-apply
73
+ resource_group: migrations-prod
74
+ environment:
75
+ name: prod-eu
76
+ id_tokens:
77
+ CLOUD_TOKEN:
78
+ aud: eu.example
79
+ variables:
80
+ GIT_DEPTH: '0'
81
+ rules:
82
+ - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
83
+ needs:
84
+ - wave-2-prod-decide
85
+ script:
86
+ - chant run wave --spec waves.json --wave 2 --group eu
@@ -69,3 +69,43 @@ describe("a wave's environment is checked as an Op's is (#3765)", () => {
69
69
  expect(generateGitlabOpWavesPipeline(one({ name: "dev", url: "$CI_ENVIRONMENT_URL" }), { specFile: "waves.json" }).files[0]!.yaml).toContain("url: '$CI_ENVIRONMENT_URL'");
70
70
  });
71
71
  });
72
+
73
+ describe("a runner per wave and job groups per wave (#3914)", () => {
74
+ const grouped: OpWavesSpec = {
75
+ ...spec,
76
+ runner: "plan",
77
+ waves: [
78
+ { name: "dev", runs: [{ target: "dev" }], gate: "never", runner: ["docker", "dev"] },
79
+ {
80
+ name: "prod",
81
+ runs: [{ target: "us/a" }, { target: "eu/a" }],
82
+ idTokens: { PLAN_TOKEN: "plan.example" },
83
+ jobs: [
84
+ { name: "us", runs: ["us/*"], runner: "us-apply", environment: { name: "prod-us" }, variables: { REGION: "us" }, setup: [{ run: "echo us" }], idTokens: { CLOUD_TOKEN: "us.example" } },
85
+ { name: "eu", runs: ["eu/*"], runner: "eu-apply", environment: { name: "prod-eu" }, idTokens: { CLOUD_TOKEN: "eu.example" } },
86
+ ],
87
+ },
88
+ ],
89
+ };
90
+
91
+ test("the rendered pipeline matches its golden", async () => {
92
+ await expect(generateGitlabOpWavesPipeline(grouped, { specFile: "waves.json" }).files[0]!.yaml).toMatchFileSnapshot("./__golden__/op-waves-jobs.yml");
93
+ });
94
+
95
+ test("tags, environment, id_tokens, variables and the group's command on each job", () => {
96
+ const yaml = generateGitlabOpWavesPipeline(grouped, { specFile: "waves.json" }).files[0]!.yaml;
97
+ expect(yaml).toMatch(/wave-1-dev:\n\s+stage: wave-1-dev\n\s+image: node:22\n\s+tags:\n\s+- docker\n\s+- dev\n/);
98
+ expect(yaml).toMatch(/wave-2-prod-decide:\n\s+stage: wave-2-prod\n\s+image: node:22\n\s+tags:\n\s+- plan\n\s+resource_group: migrations-prod\n\s+id_tokens:\n\s+PLAN_TOKEN:\n\s+aud: plan.example\n/);
99
+ expect(yaml).toMatch(
100
+ /wave-2-prod-us:\n\s+stage: wave-2-prod\n\s+image: node:22\n\s+tags:\n\s+- us-apply\n\s+resource_group: migrations-prod\n\s+environment:\n\s+name: prod-us\n\s+id_tokens:\n\s+CLOUD_TOKEN:\n\s+aud: us.example\n\s+variables:\n\s+GIT_DEPTH: '0'\n\s+REGION: us\n[\s\S]*?needs:\n\s+- wave-2-prod-decide\n\s+script:\n\s+- echo us\n\s+- chant run wave --spec waves.json --wave 2 --group us\n/,
101
+ );
102
+ expect(yaml).toMatch(/wave-2-prod-eu:[\s\S]*?id_tokens:\n\s+CLOUD_TOKEN:\n\s+aud: eu.example\n/);
103
+ expect(yaml).not.toContain("_AUDIENCE");
104
+ });
105
+
106
+ test("a runner group is a spec error naming the wave and the field", () => {
107
+ const withGroup = (runner: { group: string }) => ({ ...grouped, waves: [grouped.waves[0]!, { ...grouped.waves[1]!, jobs: [{ ...grouped.waves[1]!.jobs![0]!, runner }, grouped.waves[1]!.jobs![1]!] }] });
108
+ expect(() => generateGitlabOpWavesPipeline(withGroup({ group: "apply" }), { specFile: "waves.json" })).toThrow(/wave "prod" job group "us" runner is the runner group "apply", which only GitHub has/);
109
+ expect(() => generateGitlabOpWavesPipeline({ ...spec, runner: { group: "apply" } }, { specFile: "waves.json" })).toThrow(/Op waves "migrations": runner is the runner group/);
110
+ });
111
+ });
@@ -18,22 +18,31 @@
18
18
  *
19
19
  * A `pr-review` wave (#3684) adds `<name>-record-plans`, run in merge request
20
20
  * pipelines only, which records the digests the merge request's head plans.
21
+ *
22
+ * `runner` (on the spec, a wave or a job group, #3914) is the job's `tags:`; a
23
+ * GitHub runner group is a spec error here. A wave's `jobs` renders its
24
+ * deciding job with the wave's own settings and a job per group (per share of
25
+ * a group) with the group's: its tags, environment, variables over the
26
+ * wave's, setup after the wave's, and `id_tokens:` from its `idTokens`.
21
27
  */
22
28
 
23
29
  import {
24
30
  assertGitlabBranch,
25
31
  assertGitlabEnvironment,
32
+ assertRunnerLabels,
26
33
  CI_IMAGE,
27
34
  environmentDoc,
28
35
  GITLAB_FULL_HISTORY,
29
36
  GITLAB_MERGE_REQUEST_SOURCE,
30
37
  GITLAB_SCHEDULE_SOURCE,
38
+ gitlabIdTokensDoc,
31
39
  gitlabPushRule,
32
40
  gitlabSetupScript,
41
+ gitlabTags,
33
42
  nonEmpty,
34
43
  pushBranches,
35
44
  } from "@intentius/chant/ci-jobs";
36
- import { opWaveJobs, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
45
+ import { opWaveGroupOf, opWaveJobs, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
37
46
  import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
38
47
  import { emitGitlabFile } from "./ci-jobs";
39
48
 
@@ -49,6 +58,18 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
49
58
  // The setup and environment each wave's jobs carry, checked as an Op's are.
50
59
  const setupOf = spec.waves.map((wave) => gitlabSetupScript(`${subject} wave "${wave.name}"`, wave.setup));
51
60
  for (const wave of spec.waves) if (wave.environment) assertGitlabEnvironment(`${spec.name}/${wave.name}`, wave.environment);
61
+ // #3914: GitLab picks a runner by tags, and each job group carries its own setup and environment.
62
+ assertRunnerLabels(subject, "runner", spec.runner, "GitLab");
63
+ const groupSetup = new Map<string, string[]>();
64
+ for (const wave of spec.waves) {
65
+ assertRunnerLabels(subject, `wave "${wave.name}" runner`, wave.runner, "GitLab");
66
+ for (const group of wave.jobs ?? []) {
67
+ assertRunnerLabels(subject, `wave "${wave.name}" job group "${group.name}" runner`, group.runner, "GitLab");
68
+ if (group.environment) assertGitlabEnvironment(`${spec.name}/${wave.name}/${group.name}`, group.environment);
69
+ groupSetup.set(`${wave.name}/${group.name}`, gitlabSetupScript(`${subject} wave "${wave.name}" job group "${group.name}"`, group.setup));
70
+ }
71
+ }
72
+ const specTags = gitlabTags(spec.runner);
52
73
 
53
74
  const byReview = spec.waves.some((w) => w.approval === "pr-review");
54
75
  const stages = [...(byReview ? ["plans"] : []), ...spec.waves.map((wave, i) => `wave-${i + 1}-${wave.name}`)];
@@ -62,6 +83,7 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
62
83
  {
63
84
  stage: "plans",
64
85
  image,
86
+ ...(specTags ? { tags: specTags } : {}),
65
87
  variables: { ...GITLAB_FULL_HISTORY },
66
88
  rules: [{ if: GITLAB_MERGE_REQUEST_SOURCE }],
67
89
  script: [...(options.beforeScript ?? []), ["chant", "run", "wave", "--spec", options.specFile, "--record-plans"].join(" ")],
@@ -70,15 +92,21 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
70
92
  }
71
93
  for (const job of jobs) {
72
94
  const wave = spec.waves[job.wave - 1]!;
73
- const setup = setupOf[job.wave - 1]!;
95
+ const group = opWaveGroupOf(spec, job);
96
+ const setup = [...setupOf[job.wave - 1]!, ...(group ? groupSetup.get(`${wave.name}/${group.name}`)! : [])];
97
+ const tags = gitlabTags(group?.runner ?? wave.runner ?? spec.runner);
98
+ const environment = group?.environment ?? wave.environment;
99
+ const idTokens = gitlabIdTokensDoc(group?.idTokens ?? wave.idTokens);
74
100
  entries.push([
75
101
  job.jobName,
76
102
  {
77
103
  stage: stages[job.wave - 1 + (byReview ? 1 : 0)],
78
104
  image,
105
+ ...(tags ? { tags } : {}),
79
106
  resource_group: `${spec.name}-${wave.name}`,
80
- ...(wave.environment ? { environment: environmentDoc(wave.environment) } : {}),
81
- variables: { ...GITLAB_FULL_HISTORY, ...(wave.variables ?? {}) },
107
+ ...(environment ? { environment: environmentDoc(environment) } : {}),
108
+ ...(idTokens ? { id_tokens: idTokens } : {}),
109
+ variables: { ...GITLAB_FULL_HISTORY, ...(wave.variables ?? {}), ...(group?.variables ?? {}) },
82
110
  rules,
83
111
  ...(job.needs.length > 0 ? { needs: job.needs } : {}),
84
112
  script: [...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
@@ -92,6 +120,7 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
92
120
  {
93
121
  stage: "resume",
94
122
  image,
123
+ ...(specTags ? { tags: specTags } : {}),
95
124
  rules: [{ if: GITLAB_SCHEDULE_SOURCE }],
96
125
  script: [...(options.beforeScript ?? []), opWavesResumeCommand(spec).join(" ")],
97
126
  },