@intentius/chant-lexicon-gitlab 0.58.0 → 0.59.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.
@@ -22,10 +22,36 @@
22
22
  * inlined audit/reconcile logic. The finding-mode itself is already baked
23
23
  * into the Op's own activity args at build time by the composite that
24
24
  * created it; GitLab has no per-job `permissions:` concept (unlike GitHub
25
- * Actions), so a non-`report` mode's write access comes from whatever
25
+ * Actions), and `findingMode: "comment"` is refused by name here because it
26
+ * posts onto a GitHub pull-request event GitLab does not have (#2231), so a
27
+ * non-`report` mode's write access comes from whatever
26
28
  * `GITLAB_TOKEN`/CI-CD-variable configuration the project already has —
27
29
  * this generator documents the requirement rather than fabricating a
28
30
  * variable nothing reads.
31
+ *
32
+ * Neither of #2242's two per-Op options survives the crossing, and both are
33
+ * refused by name rather than dropped. A `setup` entry spelled `{ uses }` is
34
+ * a GitHub Actions marketplace action; GitLab CI has `script` and nothing
35
+ * else, so there is no shape to translate it into and no way to approximate
36
+ * `aws-actions/configure-aws-credentials` in a shell line. A `{ run }` entry
37
+ * translates exactly, and is emitted ahead of the `beforeScript` lines, the
38
+ * same position the github generator gives it. An additive `permissions` map
39
+ * is refused for the same reason `permissions:` is absent here at all: GitLab
40
+ * has no per-job token-scope mapping, and its OIDC surface is a different
41
+ * declaration (`id_tokens:` with an `aud`, exchanged for cloud credentials by
42
+ * the job itself) that chant does not generate. Ignoring the map would emit a
43
+ * job that reads as having OIDC and runs with no credentials.
44
+ *
45
+ * The gated-apply mapping (#2243) has nothing to attach to here. It exists
46
+ * because a push-to-main apply that stops at its gate exits 3 and paints the
47
+ * branch red on every merge; this generator has no push pipeline at all,
48
+ * refusing a `push` trigger by name (#2084) because a GitLab schedule is a
49
+ * project-level cron object rather than an event. GitLab does have its own
50
+ * equivalent of the mapping should one ever be wanted — `allow_failure:
51
+ * { exit_codes: [3] }` turns one exit code into a warning rather than a
52
+ * failure, without a flag on the invocation — so the day this generator
53
+ * grows a push trigger, that is the shape to reach for rather than
54
+ * `--gated-exit`.
29
55
  */
30
56
  import type { ComponentPipelineOptions as GenerateGitlabOpOptions, OpPipelineResult as GenerateGitlabOpResult, ScheduledOpSpec } from "@intentius/chant/lexicon";
31
57
  export type { GenerateGitlabOpOptions, GenerateGitlabOpResult };
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAGnD,gBAAgB,IAAI,sBAAsB,EAC1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAmBhE;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAoDxB"}
1
+ {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAIH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAGnD,gBAAgB,IAAI,sBAAsB,EAC1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAiClC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAmBhE;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAmExB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "35158c32b55ed8f86ec453b7575d06fd8005641a5edb2ad972f0f4cf06fccd7e",
4
+ "manifest.json": "05eca8e8e24c1045cd8d1b9903dfa59661449d40b52b9d2f880eb2a5a00c67d2",
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": "59706e7a6edf95b2ecec5fe2220d0899b0282322ef921583a940a03fb623e7c0"
58
+ "composite": "5ba4e358f25944a07f0966355b274aebb3c199d11fc997aa7713830c7ff5320e"
59
59
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.58.0",
3
+ "version": "0.59.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.58.0",
3
+ "version": "0.59.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.58.0",
71
- "@intentius/chant-lexicon-github": "^0.58.0",
70
+ "@intentius/chant": "^0.59.0",
71
+ "@intentius/chant-lexicon-github": "^0.59.0",
72
72
  "typescript": "^5.9.3"
73
73
  },
74
74
  "peerDependenciesMeta": {
@@ -82,6 +82,24 @@ describe("generateGitlabOpPipeline: no pull_request/push event model (#2084)", (
82
82
  });
83
83
  });
84
84
 
85
+ describe("generateGitlabOpPipeline: no comment finding mode (#2231)", () => {
86
+ test("findingMode comment is refused by name, even on a cron trigger GitLab does support", () => {
87
+ const specs: ScheduledOpSpec[] = [
88
+ { name: "app-plan", schedule: "0 6 * * *", findingMode: "comment" },
89
+ ];
90
+ expect(() => generateGitlabOpPipeline(specs)).toThrow(
91
+ /Scheduled Op "app-plan".*findingMode "comment".*GitLab has no pull_request event/s,
92
+ );
93
+ });
94
+
95
+ test("the refusal names the modes GitLab does have", () => {
96
+ const specs: ScheduledOpSpec[] = [{ name: "app-plan", schedule: "0 6 * * *", findingMode: "comment" }];
97
+ expect(() => generateGitlabOpPipeline(specs)).toThrow(
98
+ /findingMode "issue" or "merge-request"/,
99
+ );
100
+ });
101
+ });
102
+
85
103
  describe("generateGitlabOpPipeline: a cross-cutting change is one generator edit, not per-job", () => {
86
104
  test("runCommand/beforeScript/extraScript apply uniformly across every job", () => {
87
105
  const specs: ScheduledOpSpec[] = [
@@ -104,3 +122,50 @@ describe("generateGitlabOpPipeline: a cross-cutting change is one generator edit
104
122
  }
105
123
  });
106
124
  });
125
+
126
+ /**
127
+ * #2242's two per-Op options against a provider that has neither concept.
128
+ * Both degrade by name at build time rather than being dropped into a job
129
+ * that would run without the thing the option asked for.
130
+ */
131
+ describe("generateGitlabOpPipeline: setup steps and additive permissions (#2242)", () => {
132
+ test("refuses a `uses` setup step by name, and names the action", () => {
133
+ expect(() =>
134
+ generateGitlabOpPipeline([
135
+ {
136
+ name: "actions-audit",
137
+ schedule: "0 6 * * *",
138
+ setup: [{ uses: "aws-actions/configure-aws-credentials@v6" }],
139
+ },
140
+ ]),
141
+ ).toThrow(/setup step 1 is `uses: "aws-actions\/configure-aws-credentials@v6"`.*GitLab CI jobs run/s);
142
+ });
143
+
144
+ test("emits a `run` setup entry ahead of the beforeScript lines", () => {
145
+ const result = generateGitlabOpPipeline(
146
+ [
147
+ {
148
+ name: "actions-audit",
149
+ schedule: "0 6 * * *",
150
+ setup: [{ run: "aws sts get-caller-identity" }],
151
+ },
152
+ ],
153
+ { beforeScript: ["npm ci"], extraScript: ["echo done"] },
154
+ );
155
+ const job = parseYAML(result.files[0].yaml)["actions-audit"] as { script: string[] };
156
+ expect(job.script).toEqual([
157
+ "aws sts get-caller-identity",
158
+ "npm ci",
159
+ "chant run actions-audit",
160
+ "echo done",
161
+ ]);
162
+ });
163
+
164
+ test("refuses additive permissions by name, and points at GitLab's own OIDC surface", () => {
165
+ expect(() =>
166
+ generateGitlabOpPipeline([
167
+ { name: "actions-audit", schedule: "0 6 * * *", permissions: { "id-token": "write" } },
168
+ ]),
169
+ ).toThrow(/adds permissions \{ id-token: write \}.*id_tokens:/s);
170
+ });
171
+ });
@@ -22,10 +22,36 @@
22
22
  * inlined audit/reconcile logic. The finding-mode itself is already baked
23
23
  * into the Op's own activity args at build time by the composite that
24
24
  * created it; GitLab has no per-job `permissions:` concept (unlike GitHub
25
- * Actions), so a non-`report` mode's write access comes from whatever
25
+ * Actions), and `findingMode: "comment"` is refused by name here because it
26
+ * posts onto a GitHub pull-request event GitLab does not have (#2231), so a
27
+ * non-`report` mode's write access comes from whatever
26
28
  * `GITLAB_TOKEN`/CI-CD-variable configuration the project already has —
27
29
  * this generator documents the requirement rather than fabricating a
28
30
  * variable nothing reads.
31
+ *
32
+ * Neither of #2242's two per-Op options survives the crossing, and both are
33
+ * refused by name rather than dropped. A `setup` entry spelled `{ uses }` is
34
+ * a GitHub Actions marketplace action; GitLab CI has `script` and nothing
35
+ * else, so there is no shape to translate it into and no way to approximate
36
+ * `aws-actions/configure-aws-credentials` in a shell line. A `{ run }` entry
37
+ * translates exactly, and is emitted ahead of the `beforeScript` lines, the
38
+ * same position the github generator gives it. An additive `permissions` map
39
+ * is refused for the same reason `permissions:` is absent here at all: GitLab
40
+ * has no per-job token-scope mapping, and its OIDC surface is a different
41
+ * declaration (`id_tokens:` with an `aud`, exchanged for cloud credentials by
42
+ * the job itself) that chant does not generate. Ignoring the map would emit a
43
+ * job that reads as having OIDC and runs with no credentials.
44
+ *
45
+ * The gated-apply mapping (#2243) has nothing to attach to here. It exists
46
+ * because a push-to-main apply that stops at its gate exits 3 and paints the
47
+ * branch red on every merge; this generator has no push pipeline at all,
48
+ * refusing a `push` trigger by name (#2084) because a GitLab schedule is a
49
+ * project-level cron object rather than an event. GitLab does have its own
50
+ * equivalent of the mapping should one ever be wanted — `allow_failure:
51
+ * { exit_codes: [3] }` turns one exit code into a warning rather than a
52
+ * failure, without a flag on the invocation — so the day this generator
53
+ * grows a push trigger, that is the shape to reach for rather than
54
+ * `--gated-exit`.
29
55
  */
30
56
 
31
57
  import { emitYAML } from "@intentius/chant/yaml";
@@ -38,6 +64,37 @@ import type {
38
64
  ScheduledOpSpec,
39
65
  } from "@intentius/chant/lexicon";
40
66
 
67
+ /**
68
+ * Refuse the two #2242 options GitLab cannot honour, by name and before any
69
+ * YAML exists, and return the `run` setup lines that do translate. See the
70
+ * module doc for why each one is a refusal rather than a silent drop.
71
+ */
72
+ function gitlabSetupScript(spec: ScheduledOpSpec): string[] {
73
+ if (spec.permissions && Object.keys(spec.permissions).length > 0) {
74
+ const scopes = Object.entries(spec.permissions)
75
+ .map(([scope, value]) => `${scope}: ${value}`)
76
+ .join(", ");
77
+ throw new Error(
78
+ `Scheduled Op "${spec.name}" adds permissions { ${scopes} }, but GitLab CI has no per-job token-scope ` +
79
+ `mapping — there is no \`permissions:\` key to add them to (#2242). Its OIDC surface is a separate ` +
80
+ `\`id_tokens:\` declaration the job exchanges for cloud credentials itself, which chant does not ` +
81
+ `generate. Drop the option here, or generate this Op for github.`,
82
+ );
83
+ }
84
+ const lines: string[] = [];
85
+ (spec.setup ?? []).forEach((step, index) => {
86
+ if ("uses" in step) {
87
+ throw new Error(
88
+ `Scheduled Op "${spec.name}" setup step ${index + 1} is \`uses: "${step.uses}"\`, a GitHub Actions ` +
89
+ `marketplace action. GitLab CI jobs run \`script\` lines only, so there is nothing to translate it ` +
90
+ `into (#2242). Express the setup as a \`{ run }\` entry, or generate this Op for github/forgejo.`,
91
+ );
92
+ }
93
+ lines.push(step.run);
94
+ });
95
+ return lines;
96
+ }
97
+
41
98
  export type { GenerateGitlabOpOptions, GenerateGitlabOpResult };
42
99
 
43
100
  /** GitLab CI job names must be safe YAML keys; Op names are already kebab-case in every fixture, but normalize defensively (mirrors `./generate-pipeline.ts`'s `toJobName`). */
@@ -85,7 +142,22 @@ export function generateGitlabOpPipeline(
85
142
  ];
86
143
 
87
144
  for (const spec of ops) {
145
+ const setupScript = gitlabSetupScript(spec);
88
146
  const findingMode = spec.findingMode ?? "report";
147
+ if (findingMode === "comment") {
148
+ // The mode posts onto the pull request that triggered the run (#2231),
149
+ // read out of the GitHub Actions event payload by `reconcilePr`. GitLab
150
+ // has neither that event model nor that payload, and core carries no
151
+ // GitLab API client that would post the merge-request note instead, so
152
+ // this refuses the mode by name rather than emitting a job whose finding
153
+ // step fails on every pipeline.
154
+ throw new Error(
155
+ `Scheduled Op "${spec.name}" has findingMode "comment", which posts its finding on the pull request ` +
156
+ `that triggered the run. GitLab has no pull_request event and chant has no GitLab merge-request ` +
157
+ `note activity (#2231). Use findingMode "issue" or "merge-request" here, or generate this Op for ` +
158
+ `github.`,
159
+ );
160
+ }
89
161
  const trigger = resolveOpTrigger(spec);
90
162
  if (trigger.kind !== "cron") {
91
163
  throw new Error(
@@ -98,7 +170,7 @@ export function generateGitlabOpPipeline(
98
170
  headerLines.push(setupLine(spec, trigger.schedule, jobName, findingMode));
99
171
 
100
172
  const runParts = runCommand.map((part) => part.replace("{name}", spec.name));
101
- const script = [...beforeScript, runParts.join(" "), ...extraScript];
173
+ const script = [...setupScript, ...beforeScript, runParts.join(" "), ...extraScript];
102
174
 
103
175
  doc[jobName] = {
104
176
  stage: STAGE,