@intentius/chant-lexicon-gitlab 0.124.43 → 0.124.45

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.
@@ -24,6 +24,11 @@
24
24
  * deciding job with the wave's own settings and a job per group (per share of
25
25
  * a group) with the group's: its tags, environment, variables over the
26
26
  * wave's, setup after the wave's, and `id_tokens:` from its `idTokens`.
27
+ *
28
+ * A job whose wave or group gives `cloud` (#4332) takes an `id_tokens:`
29
+ * entry per cloud, and its script starts by trading them for those
30
+ * identities and unsetting the token variables, before any line of the
31
+ * checkout runs.
27
32
  */
28
33
  import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
29
34
  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;;;;;;;;;;;;;;;;;;;;;;;;;;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
+ {"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAkBH,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AACjH,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAI9F,gHAAgH;AAChH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CA4FvH"}
@@ -24,9 +24,15 @@
24
24
  * deciding job with the wave's own settings and a job per group (per share of
25
25
  * a group) with the group's: its tags, environment, variables over the
26
26
  * wave's, setup after the wave's, and `id_tokens:` from its `idTokens`.
27
+ *
28
+ * A job whose wave or group gives `cloud` (#4332) takes an `id_tokens:`
29
+ * entry per cloud, and its script starts by trading them for those
30
+ * identities and unsetting the token variables, before any line of the
31
+ * checkout runs.
27
32
  */
28
33
  import { assertGitlabBranch, assertGitlabEnvironment, assertRunnerLabels, CI_IMAGE, environmentDoc, GITLAB_FULL_HISTORY, GITLAB_MERGE_REQUEST_SOURCE, GITLAB_SCHEDULE_SOURCE, gitlabIdTokensDoc, gitlabPushRule, gitlabSetupScript, gitlabTags, nonEmpty, pushBranches, } from "@intentius/chant/ci-jobs";
29
34
  import { opWaveGroupOf, opWaveJobs, opWavesResumeCommand } from "@intentius/chant/op/op-waves";
35
+ import { gitlabTradeIdTokens, gitlabTradeLines, TRADE_SPEC_ENV } from "@intentius/chant/ci-identity";
30
36
  import { emitGitlabFile } from "./ci-jobs.js";
31
37
  /** Render an Op waves spec as one GitLab CI file, `<name>.gitlab-ci.yml` unless `opsFileName` names another. */
32
38
  export function generateGitlabOpWavesPipeline(spec, options) {
@@ -82,7 +88,10 @@ export function generateGitlabOpWavesPipeline(spec, options) {
82
88
  const setup = [...setupOf[job.wave - 1], ...(group ? groupSetup.get(`${wave.name}/${group.name}`) : [])];
83
89
  const tags = gitlabTags(group?.runner ?? wave.runner ?? spec.runner);
84
90
  const environment = group?.environment ?? wave.environment;
85
- const idTokens = gitlabIdTokensDoc(group?.idTokens ?? wave.idTokens);
91
+ // #4332: the job's cloud identities, traded first.
92
+ const cloud = group?.cloud ?? wave.cloud;
93
+ const declared = gitlabIdTokensDoc(group?.idTokens ?? wave.idTokens);
94
+ const idTokens = cloud ? { ...declared, ...gitlabTradeIdTokens(cloud) } : declared;
86
95
  entries.push([
87
96
  job.jobName,
88
97
  {
@@ -92,10 +101,10 @@ export function generateGitlabOpWavesPipeline(spec, options) {
92
101
  resource_group: `${spec.name}-${wave.name}`,
93
102
  ...(environment ? { environment: environmentDoc(environment) } : {}),
94
103
  ...(idTokens ? { id_tokens: idTokens } : {}),
95
- variables: { ...GITLAB_FULL_HISTORY, ...(wave.variables ?? {}), ...(group?.variables ?? {}) },
104
+ variables: { ...GITLAB_FULL_HISTORY, ...(wave.variables ?? {}), ...(group?.variables ?? {}), ...(cloud ? { [TRADE_SPEC_ENV]: JSON.stringify(cloud) } : {}) },
96
105
  rules,
97
106
  ...(job.needs.length > 0 ? { needs: job.needs } : {}),
98
- script: [...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
107
+ script: [...(cloud ? gitlabTradeLines() : []), ...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
99
108
  ...(job.kind === "decide" ? { artifacts: { when: "always", paths: [job.record], expire_in: "30 days" } } : {}),
100
109
  },
101
110
  ]);
@@ -422,6 +422,10 @@ function prEnvGitlabJobs(image, beforeScript, extraScript, enter, loop, member,
422
422
  const group = `chant-pr-env-${member ? `${member.name}-` : ""}$CI_MERGE_REQUEST_IID`;
423
423
  const script = (j) => [...prLoopSetup(image), ...enter, ...beforeScript, ...(j.build ?? []), j.command, ...extraScript];
424
424
  const artifacts = { when: "always", paths: [reportPath] };
425
+ // #4330: env-up, env-down and env-sweep trade the job's OIDC token for the pull request environments' identity first.
426
+ const identity = loop.prEnvIdentity;
427
+ const trading = identity ? { id_tokens: gitlabTradeIdTokens(identity) } : {};
428
+ const tradeVars = identity ? { [TRADE_SPEC_ENV]: JSON.stringify(identity) } : {};
425
429
  return {
426
430
  jobs: {
427
431
  [named("env-up")]: {
@@ -430,9 +434,11 @@ function prEnvGitlabJobs(image, beforeScript, extraScript, enter, loop, member,
430
434
  rules: [{ if: onMergeRequest }],
431
435
  resource_group: group,
432
436
  environment: { name: environment, on_stop: named("env-down") },
433
- variables: up.env,
437
+ ...trading,
438
+ variables: { ...up.env, ...tradeVars },
434
439
  // #4295: the merge request's code runs in this job, so the job's shell drops the forge tokens first.
435
- script: [GITLAB_DROP_FORGE_TOKENS, ...script(up)],
440
+ // #4330: with an identity, the trade runs before that, and the same unset drops the token variables too.
441
+ script: [...(identity ? gitlabTradeLines(FORGE_TOKEN_VARS) : [GITLAB_DROP_FORGE_TOKENS]), ...script(up)],
436
442
  artifacts,
437
443
  },
438
444
  [named(PR_ENV_POST_JOB)]: {
@@ -451,8 +457,9 @@ function prEnvGitlabJobs(image, beforeScript, extraScript, enter, loop, member,
451
457
  allow_failure: true,
452
458
  resource_group: group,
453
459
  environment: { name: environment, action: "stop" },
454
- variables: down.env,
455
- script: script(down),
460
+ ...trading,
461
+ variables: { ...down.env, ...tradeVars },
462
+ script: [...(identity ? gitlabTradeLines() : []), ...script(down)],
456
463
  artifacts,
457
464
  },
458
465
  [named("env-sweep")]: {
@@ -460,8 +467,9 @@ function prEnvGitlabJobs(image, beforeScript, extraScript, enter, loop, member,
460
467
  image,
461
468
  rules: [{ if: GITLAB_SCHEDULE_RULE }],
462
469
  resource_group: `chant-pr-env-sweep${member ? `-${member.name}` : ""}`,
463
- variables: sweep.env,
464
- script: script(sweep),
470
+ ...trading,
471
+ variables: { ...sweep.env, ...tradeVars },
472
+ script: [...(identity ? gitlabTradeLines() : []), ...script(sweep)],
465
473
  artifacts,
466
474
  },
467
475
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "94ae30275c785d868f487e6ea2f8cd1d0a1236acfd4160df5578024e351e4336",
4
+ "manifest.json": "88db3565e73e6e08093cc50a58cd3ff61cac32c75781ca00817628cf7d27daa7",
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": "f66b0452e1ea5496564529fc156a4cdf9123a85cf40b9dc73c6c6a22ce377d34"
58
+ "composite": "6aa6f17b680f94ee132550ccaf9ba993840b3eef23113c79ec226031546800f0"
59
59
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.124.43",
3
+ "version": "0.124.45",
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.43",
3
+ "version": "0.124.45",
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.43",
71
- "@intentius/chant-lexicon-github": "^0.124.43"
70
+ "@intentius/chant": "^0.124.45",
71
+ "@intentius/chant-lexicon-github": "^0.124.45"
72
72
  },
73
73
  "peerDependenciesMeta": {
74
74
  "@intentius/chant-lexicon-github": {
@@ -109,3 +109,24 @@ describe("a runner per wave and job groups per wave (#3914)", () => {
109
109
  expect(() => generateGitlabOpWavesPipeline({ ...spec, runner: { group: "apply" } }, { specFile: "waves.json" })).toThrow(/Op waves "migrations": runner is the runner group/);
110
110
  });
111
111
  });
112
+
113
+ describe("an Op wave's jobs by cloud identity (#4332)", () => {
114
+ test("each job takes an id_tokens entry per cloud and starts its script with the trade, the deciding job for the plan identities", async () => {
115
+ const { parseYAML } = await import("@intentius/chant/yaml");
116
+ const { cloudIdentityWave, gitlabTradeLines, TRADE_SPEC_ENV, tradeIdentities } = await import("@intentius/chant/ci-identity");
117
+ const decl = { aws: { rules: [{ targets: ["envs/prod/**"], plan: "prod-plan", apply: "prod-apply" }, { plan: "own-plan", apply: "own-apply" }] } };
118
+ const roots = ["envs/prod/net", "shared/dns"];
119
+ const grouped: OpWavesSpec = { ...spec, waves: [{ name: "prod", runs: roots.map((target) => ({ target })), idTokens: { OTHER_TOKEN: "other.example" }, ...cloudIdentityWave(decl, "prod", "infra", roots) }] };
120
+ const doc = parseYAML(generateGitlabOpWavesPipeline(grouped, { specFile: "waves.json", beforeScript: ["npm ci"] }).files[0]!.yaml) as Record<string, { id_tokens?: Record<string, { aud: string }>; variables: Record<string, string>; script: string[] }>;
121
+ const want: Record<string, string[]> = { "wave-1-prod-decide": ["own-plan", "prod-plan"], "wave-1-prod-envs-prod": ["prod-apply"], "wave-1-prod-default": ["own-apply"] };
122
+ for (const [name, ids] of Object.entries(want)) {
123
+ const job = doc[name]!;
124
+ expect(job.id_tokens).toEqual({ OTHER_TOKEN: { aud: "other.example" }, CHANT_ID_TOKEN_AWS: { aud: "sts.amazonaws.com" } });
125
+ expect(job.script.slice(0, 3)).toEqual(gitlabTradeLines());
126
+ expect(job.script[3]).toBe("npm ci");
127
+ expect(tradeIdentities(JSON.parse(job.variables[TRADE_SPEC_ENV]!))).toEqual(ids);
128
+ }
129
+ expect(JSON.stringify(doc["wave-1-prod-decide"])).not.toContain("apply\"");
130
+ expect(JSON.stringify(doc["wave-1-prod-default"])).not.toContain("prod-apply");
131
+ });
132
+ });
@@ -24,6 +24,11 @@
24
24
  * deciding job with the wave's own settings and a job per group (per share of
25
25
  * a group) with the group's: its tags, environment, variables over the
26
26
  * wave's, setup after the wave's, and `id_tokens:` from its `idTokens`.
27
+ *
28
+ * A job whose wave or group gives `cloud` (#4332) takes an `id_tokens:`
29
+ * entry per cloud, and its script starts by trading them for those
30
+ * identities and unsetting the token variables, before any line of the
31
+ * checkout runs.
27
32
  */
28
33
 
29
34
  import {
@@ -44,6 +49,7 @@ import {
44
49
  } from "@intentius/chant/ci-jobs";
45
50
  import { opWaveGroupOf, opWaveJobs, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
46
51
  import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
52
+ import { gitlabTradeIdTokens, gitlabTradeLines, TRADE_SPEC_ENV } from "@intentius/chant/ci-identity";
47
53
  import { emitGitlabFile } from "./ci-jobs";
48
54
 
49
55
  /** Render an Op waves spec as one GitLab CI file, `<name>.gitlab-ci.yml` unless `opsFileName` names another. */
@@ -96,7 +102,10 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
96
102
  const setup = [...setupOf[job.wave - 1]!, ...(group ? groupSetup.get(`${wave.name}/${group.name}`)! : [])];
97
103
  const tags = gitlabTags(group?.runner ?? wave.runner ?? spec.runner);
98
104
  const environment = group?.environment ?? wave.environment;
99
- const idTokens = gitlabIdTokensDoc(group?.idTokens ?? wave.idTokens);
105
+ // #4332: the job's cloud identities, traded first.
106
+ const cloud = group?.cloud ?? wave.cloud;
107
+ const declared = gitlabIdTokensDoc(group?.idTokens ?? wave.idTokens);
108
+ const idTokens = cloud ? { ...declared, ...gitlabTradeIdTokens(cloud) } : declared;
100
109
  entries.push([
101
110
  job.jobName,
102
111
  {
@@ -106,10 +115,10 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
106
115
  resource_group: `${spec.name}-${wave.name}`,
107
116
  ...(environment ? { environment: environmentDoc(environment) } : {}),
108
117
  ...(idTokens ? { id_tokens: idTokens } : {}),
109
- variables: { ...GITLAB_FULL_HISTORY, ...(wave.variables ?? {}), ...(group?.variables ?? {}) },
118
+ variables: { ...GITLAB_FULL_HISTORY, ...(wave.variables ?? {}), ...(group?.variables ?? {}), ...(cloud ? { [TRADE_SPEC_ENV]: JSON.stringify(cloud) } : {}) },
110
119
  rules,
111
120
  ...(job.needs.length > 0 ? { needs: job.needs } : {}),
112
- script: [...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
121
+ script: [...(cloud ? gitlabTradeLines() : []), ...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
113
122
  ...(job.kind === "decide" ? { artifacts: { when: "always", paths: [job.record!], expire_in: "30 days" } } : {}),
114
123
  },
115
124
  ]);
@@ -496,6 +496,10 @@ function prEnvGitlabJobs(
496
496
  const group = `chant-pr-env-${member ? `${member.name}-` : ""}$CI_MERGE_REQUEST_IID`;
497
497
  const script = (j: typeof up): string[] => [...prLoopSetup(image), ...enter, ...beforeScript, ...(j.build ?? []), j.command, ...extraScript];
498
498
  const artifacts = { when: "always", paths: [reportPath] };
499
+ // #4330: env-up, env-down and env-sweep trade the job's OIDC token for the pull request environments' identity first.
500
+ const identity = loop.prEnvIdentity;
501
+ const trading = identity ? { id_tokens: gitlabTradeIdTokens(identity) } : {};
502
+ const tradeVars: Record<string, string> = identity ? { [TRADE_SPEC_ENV]: JSON.stringify(identity) } : {};
499
503
  return {
500
504
  jobs: {
501
505
  [named("env-up")]: {
@@ -504,9 +508,11 @@ function prEnvGitlabJobs(
504
508
  rules: [{ if: onMergeRequest }],
505
509
  resource_group: group,
506
510
  environment: { name: environment, on_stop: named("env-down") },
507
- variables: up.env,
511
+ ...trading,
512
+ variables: { ...up.env, ...tradeVars },
508
513
  // #4295: the merge request's code runs in this job, so the job's shell drops the forge tokens first.
509
- script: [GITLAB_DROP_FORGE_TOKENS, ...script(up)],
514
+ // #4330: with an identity, the trade runs before that, and the same unset drops the token variables too.
515
+ script: [...(identity ? gitlabTradeLines(FORGE_TOKEN_VARS) : [GITLAB_DROP_FORGE_TOKENS]), ...script(up)],
510
516
  artifacts,
511
517
  },
512
518
  [named(PR_ENV_POST_JOB)]: {
@@ -525,8 +531,9 @@ function prEnvGitlabJobs(
525
531
  allow_failure: true,
526
532
  resource_group: group,
527
533
  environment: { name: environment, action: "stop" },
528
- variables: down.env,
529
- script: script(down),
534
+ ...trading,
535
+ variables: { ...down.env, ...tradeVars },
536
+ script: [...(identity ? gitlabTradeLines() : []), ...script(down)],
530
537
  artifacts,
531
538
  },
532
539
  [named("env-sweep")]: {
@@ -534,8 +541,9 @@ function prEnvGitlabJobs(
534
541
  image,
535
542
  rules: [{ if: GITLAB_SCHEDULE_RULE }],
536
543
  resource_group: `chant-pr-env-sweep${member ? `-${member.name}` : ""}`,
537
- variables: sweep.env,
538
- script: script(sweep),
544
+ ...trading,
545
+ variables: { ...sweep.env, ...tradeVars },
546
+ script: [...(identity ? gitlabTradeLines() : []), ...script(sweep)],
539
547
  artifacts,
540
548
  },
541
549
  },
@@ -89,3 +89,23 @@ describe("the GitLab env-up job holds no forge token while the merge request's c
89
89
  expect(post.script.join("\n")).not.toMatch(/chant (build|lint)|pr-env up/);
90
90
  });
91
91
  });
92
+
93
+ describe("the environment's cloud identity (#4330)", () => {
94
+ const prEnvIdentity = { aws: { role: "arn:aws:iam::000000000000:role/pr-env-apply", region: "us-east-1", audience: "sts.amazonaws.com", endpoint: "http://localhost:4566" } };
95
+ const doc = parseYAML(generateGitlabPipeline(ESTATE, { env: "staging", image: "node:22", prLoop: { prEnvironment: true, prEnvIdentity } }).yaml) as Record<string, Job & { id_tokens?: Record<string, unknown> }>;
96
+
97
+ test("env-up trades first, and the same line drops the forge tokens before the merge request's code runs", () => {
98
+ const up = doc["env-up"]!;
99
+ expect(up.id_tokens).toBeDefined();
100
+ expect(up.variables.CHANT_CLOUD_TRADE).toBe(JSON.stringify(prEnvIdentity));
101
+ expect(up.script).not.toContain(GITLAB_DROP_FORGE_TOKENS);
102
+ const unset = up.script.findIndex((l) => l.startsWith("unset ") && l.includes("CHANT_FORGE_TOKEN"));
103
+ expect(unset).toBeGreaterThan(-1);
104
+ expect(unset).toBeLessThan(up.script.findIndex((l) => l.startsWith("chant components pr-env up")));
105
+ });
106
+
107
+ test("env-down and env-sweep trade for it too, and env-post does not", () => {
108
+ for (const name of ["env-down", "env-sweep"]) expect(doc[name]!.variables.CHANT_CLOUD_TRADE).toBe(JSON.stringify(prEnvIdentity));
109
+ expect(doc["env-post"]!.variables.CHANT_CLOUD_TRADE).toBeUndefined();
110
+ });
111
+ });