@sverka/compiler 0.0.0 → 0.1.1

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/index.mjs ADDED
@@ -0,0 +1,3206 @@
1
+ import { stringify } from "yaml";
2
+ import { expandPipelineCalls } from "@sverka/workflow";
3
+ //#region src/plugin/errors.ts
4
+ var PluginError = class extends Error {
5
+ code;
6
+ cause;
7
+ constructor(message, code, cause) {
8
+ super(message);
9
+ this.name = "PluginError";
10
+ this.code = code;
11
+ this.cause = cause;
12
+ }
13
+ };
14
+ //#endregion
15
+ //#region src/plugin/capabilities.ts
16
+ const SUPPORT_LEVELS = [
17
+ "native",
18
+ "lowered",
19
+ "emulated",
20
+ "connector",
21
+ "partial",
22
+ "unsupported"
23
+ ];
24
+ const PRIORITY = {
25
+ native: 6,
26
+ lowered: 5,
27
+ connector: 4,
28
+ emulated: 3,
29
+ partial: 2,
30
+ unsupported: 1
31
+ };
32
+ /**
33
+ * Validate that a value is a valid capability support level.
34
+ */
35
+ function validateSupport(value) {
36
+ if (typeof value !== "string" || !SUPPORT_LEVELS.includes(value)) throw new PluginError(`invalid capability support level: ${String(value)}`, "INVALID_CAPABILITY");
37
+ return value;
38
+ }
39
+ /**
40
+ * Validate a capability manifest object.
41
+ */
42
+ function validateCapabilityManifest(manifest) {
43
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) throw new PluginError("capability manifest must be an object", "INVALID_CAPABILITY");
44
+ const obj = manifest;
45
+ for (const [key, value] of Object.entries(obj)) validateManifestEntry(key, value);
46
+ }
47
+ function validateManifestEntry(key, value) {
48
+ if (typeof value === "string") {
49
+ validateSupport(value);
50
+ return;
51
+ }
52
+ if (typeof value === "object" && value !== null) {
53
+ validateManifestDetail(key, value);
54
+ return;
55
+ }
56
+ throw new PluginError(`capability '${key}' must be a support string or CapabilityDetail object`, "INVALID_CAPABILITY");
57
+ }
58
+ function validateManifestDetail(key, detail) {
59
+ validateSupport(detail.support);
60
+ if (detail.via !== void 0 && typeof detail.via !== "string") throw new PluginError(`capability '${key}' has invalid via: must be a string`, "INVALID_CAPABILITY");
61
+ if (detail.notes !== void 0 && typeof detail.notes !== "string") throw new PluginError(`capability '${key}' has invalid notes: must be a string`, "INVALID_CAPABILITY");
62
+ }
63
+ /**
64
+ * Detect capabilities used by a single step.
65
+ */
66
+ function detectStepCapabilities(step, caps) {
67
+ detectRuntimeCapabilities(step, caps);
68
+ detectRunnerCapabilities(step, caps);
69
+ detectIdentityCapabilities(step, caps);
70
+ detectRuleCapabilities(step, caps);
71
+ detectServiceCapabilities(step, caps);
72
+ detectEnvironmentCapabilities(step, caps);
73
+ detectCacheCapabilities(step, caps);
74
+ detectConcurrencyCapabilities(step, caps);
75
+ detectSpecialStepCapabilities(step, caps);
76
+ const outputFlags = {
77
+ scalar: false,
78
+ artifact: false
79
+ };
80
+ for (const op of step.operations) detectOperationCapabilities(op, caps, outputFlags);
81
+ detectOutputTypeCapabilities(step.outputs, outputFlags);
82
+ if (step.dependencies.length > 0) caps.add("graph.dependencies");
83
+ if (step.condition !== void 0) caps.add("graph.conditions");
84
+ if (outputFlags.scalar) caps.add("output.scalar");
85
+ if (outputFlags.artifact) caps.add("output.artifact");
86
+ detectMatrixCapabilities(step.matrix, caps);
87
+ detectScriptCapabilities(step, caps);
88
+ }
89
+ /**
90
+ * Detect capabilities for special step kinds: call, component, child pipeline,
91
+ * downstream, and delay.
92
+ */
93
+ function detectSpecialStepCapabilities(step, caps) {
94
+ if (step.call !== void 0) caps.add("reusable.pipeline");
95
+ if (step.component !== void 0) caps.add("reusable.component");
96
+ if (step.childPipeline !== void 0) caps.add("reusable.childPipeline");
97
+ if (step.downstream !== void 0) caps.add("reusable.downstream");
98
+ if (step.delay !== void 0) caps.add("scheduling.delay");
99
+ }
100
+ function detectMatrixCapabilities(matrix, caps) {
101
+ if (!matrix) return;
102
+ caps.add("graph.matrix");
103
+ if (matrix.include && matrix.include.length > 0) caps.add("matrix.include");
104
+ if (matrix.exclude && matrix.exclude.length > 0) caps.add("matrix.exclude");
105
+ if (matrix.failFast !== void 0) caps.add("matrix.failFast");
106
+ if (matrix.maxParallel !== void 0) caps.add("matrix.maxParallel");
107
+ }
108
+ function detectScriptCapabilities(step, caps) {
109
+ if (step.beforeScript && step.beforeScript.length > 0) caps.add("step.beforeScript");
110
+ if (step.afterScript && step.afterScript.length > 0) caps.add("step.afterScript");
111
+ if (step.continueOnError !== void 0) caps.add("step.continueOnError");
112
+ if (step.retry !== void 0) caps.add("policy.retry");
113
+ if (step.timeout !== void 0) caps.add("policy.timeout");
114
+ }
115
+ function detectRuntimeCapabilities(step, caps) {
116
+ const mode = step.runtime?.mode ?? "host";
117
+ caps.add(`runtime.${mode}`);
118
+ if (step.runtime?.workingDir) caps.add("execution.workdir");
119
+ if (step.runtime?.shell) caps.add("execution.shell");
120
+ if (step.runtime?.env && Object.keys(step.runtime.env).length > 0) caps.add("environment.variables");
121
+ if (step.runtime?.secrets && step.runtime.secrets.length > 0) caps.add("secrets.runtime");
122
+ if (step.interruptible === true) caps.add("concurrency.interruptible");
123
+ }
124
+ function detectRunnerCapabilities(step, caps) {
125
+ if (step.runner === void 0) return;
126
+ caps.add("runner.selection");
127
+ if (step.runner.group !== void 0) caps.add("runner.group");
128
+ }
129
+ function detectIdentityCapabilities(step, caps) {
130
+ if (step.identity === void 0) return;
131
+ caps.add("secrets.oidc");
132
+ const audiences = /* @__PURE__ */ new Set();
133
+ for (const token of Object.values(step.identity.tokens ?? {})) audiences.add(token.audience);
134
+ if (audiences.size > 1) caps.add("secrets.oidc.multiAudience");
135
+ }
136
+ function detectRuleCapabilities(step, caps) {
137
+ if (step.rules === void 0 || step.rules.length === 0) return;
138
+ caps.add("workflow.rules");
139
+ for (const rule of step.rules) {
140
+ if (rule.changes !== void 0) caps.add("workflow.rules.changes");
141
+ if (rule.exists !== void 0) caps.add("workflow.rules.exists");
142
+ }
143
+ }
144
+ function detectServiceCapabilities(step, caps) {
145
+ if (step.services === void 0 || step.services.length === 0) return;
146
+ caps.add("environment.services");
147
+ for (const service of step.services) if (service.ports !== void 0 && service.ports.length > 0) caps.add("environment.services.ports");
148
+ }
149
+ function detectEnvironmentCapabilities(step, caps) {
150
+ if (step.environment === void 0) return;
151
+ caps.add("deployment.environment");
152
+ if (step.environment.action !== void 0) caps.add("deployment.environment.action");
153
+ if (step.environment.tier !== void 0) caps.add("deployment.environment.tier");
154
+ }
155
+ function detectCacheCapabilities(step, caps) {
156
+ if (step.cache === void 0) return;
157
+ caps.add("cache");
158
+ if (step.cache.policy !== void 0) caps.add("cache.policy");
159
+ if (step.cache.restoreKeys !== void 0 && step.cache.restoreKeys.length > 0) caps.add("cache.fallbackKeys");
160
+ }
161
+ function detectConcurrencyCapabilities(step, caps) {
162
+ if (step.concurrency === void 0) return;
163
+ caps.add("concurrency.group");
164
+ if (step.concurrency.cancelInProgress !== void 0) caps.add("concurrency.cancelInProgress");
165
+ }
166
+ function detectOperationCapabilities(op, caps, outputFlags) {
167
+ switch (op.kind) {
168
+ case "shell":
169
+ caps.add("operation.shell");
170
+ break;
171
+ case "exportOutput":
172
+ outputFlags.scalar = true;
173
+ break;
174
+ case "exportArtifact":
175
+ detectExportArtifactCapabilities(op, caps, outputFlags);
176
+ break;
177
+ case "importArtifact":
178
+ caps.add("operation.import");
179
+ break;
180
+ case "report": detectReportCapabilities(op, caps);
181
+ }
182
+ }
183
+ function detectExportArtifactCapabilities(op, caps, outputFlags) {
184
+ outputFlags.artifact = true;
185
+ if (op.retention !== void 0) caps.add("artifact.retention");
186
+ if (op.access !== void 0) caps.add("artifact.access");
187
+ }
188
+ function detectReportCapabilities(op, caps) {
189
+ caps.add("artifact.report");
190
+ if (op.spec?.type !== void 0) caps.add(`artifact.report.${op.spec.type}`);
191
+ }
192
+ function detectOutputTypeCapabilities(outputs, outputFlags) {
193
+ for (const output of outputs) if (output.type === "artifact") outputFlags.artifact = true;
194
+ else outputFlags.scalar = true;
195
+ }
196
+ /**
197
+ * Detect capabilities used by a single pipeline.
198
+ */
199
+ function detectPipelineCapabilities(pipeline, caps) {
200
+ detectTriggerCapabilities(pipeline.entries, caps);
201
+ if (pipeline.permissions !== void 0) caps.add("environment.permissions");
202
+ detectInputCapabilities(pipeline.inputs, caps);
203
+ if (pipeline.defaults !== void 0) detectDefaultCapabilities(pipeline.defaults, caps);
204
+ if (pipeline.concurrency !== void 0) {
205
+ caps.add("concurrency.group");
206
+ if (pipeline.concurrency.cancelInProgress !== void 0) caps.add("concurrency.cancelInProgress");
207
+ }
208
+ if (pipeline.rules !== void 0 && pipeline.rules.length > 0) caps.add("workflow.rules");
209
+ for (const step of pipeline.steps) detectStepCapabilities(step, caps);
210
+ }
211
+ function detectTriggerCapabilities(entries, caps) {
212
+ for (const entry of entries) caps.add(`trigger.${entry.trigger.kind}`);
213
+ }
214
+ function detectInputCapabilities(inputs, caps) {
215
+ if (inputs === void 0 || Object.keys(inputs).length === 0) return;
216
+ caps.add("workflow.inputs");
217
+ for (const input of Object.values(inputs)) {
218
+ if (input.secret) caps.add("secrets.pipeline-input");
219
+ if (input.type === "choice") caps.add("workflow.inputs.choice");
220
+ if (input.type === "array") caps.add("workflow.inputs.array");
221
+ if (input.pattern !== void 0) caps.add("workflow.inputs.pattern");
222
+ }
223
+ }
224
+ function detectDefaultCapabilities(defaults, caps) {
225
+ caps.add("workflow.defaults");
226
+ const defaultCaps = [
227
+ [defaults.shell, "workflow.defaults.shell"],
228
+ [defaults.workdir, "workflow.defaults.workdir"],
229
+ [defaults.env, "workflow.defaults.env"],
230
+ [defaults.beforeScript, "workflow.defaults.beforeScript"],
231
+ [defaults.afterScript, "workflow.defaults.afterScript"],
232
+ [defaults.timeout, "workflow.defaults.timeout"],
233
+ [defaults.retry, "workflow.defaults.retry"],
234
+ [defaults.interruptible, "workflow.defaults.interruptible"]
235
+ ];
236
+ for (const [value, cap] of defaultCaps) if (value !== void 0) caps.add(cap);
237
+ }
238
+ /**
239
+ * Detect capabilities used by a Definition Graph.
240
+ * Returns a set of capability identifiers (e.g., "trigger.push").
241
+ */
242
+ function detectCapabilities(graph) {
243
+ const caps = /* @__PURE__ */ new Set();
244
+ for (const pipeline of graph.project.pipelines) detectPipelineCapabilities(pipeline, caps);
245
+ return caps;
246
+ }
247
+ /**
248
+ * Get the support level from a manifest entry.
249
+ */
250
+ function getSupportLevel(entry) {
251
+ if (typeof entry === "string") return validateSupport(entry);
252
+ if (entry === null || typeof entry !== "object") throw new PluginError("capability entry must be a string or CapabilityDetail", "INVALID_CAPABILITY");
253
+ return validateSupport(entry.support);
254
+ }
255
+ /**
256
+ * Find the best support level for a capability across all manifests.
257
+ * "native" > "lowered" > "connector" > "emulated" > "partial" > "unsupported".
258
+ */
259
+ function findBestSupport(capability, manifests) {
260
+ let best = "unsupported";
261
+ for (const manifest of manifests) {
262
+ const entry = manifest[capability];
263
+ if (entry === void 0) continue;
264
+ const level = getSupportLevel(entry);
265
+ if (PRIORITY[level] > PRIORITY[best]) best = level;
266
+ }
267
+ return best;
268
+ }
269
+ /**
270
+ * Map a support level to a diagnostic severity.
271
+ */
272
+ function severityFor(support) {
273
+ switch (support) {
274
+ case "unsupported": return "error";
275
+ case "emulated":
276
+ case "partial": return "warning";
277
+ case "connector": return "info";
278
+ default: return "info";
279
+ }
280
+ }
281
+ /**
282
+ * Analyze a Definition Graph against capability manifests.
283
+ * Returns diagnostics for capabilities that are not natively or lowered
284
+ * supported.
285
+ */
286
+ function analyzeCapabilities(graph, manifests) {
287
+ for (const manifest of manifests) validateCapabilityManifest(manifest);
288
+ const used = detectCapabilities(graph);
289
+ const diagnostics = [];
290
+ for (const capability of used) {
291
+ const support = findBestSupport(capability, manifests);
292
+ if (support === "native" || support === "lowered") continue;
293
+ diagnostics.push({
294
+ capability,
295
+ support,
296
+ severity: severityFor(support),
297
+ message: `capability '${capability}' is ${support}`
298
+ });
299
+ }
300
+ return diagnostics;
301
+ }
302
+ //#endregion
303
+ //#region src/plugin/factory.ts
304
+ /**
305
+ * Define a Sverka plugin. The factory function receives optional options
306
+ * and meta, and returns a SverkaPlugin object.
307
+ *
308
+ * @example
309
+ * const plugin = defineSverkaPlugin(() => ({
310
+ * name: "github",
311
+ * apiVersion: "sverka.dev/v1",
312
+ * capabilities: { "trigger.push": "native" },
313
+ * }));
314
+ */
315
+ function defineSverkaPlugin(factory, options) {
316
+ const plugin = factory(options, { version: "1.0.0" });
317
+ validatePlugin(plugin);
318
+ return snapshotPlugin(plugin);
319
+ }
320
+ /**
321
+ * Store a validated, defensive copy of a plugin so callers cannot mutate
322
+ * the registry's internal state after registration.
323
+ */
324
+ function snapshotPlugin(plugin) {
325
+ const copy = {
326
+ name: plugin.name,
327
+ apiVersion: plugin.apiVersion
328
+ };
329
+ if (plugin.capabilities) copy.capabilities = snapshotCapabilityManifest(plugin.capabilities);
330
+ if (plugin.model) copy.model = [...plugin.model];
331
+ if (plugin.transforms) copy.transforms = [...plugin.transforms];
332
+ if (plugin.validators) copy.validators = [...plugin.validators];
333
+ if (plugin.targets) copy.targets = [...plugin.targets];
334
+ if (plugin.importers) copy.importers = [...plugin.importers];
335
+ if (plugin.engines) copy.engines = [...plugin.engines];
336
+ if (plugin.connectors) copy.connectors = [...plugin.connectors];
337
+ if (plugin.extensions) copy.extensions = [...plugin.extensions];
338
+ if (plugin.tools) copy.tools = plugin.tools;
339
+ return copy;
340
+ }
341
+ function snapshotCapabilityManifest(manifest) {
342
+ const copy = {};
343
+ for (const [capability, value] of Object.entries(manifest)) copy[capability] = typeof value === "string" ? value : { ...value };
344
+ return copy;
345
+ }
346
+ /**
347
+ * Validate a plugin has required fields and a valid capabilities manifest.
348
+ */
349
+ function validatePlugin(plugin) {
350
+ if (!plugin || typeof plugin !== "object") throw new PluginError("plugin must be an object", "INVALID_PLUGIN");
351
+ if (typeof plugin.name !== "string" || plugin.name.length === 0) throw new PluginError("plugin must have a non-empty name", "INVALID_PLUGIN");
352
+ if (typeof plugin.apiVersion !== "string" || plugin.apiVersion.length === 0) throw new PluginError("plugin must have an apiVersion", "INVALID_PLUGIN");
353
+ if (plugin.capabilities !== void 0) validateCapabilityManifest(plugin.capabilities);
354
+ }
355
+ /**
356
+ * Create a plugin registry.
357
+ */
358
+ function createPluginRegistry() {
359
+ const plugins = [];
360
+ const names = /* @__PURE__ */ new Set();
361
+ return {
362
+ register(plugin) {
363
+ validatePlugin(plugin);
364
+ if (names.has(plugin.name)) throw new PluginError(`plugin '${plugin.name}' is already registered`, "DUPLICATE_PLUGIN");
365
+ names.add(plugin.name);
366
+ plugins.push(snapshotPlugin(plugin));
367
+ },
368
+ get plugins() {
369
+ return plugins.map((p) => snapshotPlugin(p));
370
+ },
371
+ getCapabilities() {
372
+ return plugins.filter((p) => p.capabilities !== void 0).map((p) => snapshotCapabilityManifest(p.capabilities));
373
+ }
374
+ };
375
+ }
376
+ //#endregion
377
+ //#region src/github/capabilities.ts
378
+ const githubCapabilities = {
379
+ "trigger.push": "native",
380
+ "trigger.changeRequest": "native",
381
+ "trigger.manual": "native",
382
+ "runtime.host": "native",
383
+ "runtime.container": "native",
384
+ "operation.shell": "native",
385
+ "operation.import": "lowered",
386
+ "output.scalar": "lowered",
387
+ "output.artifact": "native",
388
+ "graph.dependencies": "native",
389
+ "graph.conditions": "native",
390
+ "graph.matrix": "native",
391
+ "matrix.include": "native",
392
+ "matrix.exclude": "native",
393
+ "matrix.failFast": "native",
394
+ "matrix.maxParallel": "native",
395
+ "trigger.schedule": "native",
396
+ "step.beforeScript": "native",
397
+ "step.afterScript": "native",
398
+ "step.continueOnError": "native",
399
+ "policy.retry": "emulated",
400
+ "policy.compensation": "emulated",
401
+ "execution.workdir": "native",
402
+ "execution.shell": "native",
403
+ "environment.variables": "native",
404
+ "secrets.runtime": "native",
405
+ "secrets.pipeline-input": "native",
406
+ "concurrency.interruptible": "partial",
407
+ "environment.permissions": "native",
408
+ "runner.selection": "native",
409
+ "runner.group": "native",
410
+ "secrets.oidc": "native",
411
+ "secrets.oidc.multiAudience": "unsupported",
412
+ "workflow.rules": "partial",
413
+ "workflow.rules.changes": "unsupported",
414
+ "workflow.rules.exists": "unsupported",
415
+ "workflow.defaults": "native",
416
+ "workflow.defaults.shell": "native",
417
+ "workflow.defaults.workdir": "native",
418
+ "workflow.defaults.env": "unsupported",
419
+ "workflow.defaults.beforeScript": "lowered",
420
+ "workflow.defaults.afterScript": "lowered",
421
+ "workflow.defaults.timeout": "unsupported",
422
+ "workflow.defaults.retry": "unsupported",
423
+ "workflow.defaults.interruptible": "unsupported",
424
+ "artifact.report": "emulated",
425
+ "artifact.report.junit": "emulated",
426
+ "artifact.report.coverage": "emulated",
427
+ "artifact.report.dotenv": "emulated",
428
+ "artifact.report.sast": "emulated",
429
+ "artifact.report.dast": "emulated",
430
+ "artifact.report.dependencyScanning": "emulated",
431
+ "artifact.report.containerScanning": "emulated",
432
+ "artifact.report.licenseScanning": "emulated",
433
+ "artifact.report.performance": "emulated",
434
+ "artifact.report.metrics": "emulated",
435
+ "artifact.report.terraform": "emulated",
436
+ "artifact.report.quality": "emulated",
437
+ "artifact.report.sarif": "emulated",
438
+ "workflow.inputs": "native",
439
+ "workflow.inputs.choice": "native",
440
+ "workflow.inputs.array": "unsupported",
441
+ "workflow.inputs.pattern": "unsupported",
442
+ "environment.services": "native",
443
+ "environment.services.ports": "native",
444
+ "deployment.environment": "native",
445
+ "deployment.environment.action": "unsupported",
446
+ "deployment.environment.tier": "unsupported",
447
+ "artifact.retention": "native",
448
+ "artifact.access": "unsupported",
449
+ "cache": "lowered",
450
+ "cache.policy": "lowered",
451
+ "cache.fallbackKeys": "lowered",
452
+ "concurrency.group": "native",
453
+ "concurrency.cancelInProgress": "native",
454
+ "reusable.pipeline": "native",
455
+ "reusable.pipeline.inputs": "native",
456
+ "reusable.pipeline.outputs": "native",
457
+ "reusable.component": "native",
458
+ "reusable.component.versioning": "native",
459
+ "reusable.childPipeline": "unsupported",
460
+ "reusable.downstream": "emulated",
461
+ "deployment.release": "emulated",
462
+ "deployment.pages": "native",
463
+ "import.github": "native",
464
+ "import.include": "emulated",
465
+ "scheduling.delay": "emulated",
466
+ "execution.background": "emulated",
467
+ "runtime.network": "emulated",
468
+ "step.permissions": "native",
469
+ "agent.step": "emulated",
470
+ "run.query": "emulated"
471
+ };
472
+ //#endregion
473
+ //#region src/github/errors.ts
474
+ var GithubTargetError = class extends Error {
475
+ code;
476
+ cause;
477
+ constructor(message, code, cause) {
478
+ super(message);
479
+ this.name = "GithubTargetError";
480
+ this.code = code;
481
+ this.cause = cause;
482
+ }
483
+ };
484
+ //#endregion
485
+ //#region src/stdout-capture.ts
486
+ /**
487
+ * Quote a literal string using single quotes for a POSIX shell.
488
+ */
489
+ function shellQuoteSingle(value) {
490
+ return `'${value.replace(/'/g, "'\\''")}'`;
491
+ }
492
+ /**
493
+ * Wrap a shell command line so its stdout is also written to the declared
494
+ * artifact file(s), matching the runtime's exportStdout semantics. The
495
+ * capture must work under POSIX sh (no pipefail), so the command's stdout is
496
+ * redirected to the file and replayed with cat; the original exit code is
497
+ * re-raised so a failing check still fails the job.
498
+ *
499
+ * Background commands (`cmd &`) detach before stdout can be captured; the
500
+ * runtime records an empty artifact for them, so empty files are created.
501
+ */
502
+ function wrapStdoutCaptureLine(command, names) {
503
+ const quoted = names.map(shellQuoteSingle);
504
+ if (command.endsWith(" &")) return [command, `touch ${quoted.join(" ")}`].join("\n");
505
+ const [first, ...rest] = quoted;
506
+ const teeRest = rest.length > 0 ? ` | tee ${rest.join(" ")}` : "";
507
+ return [
508
+ "sverka_rc=0",
509
+ `{ ${command}`,
510
+ `} > ${first} || sverka_rc=$?`,
511
+ `cat ${first}${teeRest}`,
512
+ `if [ "$sverka_rc" -gt 0 ]; then exit "$sverka_rc"; fi`
513
+ ].join("\n");
514
+ }
515
+ //#endregion
516
+ //#region src/github/lower.ts
517
+ /**
518
+ * Lower a Definition Graph to one or more GithubTargetGraphs.
519
+ * - Each pipeline with entries → a root workflow with its triggers.
520
+ * - Each pipeline referenced by a call step → a reusable workflow with
521
+ * `on: workflow_call` (+ its own triggers if it also has entries).
522
+ * - Call steps become `uses:` jobs.
523
+ * Single-pipeline graphs (no calls) are unchanged (backward compat).
524
+ */
525
+ function lowerGithub(graph) {
526
+ if (graph.project.pipelines.length === 0) throw new GithubTargetError("graph has no pipelines", "INVALID_GRAPH");
527
+ if (graph.project.pipelines.length === 1) {
528
+ const pipeline = graph.project.pipelines[0];
529
+ if (!pipeline.steps.some((s) => s.call || s.component || s.childPipeline || s.downstream)) return lowerSinglePipeline(pipeline);
530
+ }
531
+ return lowerMultiPipeline(graph);
532
+ }
533
+ /**
534
+ * Lower a single pipeline (no calls) — original v0 behavior.
535
+ */
536
+ function lowerSinglePipeline(pipeline) {
537
+ const reachableSteps = filterReachableSteps$1(pipeline);
538
+ const jobIdMap = buildJobIdMap$1(reachableSteps);
539
+ return assemblePipelineTarget(pipeline, lowerTriggers$1(pipeline.entries, pipeline.inputs), lowerStepsWithCalls(reachableSteps, jobIdMap, pipeline.id));
540
+ }
541
+ /**
542
+ * Assemble a GithubTargetGraph from a pipeline, its triggers, and jobs.
543
+ * Shared by lowerSinglePipeline and lowerMultiPipeline.
544
+ */
545
+ function assemblePipelineTarget(pipeline, triggers, jobs) {
546
+ return {
547
+ name: pipeline.id,
548
+ on: triggers,
549
+ jobs,
550
+ env: collectEnv(pipeline),
551
+ ...pipeline.permissions !== void 0 ? { permissions: pipeline.permissions } : {},
552
+ ...pipeline.defaults !== void 0 ? { defaults: lowerDefaults(pipeline.defaults) } : {},
553
+ ...pipeline.concurrency !== void 0 ? { concurrency: pipeline.concurrency } : {}
554
+ };
555
+ }
556
+ /**
557
+ * Lower pipeline defaults to GitHub defaults.run (shell + working-directory only).
558
+ */
559
+ function lowerDefaults(defaults) {
560
+ return { run: {
561
+ ...defaults.shell !== void 0 ? { shell: defaults.shell } : {},
562
+ ...defaults.workdir !== void 0 ? { "working-directory": defaults.workdir } : {}
563
+ } };
564
+ }
565
+ /**
566
+ * Lower a multi-pipeline graph. Each pipeline that has entries OR is referenced
567
+ * by a call step gets its own workflow file.
568
+ */
569
+ function lowerMultiPipeline(graph) {
570
+ const pipelines = graph.project.pipelines;
571
+ const calledPipelineIds = collectCalledPipelineIds(pipelines);
572
+ const result = [];
573
+ for (const pipeline of pipelines) {
574
+ const target = lowerPipelineInGraph(pipeline, calledPipelineIds.has(pipeline.id));
575
+ if (target !== void 0) result.push(target);
576
+ }
577
+ return result;
578
+ }
579
+ /**
580
+ * Collect the set of pipeline IDs that are referenced by at least one call step.
581
+ */
582
+ function collectCalledPipelineIds(pipelines) {
583
+ const called = /* @__PURE__ */ new Set();
584
+ for (const p of pipelines) for (const step of p.steps) if (step.call) called.add(step.call.callee);
585
+ return called;
586
+ }
587
+ /**
588
+ * Lower a single pipeline within a multi-pipeline graph.
589
+ * Returns undefined if the pipeline is neither a root nor a callee.
590
+ */
591
+ function lowerPipelineInGraph(pipeline, isCalled) {
592
+ const hasEntries = pipeline.entries.length > 0;
593
+ if (!hasEntries && !isCalled) return void 0;
594
+ const reachableSteps = hasEntries ? filterReachableSteps$1(pipeline) : pipeline.steps;
595
+ const jobIdMap = buildJobIdMap$1(reachableSteps);
596
+ let triggers = lowerTriggers$1(pipeline.entries, pipeline.inputs);
597
+ if (isCalled) triggers = addWorkflowCall(triggers, pipeline.inputs);
598
+ const jobs = lowerStepsWithCalls(reachableSteps, jobIdMap, pipeline.id);
599
+ return assemblePipelineTarget(pipeline, triggers, jobs);
600
+ }
601
+ /**
602
+ * Add workflow_call trigger with inputs to a GithubTriggers object.
603
+ */
604
+ function addWorkflowCall(triggers, inputs) {
605
+ const inputEntries = Object.entries(inputs);
606
+ if (inputEntries.length === 0) return {
607
+ ...triggers,
608
+ workflow_call: null
609
+ };
610
+ const workflowInputs = {};
611
+ for (const [name, input] of inputEntries) {
612
+ let type = "string";
613
+ if (input.type === "number") type = "number";
614
+ else if (input.type === "boolean") type = "boolean";
615
+ const ghInput = {
616
+ type,
617
+ required: input.required ?? false
618
+ };
619
+ if (input.default !== void 0) ghInput.default = input.default;
620
+ if (input.description !== void 0) ghInput.description = input.description;
621
+ workflowInputs[name] = ghInput;
622
+ }
623
+ return {
624
+ ...triggers,
625
+ workflow_call: { inputs: workflowInputs }
626
+ };
627
+ }
628
+ /**
629
+ * Return the steps reachable from any entry root by following dependencies.
630
+ */
631
+ function filterReachableSteps$1(pipeline) {
632
+ if (pipeline.steps.length === 0) {
633
+ const invalidRoot = pipeline.entries.flatMap((e) => e.roots).find((root) => root.length > 0);
634
+ if (invalidRoot) throw new GithubTargetError(`entry references unknown root step '${invalidRoot}'`, "INVALID_GRAPH");
635
+ return [];
636
+ }
637
+ const byId = new Map(pipeline.steps.map((step) => [step.id, step]));
638
+ const reachable = /* @__PURE__ */ new Set();
639
+ const queue = [];
640
+ for (const entry of pipeline.entries) enqueueRoots(entry.roots, byId, reachable, queue);
641
+ while (queue.length > 0) {
642
+ const id = queue.shift();
643
+ const step = byId.get(id);
644
+ if (!step) continue;
645
+ enqueueDependencies(step, byId, reachable, queue);
646
+ }
647
+ return pipeline.steps.filter((step) => reachable.has(step.id));
648
+ }
649
+ function enqueueRoots(roots, byId, reachable, queue) {
650
+ for (const root of roots) {
651
+ if (!byId.has(root)) throw new GithubTargetError(`entry references unknown root step '${root}'`, "INVALID_GRAPH");
652
+ enqueueIfNew(root, reachable, queue);
653
+ }
654
+ }
655
+ function enqueueDependencies(step, byId, reachable, queue) {
656
+ for (const dep of step.dependencies) {
657
+ const producer = dep.producer;
658
+ if (!byId.has(producer)) throw new GithubTargetError(`step depends on unknown producer '${producer}'`, "INVALID_GRAPH");
659
+ enqueueIfNew(producer, reachable, queue);
660
+ }
661
+ }
662
+ function enqueueIfNew(id, reachable, queue) {
663
+ if (!reachable.has(id)) {
664
+ reachable.add(id);
665
+ queue.push(id);
666
+ }
667
+ }
668
+ /**
669
+ * Build a mapping from full step IDs (e.g., "ci/lint") to GitHub-safe
670
+ * job IDs (e.g., "lint"). If there are collisions, append a suffix.
671
+ */
672
+ function buildJobIdMap$1(steps) {
673
+ const map = /* @__PURE__ */ new Map();
674
+ const used = /* @__PURE__ */ new Set();
675
+ for (const step of steps) {
676
+ const shortId = step.id.includes("/") ? step.id.split("/").pop() : step.id;
677
+ let jobId = shortId;
678
+ let suffix = 1;
679
+ while (used.has(jobId)) {
680
+ jobId = `${shortId}-${suffix}`;
681
+ suffix++;
682
+ }
683
+ used.add(jobId);
684
+ map.set(step.id, jobId);
685
+ }
686
+ return map;
687
+ }
688
+ /**
689
+ * Map Sverka triggers to GitHub triggers.
690
+ * Multiple entries of the same kind have their filters merged.
691
+ */
692
+ function lowerTriggers$1(entries, inputs) {
693
+ const pushBranches = /* @__PURE__ */ new Set();
694
+ const pushTags = /* @__PURE__ */ new Set();
695
+ const pushPaths = /* @__PURE__ */ new Set();
696
+ let pushAll = false;
697
+ const prBranches = /* @__PURE__ */ new Set();
698
+ const prPaths = /* @__PURE__ */ new Set();
699
+ let prAll = false;
700
+ let hasManual = false;
701
+ const scheduleEntries = [];
702
+ for (const entry of entries) {
703
+ const t = entry.trigger;
704
+ switch (t.kind) {
705
+ case "push":
706
+ collectFilters(t, pushBranches, pushTags, pushPaths, () => pushAll = true);
707
+ break;
708
+ case "changeRequest":
709
+ collectFilters(t, prBranches, void 0, prPaths, () => prAll = true);
710
+ break;
711
+ case "manual":
712
+ hasManual = true;
713
+ break;
714
+ case "schedule":
715
+ scheduleEntries.push({
716
+ cron: t.cron,
717
+ ...t.timezone ? { timezone: t.timezone } : {}
718
+ });
719
+ break;
720
+ default: throw new GithubTargetError(`unsupported trigger kind: ${JSON.stringify(t.kind)}`, "UNSUPPORTED_TRIGGER");
721
+ }
722
+ }
723
+ return assembleTriggers({
724
+ pushAll,
725
+ pushBranches,
726
+ pushTags,
727
+ pushPaths,
728
+ prAll,
729
+ prBranches,
730
+ prPaths,
731
+ hasManual,
732
+ scheduleEntries,
733
+ inputs
734
+ });
735
+ }
736
+ function collectFilters(t, branches, tags, paths, markAll) {
737
+ if (t.kind === "schedule") {
738
+ markAll();
739
+ return;
740
+ }
741
+ const filter = t.filter;
742
+ const hasBranches = filter?.branches && filter.branches.length > 0;
743
+ const hasTags = filter?.tags && filter.tags.length > 0;
744
+ const hasPaths = filter?.paths && filter.paths.length > 0;
745
+ if (hasTags && tags === void 0) throw new GithubTargetError("tag filters are not supported on change-request triggers", "UNSUPPORTED_TRIGGER");
746
+ if (hasBranches) addAll(branches, filter.branches);
747
+ if (hasTags && tags) addAll(tags, filter.tags);
748
+ if (hasPaths) addAll(paths, filter.paths);
749
+ if (!hasBranches && !hasTags && !hasPaths) markAll();
750
+ }
751
+ /** Add all items from a readonly array to a Set. */
752
+ function addAll(set, items) {
753
+ for (const item of items) set.add(item);
754
+ }
755
+ function assembleTriggers(f) {
756
+ const triggers = {};
757
+ const push = assemblePushTrigger(f);
758
+ if (push) triggers.push = push;
759
+ const pr = assemblePullRequestTrigger(f);
760
+ if (pr) triggers.pull_request = pr;
761
+ if (f.hasManual) {
762
+ const loweredInputs = lowerInputs(f.inputs);
763
+ triggers.workflow_dispatch = loweredInputs !== void 0 ? { inputs: loweredInputs } : null;
764
+ }
765
+ if (f.scheduleEntries.length > 0) triggers.schedule = f.scheduleEntries;
766
+ return triggers;
767
+ }
768
+ function assemblePushTrigger(f) {
769
+ if (f.pushAll) return {};
770
+ if (f.pushBranches.size === 0 && f.pushTags.size === 0 && f.pushPaths.size === 0) return null;
771
+ const push = {};
772
+ if (f.pushBranches.size > 0) push.branches = [...f.pushBranches];
773
+ if (f.pushTags.size > 0) push.tags = [...f.pushTags];
774
+ if (f.pushPaths.size > 0) push.paths = [...f.pushPaths];
775
+ return push;
776
+ }
777
+ function assemblePullRequestTrigger(f) {
778
+ if (f.prAll) return {};
779
+ if (f.prBranches.size === 0 && f.prPaths.size === 0) return null;
780
+ const pr = {};
781
+ if (f.prBranches.size > 0) pr.branches = [...f.prBranches];
782
+ if (f.prPaths.size > 0) pr.paths = [...f.prPaths];
783
+ return pr;
784
+ }
785
+ /**
786
+ * Lower pipeline inputs to GitHub workflow_dispatch inputs.
787
+ * Returns undefined if no inputs are present.
788
+ * Maps Sverka types to GitHub types: choice→choice, array→unsupported (dropped),
789
+ * others map directly. pattern is dropped (unsupported on GitHub).
790
+ */
791
+ function lowerInputs(inputs) {
792
+ if (Object.keys(inputs).length === 0) return void 0;
793
+ const result = {};
794
+ for (const [name, input] of Object.entries(inputs)) result[name] = {
795
+ type: input.type === "array" ? "string" : input.type,
796
+ ...input.description !== void 0 ? { description: input.description } : {},
797
+ ...input.required !== void 0 ? { required: input.required } : {},
798
+ ...input.default !== void 0 && typeof input.default !== "object" ? { default: input.default } : {},
799
+ ...input.options !== void 0 ? { options: input.options } : {}
800
+ };
801
+ return result;
802
+ }
803
+ /**
804
+ * Lower reachable steps to GitHub jobs. One job per step.
805
+ * Call steps become `uses:` jobs (reusable workflow calls).
806
+ * Component steps become `uses:` jobs (composite action calls).
807
+ */
808
+ function lowerStepsWithCalls(steps, jobIdMap, pipelineId) {
809
+ return steps.map((step) => {
810
+ if (step.call) return lowerCallStep(step, jobIdMap, pipelineId);
811
+ if (step.component) return lowerComponentStep(step, jobIdMap);
812
+ if (step.childPipeline) return lowerChildPipelineStep$1(step, jobIdMap);
813
+ if (step.downstream) return lowerDownstreamStep$1(step, jobIdMap);
814
+ return lowerStep$1(step, jobIdMap);
815
+ });
816
+ }
817
+ /**
818
+ * Lower a Reference value to a GitHub expression string.
819
+ * Returns the expression if `value` is a Reference, otherwise `undefined`
820
+ * (meaning the value is a literal the caller should handle itself).
821
+ * Context references are mapped through GITHUB_CONTEXT_MAP so that Sverka
822
+ * namespaces (e.g. `git.sha`) become valid GitHub expressions (e.g.
823
+ * `github.sha`).
824
+ *
825
+ * Secret references are sink-specific:
826
+ * - In `with:` inputs (`forWithInput = true`): `${{ secrets.FIELD }}`.
827
+ * GitHub Actions does not expand `$FIELD` in `with:` values — it treats it
828
+ * as a literal string. The `secrets` context is the correct syntax here.
829
+ * - In `run:` commands (`forWithInput = false`, default): `$FIELD`.
830
+ * The shell expands the env var, and this avoids exposing the secret value
831
+ * in workflow logs.
832
+ */
833
+ function lowerReferenceExpr(value, jobIdMap, forWithInput = false) {
834
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value) {
835
+ const ref = value;
836
+ if (ref.kind === "step") return `\${{ needs.${jobIdMap.get(ref.step) ?? ref.step}.outputs.${ref.output} }}`;
837
+ if (ref.kind === "context") return translateContextRef(ref.namespace, ref.field, false, forWithInput);
838
+ }
839
+ }
840
+ /**
841
+ * Lower a call step to a GitHub reusable workflow call job.
842
+ */
843
+ function lowerCallStep(step, jobIdMap, pipelineId) {
844
+ const jobId = jobIdMap.get(step.id) ?? step.id;
845
+ const needs = lowerDependencies$1(step.dependencies, jobIdMap);
846
+ const call = step.call;
847
+ const callee = call.callee;
848
+ const withMap = {};
849
+ for (const [name, value] of Object.entries(call.inputs)) withMap[name] = lowerReferenceExpr(value, jobIdMap, true) ?? value;
850
+ return {
851
+ id: jobId,
852
+ name: jobId,
853
+ runsOn: "ubuntu-latest",
854
+ needs,
855
+ steps: [],
856
+ uses: `./.github/workflows/${callee}.yml`,
857
+ ...Object.keys(withMap).length > 0 ? { with: withMap } : {},
858
+ secrets: "inherit"
859
+ };
860
+ }
861
+ /**
862
+ * Lower a component step to a GitHub composite action call job.
863
+ * F-32: component → uses: org/component@version with `with:` inputs.
864
+ */
865
+ function lowerComponentStep(step, jobIdMap) {
866
+ const jobId = jobIdMap.get(step.id) ?? step.id;
867
+ const needs = lowerDependencies$1(step.dependencies, jobIdMap);
868
+ const comp = step.component;
869
+ const withMap = {};
870
+ for (const [name, value] of Object.entries(comp.inputs)) withMap[name] = lowerReferenceExpr(value, jobIdMap, true) ?? value;
871
+ if (!comp.name.startsWith("./")) return {
872
+ id: jobId,
873
+ name: jobId,
874
+ runsOn: resolveRunsOn(step),
875
+ needs,
876
+ steps: [{
877
+ name: "Checkout",
878
+ uses: "actions/checkout@v4"
879
+ }, {
880
+ name: `Component ${comp.name}`,
881
+ uses: `${comp.name}@${comp.version}`,
882
+ ...Object.keys(withMap).length > 0 ? { with: withMap } : {}
883
+ }]
884
+ };
885
+ return {
886
+ id: jobId,
887
+ name: jobId,
888
+ runsOn: "ubuntu-latest",
889
+ needs,
890
+ steps: [],
891
+ uses: `${comp.name}@${comp.version}`,
892
+ ...Object.keys(withMap).length > 0 ? { with: withMap } : {}
893
+ };
894
+ }
895
+ /**
896
+ * Lower a child pipeline step to a GitHub job.
897
+ * F-33: GitHub does not natively support dynamic child pipelines.
898
+ * We emit a no-op job with a warning comment. The native engine handles
899
+ * the actual dynamic generation.
900
+ */
901
+ function lowerChildPipelineStep$1(step, jobIdMap) {
902
+ const jobId = jobIdMap.get(step.id) ?? step.id;
903
+ const needs = lowerDependencies$1(step.dependencies, jobIdMap);
904
+ return {
905
+ id: jobId,
906
+ name: jobId,
907
+ runsOn: resolveRunsOn(step),
908
+ needs,
909
+ steps: [{
910
+ name: "Dynamic child pipeline (not natively supported on GitHub)",
911
+ run: `echo "WARNING: Dynamic child pipelines are not supported by GitHub Actions. Generator: ${step.childPipeline.generator}, artifact: ${step.childPipeline.artifact}"`
912
+ }]
913
+ };
914
+ }
915
+ /**
916
+ * Lower a downstream step to a GitHub job.
917
+ * F-34: GitHub emulates downstream project triggers via repository_dispatch API.
918
+ */
919
+ function lowerDownstreamStep$1(step, jobIdMap) {
920
+ const jobId = jobIdMap.get(step.id) ?? step.id;
921
+ const needs = lowerDependencies$1(step.dependencies, jobIdMap);
922
+ const ds = step.downstream;
923
+ const payloadObj = {};
924
+ if (ds.inputs) for (const [name, value] of Object.entries(ds.inputs)) payloadObj[name] = lowerReferenceExpr(value, jobIdMap) ?? String(value);
925
+ const env = { CLIENT_PAYLOAD: JSON.stringify(payloadObj) };
926
+ if (ds.branch) env.DOWNSTREAM_BRANCH = ds.branch;
927
+ const refPart = ds.branch ? ` -f ref="$DOWNSTREAM_BRANCH"` : "";
928
+ return {
929
+ id: jobId,
930
+ name: jobId,
931
+ runsOn: resolveRunsOn(step),
932
+ needs,
933
+ steps: [{
934
+ name: `Trigger downstream: ${ds.project}`,
935
+ env,
936
+ run: `gh api repos/${ds.project}/dispatches -f event_type=sverka-trigger -f client_payload="$CLIENT_PAYLOAD"${refPart}`
937
+ }]
938
+ };
939
+ }
940
+ /**
941
+ * Lower a single Step to a GitHub job.
942
+ */
943
+ function lowerStep$1(step, jobIdMap) {
944
+ const needs = lowerDependencies$1(step.dependencies, jobIdMap);
945
+ const rawSteps = lowerOperations$1(step, jobIdMap);
946
+ if (step.continueOnError !== void 0 && typeof step.continueOnError !== "boolean") throw new GithubTargetError("GitHub does not support exit-code continueOnError; use a boolean value", "UNSUPPORTED_FEATURE");
947
+ const steps = step.continueOnError !== void 0 ? rawSteps.map((s) => s.run !== void 0 ? {
948
+ ...s,
949
+ continueOnError: typeof step.continueOnError === "boolean" ? step.continueOnError : true
950
+ } : s) : [...rawSteps];
951
+ const jobId = jobIdMap.get(step.id) ?? step.id;
952
+ applyDelay(steps, step);
953
+ const runtime = step.runtime;
954
+ const mode = runtime.mode ?? "host";
955
+ return assembleGithubJob({
956
+ jobId,
957
+ steps,
958
+ needs,
959
+ step,
960
+ runsOn: resolveRunsOn(step),
961
+ container: resolveContainer(step, mode),
962
+ jobEnv: collectJobEnv(runtime),
963
+ jobIdMap
964
+ });
965
+ }
966
+ /**
967
+ * Insert a sleep step to emulate delay (GitHub has no native delayed execution).
968
+ */
969
+ function applyDelay(steps, step) {
970
+ if (!step.delay) return;
971
+ const sleepSeconds = parseDurationToSeconds(step.delay);
972
+ if (steps.length > 0) steps.splice(1, 0, {
973
+ name: `Delay (${step.delay})`,
974
+ run: `sleep ${sleepSeconds}`
975
+ });
976
+ }
977
+ /**
978
+ * Collect scalar output names for the job's `outputs:` mapping.
979
+ * GitHub Actions requires outputs to be declared at the job level for
980
+ * `needs.<job>.outputs.<name>` expressions to work.
981
+ */
982
+ function collectJobOutputs(step) {
983
+ const jobOutputs = {};
984
+ for (const op of step.operations) if (op.kind === "exportOutput") jobOutputs[op.name] = `\${{ steps.output.outputs.${op.name} }}`;
985
+ return jobOutputs;
986
+ }
987
+ /**
988
+ * Build the job's `steps` array, inserting a cache step after checkout when
989
+ * caching is enabled. Checkout is always the first step.
990
+ */
991
+ function resolveJobSteps(steps, step) {
992
+ if (step.cache === void 0) return steps;
993
+ return [
994
+ steps[0],
995
+ lowerCacheStep(step.cache),
996
+ ...steps.slice(1)
997
+ ];
998
+ }
999
+ /**
1000
+ * Resolve the job-level `if` expression.
1001
+ *
1002
+ * Rules take precedence over condition when both are present, matching
1003
+ * GitHub's behavior where workflow rules override step conditions. Multiple
1004
+ * rules are OR'd: GitHub's job-level `if` is a single expression, so we
1005
+ * combine all rule `if` conditions with `||`.
1006
+ */
1007
+ function resolveJobIf(step, jobIdMap) {
1008
+ if (step.rules !== void 0 && step.rules.length > 0) return { if: lowerRulesIf(step.rules) };
1009
+ if (step.condition !== void 0) return { if: lowerCondition(step.condition, jobIdMap) };
1010
+ return {};
1011
+ }
1012
+ /**
1013
+ * Resolve job-level permissions and environment for Pages deploys and
1014
+ * identity-token requests.
1015
+ *
1016
+ * GitHub Pages jobs need pages:write and id-token:write permissions.
1017
+ * Safe-outputs (Spec 25): steps with permissions.write get a scoped
1018
+ * permissions block from write kinds. Steps without writes get
1019
+ * permissions: {} (read-only). deployPages/identity take precedence.
1020
+ */
1021
+ const WRITE_KIND_TO_GHA_PERMISSION = {
1022
+ "pull-request": "pull-requests: write",
1023
+ "comment": "issues: write",
1024
+ "deploy": "deployments: write",
1025
+ "push": "contents: write",
1026
+ "id-token": "id-token: write",
1027
+ "pages": "pages: write"
1028
+ };
1029
+ function resolveJobPermissions(step) {
1030
+ if (step.operations.some((op) => op.kind === "deployPages")) return {
1031
+ permissions: {
1032
+ "pages": "write",
1033
+ "id-token": "write"
1034
+ },
1035
+ environment: { name: "github-pages" }
1036
+ };
1037
+ if (step.identity !== void 0) return { permissions: { "id-token": "write" } };
1038
+ if (step.permissions !== void 0) {
1039
+ const writes = step.permissions.write;
1040
+ if (writes && writes.length > 0) {
1041
+ const perms = {};
1042
+ for (const decl of writes) {
1043
+ const mapped = WRITE_KIND_TO_GHA_PERMISSION[decl.kind];
1044
+ if (mapped !== void 0) {
1045
+ const [scope, level] = mapped.split(": ");
1046
+ perms[scope] = level;
1047
+ } else perms["contents"] = "read";
1048
+ }
1049
+ return { permissions: perms };
1050
+ }
1051
+ return { permissions: {} };
1052
+ }
1053
+ return {};
1054
+ }
1055
+ /**
1056
+ * Assemble the final GithubJob object from its constituent parts.
1057
+ */
1058
+ function assembleGithubJob(parts) {
1059
+ const { jobId, steps, needs, step, runsOn, container, jobEnv, jobIdMap } = parts;
1060
+ const jobOutputs = collectJobOutputs(step);
1061
+ const jobIf = resolveJobIf(step, jobIdMap);
1062
+ const jobPermissions = resolveJobPermissions(step);
1063
+ return {
1064
+ id: jobId,
1065
+ name: jobId,
1066
+ runsOn,
1067
+ needs,
1068
+ ...Object.keys(jobOutputs).length > 0 ? { outputs: jobOutputs } : {},
1069
+ steps: resolveJobSteps(steps, step),
1070
+ ...step.timeout !== void 0 ? { timeoutMinutes: Math.ceil(step.timeout / 6e4) } : {},
1071
+ ...Object.keys(jobEnv).length > 0 ? { env: jobEnv } : {},
1072
+ ...container ? { container } : {},
1073
+ ...step.matrix !== void 0 ? { strategy: lowerStrategy(step.matrix) } : {},
1074
+ ...jobIf,
1075
+ ...jobPermissions,
1076
+ ...resolveJobServices(step),
1077
+ ...resolveJobEnvironment(step),
1078
+ ...step.concurrency !== void 0 ? { concurrency: step.concurrency } : {}
1079
+ };
1080
+ }
1081
+ /** Resolve job-level services field from step services. */
1082
+ function resolveJobServices(step) {
1083
+ if (step.services === void 0 || step.services.length === 0) return {};
1084
+ return { services: lowerServices(step.services) };
1085
+ }
1086
+ /** Resolve job-level environment field from step environment spec. */
1087
+ function resolveJobEnvironment(step) {
1088
+ if (step.environment === void 0) return {};
1089
+ return { environment: {
1090
+ name: step.environment.name,
1091
+ ...step.environment.url !== void 0 ? { url: step.environment.url } : {}
1092
+ } };
1093
+ }
1094
+ /**
1095
+ * Lower a cache spec to a GitHub cache step.
1096
+ * pull-push → actions/cache@v4
1097
+ * pull → actions/cache/restore@v4
1098
+ * push → actions/cache/save@v4
1099
+ */
1100
+ function lowerCacheStep(cache) {
1101
+ const policy = cache.policy ?? "pull-push";
1102
+ let action = "actions/cache@v4";
1103
+ if (policy === "pull") action = "actions/cache/restore@v4";
1104
+ else if (policy === "push") action = "actions/cache/save@v4";
1105
+ const withMap = {
1106
+ path: cache.paths.length === 1 ? cache.paths[0] : cache.paths.join("\n"),
1107
+ key: cache.key
1108
+ };
1109
+ if (cache.restoreKeys !== void 0 && cache.restoreKeys.length > 0) withMap["restore-keys"] = cache.restoreKeys.length === 1 ? cache.restoreKeys[0] : cache.restoreKeys.join("\n");
1110
+ return {
1111
+ name: "Restore cache",
1112
+ uses: action,
1113
+ with: withMap
1114
+ };
1115
+ }
1116
+ /**
1117
+ * Lower service containers to GitHub services map (keyed by name).
1118
+ */
1119
+ function lowerServices(services) {
1120
+ const result = {};
1121
+ for (const svc of services) {
1122
+ const options = [];
1123
+ if (svc.entrypoint !== void 0) options.push(`--entrypoint=${svc.entrypoint[0] ?? ""}`);
1124
+ const service = {
1125
+ image: svc.image,
1126
+ ...svc.env !== void 0 ? { env: { ...svc.env } } : {},
1127
+ ...svc.ports !== void 0 ? { ports: svc.ports.map((p) => `${p}:${p}`) } : {},
1128
+ ...options.length > 0 ? { options: options.join(" ") } : {}
1129
+ };
1130
+ result[svc.name] = service;
1131
+ }
1132
+ return result;
1133
+ }
1134
+ function resolveRunsOn(step) {
1135
+ if (step.runner === void 0) return "ubuntu-latest";
1136
+ const { labels, group } = step.runner;
1137
+ if (group !== void 0) return {
1138
+ group,
1139
+ labels
1140
+ };
1141
+ if (labels.length === 1) return labels[0];
1142
+ return labels;
1143
+ }
1144
+ /**
1145
+ * Parse a duration string (e.g. "5m", "30s", "1h") to seconds.
1146
+ * F-48: used for GitHub sleep emulation.
1147
+ */
1148
+ function parseDurationToSeconds(duration) {
1149
+ const match = /^(\d+)\s*(s|m|h|seconds?|minutes?|hours?)?$/i.exec(duration);
1150
+ if (!match) throw new GithubTargetError(`invalid delay duration '${duration}'`, "LOWER_FAILED");
1151
+ const value = Number.parseInt(match[1], 10);
1152
+ const unit = (match[2] ?? "s").toLowerCase();
1153
+ if (unit.startsWith("h")) return value * 3600;
1154
+ if (unit.startsWith("m")) return value * 60;
1155
+ return value;
1156
+ }
1157
+ function resolveContainer(step, mode) {
1158
+ if (mode === "container" && !step.runtime.image) throw new GithubTargetError(`step '${step.id}' uses container mode without an image`, "LOWER_FAILED");
1159
+ return mode === "container" ? step.runtime.image : void 0;
1160
+ }
1161
+ function collectJobEnv(runtime) {
1162
+ const jobEnv = {};
1163
+ if (runtime.env) Object.assign(jobEnv, runtime.env);
1164
+ if (runtime.secrets) for (const secret of runtime.secrets) jobEnv[secret] = `\${{ secrets.${secret} }}`;
1165
+ return jobEnv;
1166
+ }
1167
+ /**
1168
+ * Map dependencies to job needs.
1169
+ * All dependency kinds create needs (GitHub jobs can't share values without artifacts).
1170
+ */
1171
+ function lowerDependencies$1(deps, jobIdMap) {
1172
+ const needs = /* @__PURE__ */ new Set();
1173
+ for (const dep of deps) {
1174
+ const jobId = jobIdMap.get(dep.producer);
1175
+ if (!jobId) throw new GithubTargetError(`step depends on unknown producer '${dep.producer}'`, "INVALID_GRAPH");
1176
+ needs.add(jobId);
1177
+ }
1178
+ return [...needs];
1179
+ }
1180
+ /**
1181
+ * Map operations to GitHub steps in original order.
1182
+ * Consecutive shell/exportOutput operations are combined into one run step.
1183
+ */
1184
+ function lowerOperations$1(step, jobIdMap) {
1185
+ const steps = [];
1186
+ const shortStepId = step.id.includes("/") ? step.id.split("/").pop() : step.id;
1187
+ steps.push({
1188
+ name: "Checkout",
1189
+ uses: "actions/checkout@v4"
1190
+ });
1191
+ if (step.runtime.network && step.runtime.network.allowed.length > 0) steps.push({
1192
+ name: "Sverka network allowlist",
1193
+ run: `echo "# sverka:network-allowlist: ${step.runtime.network.allowed.join(",")}"`
1194
+ });
1195
+ if (step.beforeScript) for (const cmd of step.beforeScript) steps.push({ run: cmd });
1196
+ let runLines = [];
1197
+ let runHasOutput = false;
1198
+ let stdoutTargetIndex;
1199
+ let stdoutNames = [];
1200
+ let stdoutUploadNames = [];
1201
+ function sealStdoutCapture() {
1202
+ if (stdoutTargetIndex === void 0 || stdoutNames.length === 0) return;
1203
+ runLines[stdoutTargetIndex] = wrapStdoutCaptureLine(runLines[stdoutTargetIndex], stdoutNames);
1204
+ stdoutUploadNames.push(...stdoutNames);
1205
+ stdoutNames = [];
1206
+ }
1207
+ function flushRun() {
1208
+ sealStdoutCapture();
1209
+ if (runLines.length === 0) return;
1210
+ const translated = translateCommand(runLines.join("\n"), step.inputs, jobIdMap);
1211
+ steps.push({
1212
+ ...runHasOutput ? { id: "output" } : {},
1213
+ run: translated,
1214
+ ...step.runtime.workingDir ? { workingDirectory: step.runtime.workingDir } : {},
1215
+ ...step.runtime.shell ? { shell: step.runtime.shell } : {}
1216
+ });
1217
+ for (const name of stdoutUploadNames) steps.push({
1218
+ name: `Upload ${name}`,
1219
+ if: "always()",
1220
+ uses: "actions/upload-artifact@v4",
1221
+ with: {
1222
+ name: artifactName(shortStepId, name),
1223
+ path: step.runtime.workingDir ? `${step.runtime.workingDir}/${name}` : name
1224
+ }
1225
+ });
1226
+ runLines = [];
1227
+ runHasOutput = false;
1228
+ stdoutTargetIndex = void 0;
1229
+ stdoutUploadNames = [];
1230
+ }
1231
+ for (const op of step.operations) {
1232
+ if (op.kind === "exportOutput") runHasOutput = true;
1233
+ switch (op.kind) {
1234
+ case "shell":
1235
+ sealStdoutCapture();
1236
+ runLines.push(op.background ? `${op.command} &` : op.command);
1237
+ stdoutTargetIndex = runLines.length - 1;
1238
+ break;
1239
+ case "exportStdout":
1240
+ if (stdoutTargetIndex === void 0) throw new GithubTargetError(`step '${step.id}' declares stdout artifact '${op.name}' but no shell output was captured`, "LOWER_FAILED");
1241
+ if (step.runtime.shell !== void 0 && !isPosixShell(step.runtime.shell)) throw new GithubTargetError(`step '${step.id}' declares stdout artifact '${op.name}' but shell '${step.runtime.shell}' is not POSIX-compatible`, "LOWER_FAILED");
1242
+ stdoutNames.push(op.name);
1243
+ break;
1244
+ default: lowerOperation$1(op, shortStepId, steps, runLines, flushRun);
1245
+ }
1246
+ }
1247
+ flushRun();
1248
+ if (step.afterScript) for (const cmd of step.afterScript) steps.push({
1249
+ run: cmd,
1250
+ if: "always()"
1251
+ });
1252
+ return steps;
1253
+ }
1254
+ /** Shells whose run scripts understand POSIX syntax (brace groups, `||`, `[ ]`). */
1255
+ const POSIX_SHELLS = /* @__PURE__ */ new Set([
1256
+ "sh",
1257
+ "bash",
1258
+ "dash",
1259
+ "ash",
1260
+ "zsh",
1261
+ "ksh"
1262
+ ]);
1263
+ function isPosixShell(shell) {
1264
+ const space = shell.indexOf(" ");
1265
+ return POSIX_SHELLS.has(space === -1 ? shell : shell.slice(0, space));
1266
+ }
1267
+ function lowerOperation$1(op, shortStepId, steps, runLines, flushRun) {
1268
+ switch (op.kind) {
1269
+ case "exportOutput":
1270
+ runLines.push(`echo "${op.name}=\${${op.name}}" >> "$GITHUB_OUTPUT"`);
1271
+ break;
1272
+ case "exportArtifact":
1273
+ flushRun();
1274
+ steps.push({
1275
+ name: `Upload ${op.name}`,
1276
+ uses: "actions/upload-artifact@v4",
1277
+ with: {
1278
+ name: artifactName(shortStepId, op.name),
1279
+ path: op.path,
1280
+ ...op.retention !== void 0 ? { "retention-days": parseRetentionDays(op.retention) } : {}
1281
+ }
1282
+ });
1283
+ break;
1284
+ case "importArtifact":
1285
+ lowerImportArtifact(op, steps, flushRun);
1286
+ break;
1287
+ case "diagnostic":
1288
+ lowerDiagnostic(op, steps, flushRun);
1289
+ break;
1290
+ case "report":
1291
+ flushRun();
1292
+ steps.push(lowerReport$1(op.spec));
1293
+ break;
1294
+ case "release":
1295
+ lowerRelease(op, steps, flushRun);
1296
+ break;
1297
+ case "deployPages":
1298
+ lowerDeployPages$1(op, steps, flushRun);
1299
+ break;
1300
+ default: throw new GithubTargetError(`unsupported operation kind: ${JSON.stringify(op.kind)}`, "LOWER_FAILED");
1301
+ }
1302
+ }
1303
+ /**
1304
+ * Lower a release operation to a GitHub release step.
1305
+ * F-39: uses softprops/action-gh-release@v2.
1306
+ */
1307
+ function lowerRelease(op, steps, flushRun) {
1308
+ flushRun();
1309
+ const withMap = { tag_name: op.tag };
1310
+ if (op.name) withMap.name = op.name;
1311
+ if (op.description) withMap.body = op.description;
1312
+ if (op.assets && op.assets.length > 0) withMap.files = op.assets.join("\n");
1313
+ if (op.draft !== void 0) withMap.draft = op.draft;
1314
+ if (op.prerelease !== void 0) withMap.prerelease = op.prerelease;
1315
+ steps.push({
1316
+ name: `Release ${op.tag}`,
1317
+ uses: "softprops/action-gh-release@v2",
1318
+ with: withMap
1319
+ });
1320
+ }
1321
+ /**
1322
+ * Lower a deployPages operation to GitHub Pages deployment steps.
1323
+ * F-40: uses actions/upload-pages-artifact + actions/deploy-pages.
1324
+ */
1325
+ function lowerDeployPages$1(op, steps, flushRun) {
1326
+ flushRun();
1327
+ steps.push({
1328
+ name: "Upload Pages artifact",
1329
+ uses: "actions/upload-pages-artifact@v3",
1330
+ with: { path: op.path }
1331
+ }, {
1332
+ name: "Deploy to GitHub Pages",
1333
+ uses: "actions/deploy-pages@v4",
1334
+ id: "deployment"
1335
+ });
1336
+ }
1337
+ function lowerImportArtifact(op, steps, flushRun) {
1338
+ flushRun();
1339
+ const fromShort = op.from.includes("/") ? op.from.split("/").pop() : op.from;
1340
+ steps.push({
1341
+ name: `Download ${op.output}`,
1342
+ uses: "actions/download-artifact@v4",
1343
+ with: {
1344
+ name: artifactName(fromShort, op.output),
1345
+ path: op.output
1346
+ }
1347
+ });
1348
+ }
1349
+ function lowerDiagnostic(op, steps, flushRun) {
1350
+ flushRun();
1351
+ const severityFlag = severityFlagFor(op.severity);
1352
+ const escapedMessage = op.message.replace(/%/g, "%25").replace(/\r\n/g, "%0D%0A").replace(/\n/g, "%0A").replace(/\r/g, "%0D");
1353
+ steps.push({
1354
+ env: { SVERKA_DIAGNOSTIC_MESSAGE: escapedMessage },
1355
+ run: String.raw`printf '%s\n' "::${severityFlag}::$SVERKA_DIAGNOSTIC_MESSAGE"`
1356
+ });
1357
+ }
1358
+ /**
1359
+ * Generate a deterministic artifact name from step ID and output name.
1360
+ */
1361
+ function artifactName(stepId, outputName) {
1362
+ return `${stepId}-${outputName}`;
1363
+ }
1364
+ /**
1365
+ * Parse a duration string (e.g., "7d", "1h", "30m", "never") into days.
1366
+ * Returns undefined for "never" (no retention limit).
1367
+ */
1368
+ function parseRetentionDays(retention) {
1369
+ if (retention === "never") return void 0;
1370
+ const match = /^(\d+)([dhm])$/.exec(retention);
1371
+ if (match === null) return void 0;
1372
+ const value = Number.parseInt(match[1], 10);
1373
+ const unit = match[2];
1374
+ if (unit === "d") return value;
1375
+ if (unit === "h") return Math.ceil(value / 24);
1376
+ if (unit === "m") return Math.ceil(value / 1440);
1377
+ }
1378
+ /**
1379
+ * Lower a report spec to a GitHub action step.
1380
+ * Maps report types to known GitHub actions.
1381
+ */
1382
+ function lowerReport$1(spec) {
1383
+ switch (spec.type) {
1384
+ case "junit": return {
1385
+ name: `Report ${spec.type}`,
1386
+ uses: "dorny/test-reporter@v1",
1387
+ with: {
1388
+ name: "Tests",
1389
+ path: spec.path,
1390
+ reporter: "java-junit"
1391
+ }
1392
+ };
1393
+ case "sarif":
1394
+ case "sast": return {
1395
+ name: `Upload ${spec.type}`,
1396
+ uses: "github/codeql-action/upload-sarif@v3",
1397
+ with: { sarif_file: spec.path }
1398
+ };
1399
+ default: return {
1400
+ name: `Upload ${spec.type} report`,
1401
+ uses: "actions/upload-artifact@v4",
1402
+ with: {
1403
+ name: `${spec.type}-report`,
1404
+ path: spec.path
1405
+ }
1406
+ };
1407
+ }
1408
+ }
1409
+ /**
1410
+ * Collect pipeline-level env vars from inputs.
1411
+ * Secret inputs are referenced via `${{ secrets.<name> }}` instead of
1412
+ * literal defaults.
1413
+ */
1414
+ function collectEnv(pipeline) {
1415
+ const env = {};
1416
+ for (const [name, input] of Object.entries(pipeline.inputs)) if (input.secret) env[name] = `\${{ secrets.${name} }}`;
1417
+ else if (input.default !== void 0) env[name] = String(input.default);
1418
+ return env;
1419
+ }
1420
+ function severityFlagFor(severity) {
1421
+ if (severity === "error") return "error";
1422
+ if (severity === "warn") return "warning";
1423
+ return "notice";
1424
+ }
1425
+ /**
1426
+ * Lower a MatrixSpec to a GitHub strategy object.
1427
+ * dimensions → matrix variables, include → include:, exclude → exclude:.
1428
+ * failFast → fail-fast, maxParallel → max-parallel.
1429
+ */
1430
+ function lowerStrategy(spec) {
1431
+ const matrix = {};
1432
+ for (const [key, values] of Object.entries(spec.dimensions)) matrix[key] = [...values];
1433
+ if (spec.include && spec.include.length > 0) matrix.include = spec.include.map((entry) => ({ ...entry }));
1434
+ if (spec.exclude && spec.exclude.length > 0) matrix.exclude = spec.exclude.map((entry) => ({ ...entry }));
1435
+ return {
1436
+ matrix,
1437
+ ...spec.failFast !== void 0 ? { failFast: spec.failFast } : {},
1438
+ ...spec.maxParallel !== void 0 ? (() => {
1439
+ if (!Number.isInteger(spec.maxParallel) || spec.maxParallel <= 0) throw new GithubTargetError(`maxParallel must be a positive integer, got ${spec.maxParallel}`, "INVALID_MATRIX");
1440
+ return { maxParallel: spec.maxParallel };
1441
+ })() : {}
1442
+ };
1443
+ }
1444
+ const GITHUB_CONTEXT_MAP = {
1445
+ "git.sha": "github.sha",
1446
+ "git.branch": "github.ref_name",
1447
+ "git.tag": "github.ref_name",
1448
+ "change.id": "github.event.pull_request.number",
1449
+ "change.source": "github.event_name",
1450
+ "change.target": "github.base_ref",
1451
+ "change.draft": "github.event.pull_request.draft",
1452
+ "event.type": "github.event_name",
1453
+ "run.id": "github.run_id",
1454
+ "run.attempt": "github.run_attempt"
1455
+ };
1456
+ /**
1457
+ * Translate a context ref (namespace.field) to GitHub expression syntax.
1458
+ * Secrets are sink-specific:
1459
+ * - In `with:` inputs (`forWithInput = true`): `${{ secrets.FIELD }}`.
1460
+ * GitHub Actions does not expand `$FIELD` in `with:` values — it treats it
1461
+ * as a literal string. The `secrets` context is the correct syntax here.
1462
+ * - In `run:` commands (`forWithInput = false`): `$FIELD` (env var, avoids
1463
+ * exposing secret values in run logs).
1464
+ * In conditions, secrets are not supported by GitHub's if: context and are
1465
+ * emitted as env var references instead.
1466
+ */
1467
+ function translateContextRef(namespace, field, inCondition = false, forWithInput = false) {
1468
+ const key = `${namespace}.${field}`;
1469
+ const mapped = GITHUB_CONTEXT_MAP[key];
1470
+ if (mapped) return `\${{ ${mapped} }}`;
1471
+ if (namespace === "env") return `\${{ env.${field} }}`;
1472
+ if (namespace === "matrix") return `\${{ matrix.${field} }}`;
1473
+ if (namespace === "inputs") return `\${{ env.${field} }}`;
1474
+ if (namespace === "secrets") {
1475
+ if (inCondition) return `\${{ env.${field} }}`;
1476
+ if (forWithInput) return `\${{ secrets.${field} }}`;
1477
+ return `$${field}`;
1478
+ }
1479
+ throw new GithubTargetError(`unsupported context namespace '${namespace}' in GitHub lowering`, "LOWER_FAILED");
1480
+ }
1481
+ /**
1482
+ * Translate a step ref to GitHub needs.<jobId>.outputs.<output> syntax.
1483
+ * Each Sverka step is lowered to a separate GitHub job, so cross-step
1484
+ * references must use the 'needs' context, not 'steps'.
1485
+ */
1486
+ function translateStepRef(ref, jobIdMap) {
1487
+ return `\${{ needs.${jobIdMap.get(ref.step) ?? ref.step}.outputs.${ref.output} }}`;
1488
+ }
1489
+ /**
1490
+ * Build a lookup map from placeholder key ("namespace.field" or "step.output")
1491
+ * to the Reference from the step's inputs array.
1492
+ */
1493
+ function buildInputLookup(inputs) {
1494
+ const map = /* @__PURE__ */ new Map();
1495
+ for (const ref of inputs) if (ref.kind === "context") map.set(`${ref.namespace}.${ref.field}`, ref);
1496
+ else if (ref.kind === "step") map.set(`${ref.step}.${ref.output}`, ref);
1497
+ return map;
1498
+ }
1499
+ /**
1500
+ * Translate ${...} placeholders in a command string to GitHub expression syntax.
1501
+ * Placeholders matching a known input ref are translated; others are left as-is.
1502
+ */
1503
+ function translateCommand(command, inputs, jobIdMap) {
1504
+ const lookup = buildInputLookup(inputs);
1505
+ return command.replace(/\$\{([^{}]+)\}/g, (_, key) => {
1506
+ const ref = lookup.get(key);
1507
+ if (ref === void 0) return `\${${key}}`;
1508
+ if (ref.kind === "context") return translateContextRef(ref.namespace, ref.field, false);
1509
+ return translateStepRef(ref, jobIdMap);
1510
+ });
1511
+ }
1512
+ /**
1513
+ * Lower step rules to a single GitHub `if:` expression.
1514
+ * Multiple rules are OR'd with `||`. Rules with `when: never` produce `false`.
1515
+ * Rules without an `if` but with `when: always` produce `true`.
1516
+ */
1517
+ function lowerRulesIf(rules) {
1518
+ const parts = [];
1519
+ for (const rule of rules) if (rule.when === "never") parts.push("${{ false }}");
1520
+ else if (rule.if !== void 0) parts.push(rule.if);
1521
+ else if (rule.when === "always") parts.push("${{ true }}");
1522
+ if (parts.length === 0) return "${{ true }}";
1523
+ if (parts.length === 1) return parts[0];
1524
+ return parts.map((p) => p.replace(/^\$\{\{|\}\}$/g, "").trim()).join(" || ");
1525
+ }
1526
+ /**
1527
+ * Lower a step condition to a GitHub if: expression.
1528
+ */
1529
+ function lowerCondition(condition, jobIdMap) {
1530
+ if (condition.kind === "status") {
1531
+ if (condition.status === "always") return "${{ always() }}";
1532
+ if (condition.status === "never") return "${{ false }}";
1533
+ if (condition.status === "failure") return "${{ failure() }}";
1534
+ return "${{ success() }}";
1535
+ }
1536
+ if (condition.kind === "context") return `\${{ ${stripBraces(translateContextRef(condition.namespace, condition.field, true))} }}`;
1537
+ if (condition.kind === "step") return translateStepRef(condition, jobIdMap);
1538
+ const lookup = buildInputLookup(condition.refs);
1539
+ return `\${{ ${condition.template.replace(/\$\{([^{}]+)\}/g, (_, key) => {
1540
+ const ref = lookup.get(key);
1541
+ if (ref === void 0) return `\${${key}}`;
1542
+ if (ref.kind === "context") return stripBraces(translateContextRef(ref.namespace, ref.field, true));
1543
+ return stripBraces(translateStepRef(ref, jobIdMap));
1544
+ })} }}`;
1545
+ }
1546
+ /** Strip the `${{ ... }}` wrapper, returning the inner expression. */
1547
+ function stripBraces(s) {
1548
+ if (!s.startsWith("${{") || !s.endsWith("}}")) return s;
1549
+ return s.slice(3, -2).trim();
1550
+ }
1551
+ //#endregion
1552
+ //#region src/github/emit.ts
1553
+ /**
1554
+ * Emit one or more GithubTargetGraphs as YAML artifacts.
1555
+ * Produces one .github/workflows/<name>.yml file per target graph.
1556
+ */
1557
+ function emitGithub(targetGraph, options) {
1558
+ return (Array.isArray(targetGraph) ? targetGraph : [targetGraph]).map((g) => ({
1559
+ path: `.github/workflows/${sanitizeWorkflowName(g.name)}.yml`,
1560
+ content: stringifyTargetGraph$1(g, options)
1561
+ }));
1562
+ }
1563
+ /**
1564
+ * Sanitize a graph name for use as a workflow filename.
1565
+ * GitHub workflow filenames must be valid path segments; replace
1566
+ * characters that could break path resolution.
1567
+ */
1568
+ function sanitizeWorkflowName(name) {
1569
+ return name.replace(/[^a-zA-Z0-9._-]/g, "-");
1570
+ }
1571
+ /**
1572
+ * Convert a GithubTargetGraph to a YAML string.
1573
+ */
1574
+ function stringifyTargetGraph$1(graph, options) {
1575
+ const doc = {
1576
+ name: graph.name,
1577
+ on: graph.on
1578
+ };
1579
+ if (Object.keys(graph.env).length > 0) doc.env = graph.env;
1580
+ if (graph.permissions) doc.permissions = graph.permissions;
1581
+ if (graph.defaults) doc.defaults = graph.defaults;
1582
+ if (graph.concurrency) doc.concurrency = concurrencyToYaml(graph.concurrency);
1583
+ const jobs = {};
1584
+ for (const job of graph.jobs) jobs[job.id] = jobToYaml$1(job, options);
1585
+ doc.jobs = jobs;
1586
+ return stringify(doc, { sortMapEntries: false });
1587
+ }
1588
+ /**
1589
+ * Convert a GithubJob to a YAML-compatible object.
1590
+ */
1591
+ function jobToYaml$1(job, options) {
1592
+ const result = { name: job.name };
1593
+ if (job.uses) return reusableJobToYaml(job, result, options);
1594
+ result["runs-on"] = job.runsOn;
1595
+ if (job.needs.length > 0) result.needs = job.needs.length === 1 ? job.needs[0] : [...job.needs];
1596
+ if (job.timeoutMinutes !== void 0) result["timeout-minutes"] = job.timeoutMinutes;
1597
+ if (job.container) result.container = job.container;
1598
+ if (job.env) result.env = job.env;
1599
+ if (job.strategy) result.strategy = strategyToYaml(job.strategy);
1600
+ if (job.permissions) result.permissions = job.permissions;
1601
+ if (job.if) result.if = job.if;
1602
+ if (job.services) result.services = job.services;
1603
+ if (job.environment) result.environment = job.environment;
1604
+ if (job.concurrency) result.concurrency = concurrencyToYaml(job.concurrency);
1605
+ result.steps = job.steps.map((step, i) => stepToYaml(step, i, options));
1606
+ return result;
1607
+ }
1608
+ /**
1609
+ * Convert a job strategy spec to a YAML-compatible object.
1610
+ */
1611
+ function strategyToYaml(strategy) {
1612
+ const strat = { matrix: strategy.matrix };
1613
+ if (strategy.failFast !== void 0) strat["fail-fast"] = strategy.failFast;
1614
+ if (strategy.maxParallel !== void 0) strat["max-parallel"] = strategy.maxParallel;
1615
+ return strat;
1616
+ }
1617
+ /**
1618
+ * Convert a reusable workflow call job (uses) to a YAML-compatible object.
1619
+ */
1620
+ function reusableJobToYaml(job, result, options) {
1621
+ result.uses = options?.pinUses ? options.pinUses(job.uses) : job.uses;
1622
+ if (job.needs.length > 0) result.needs = job.needs.length === 1 ? job.needs[0] : [...job.needs];
1623
+ if (job.with && Object.keys(job.with).length > 0) result.with = job.with;
1624
+ if (job.secrets) result.secrets = job.secrets;
1625
+ return result;
1626
+ }
1627
+ /**
1628
+ * Convert a concurrency spec to a YAML-compatible object.
1629
+ */
1630
+ function concurrencyToYaml(conc) {
1631
+ const result = { group: conc.group };
1632
+ if (conc.cancelInProgress !== void 0) result["cancel-in-progress"] = conc.cancelInProgress;
1633
+ return result;
1634
+ }
1635
+ /**
1636
+ * Convert a GithubStep to a YAML-compatible object.
1637
+ */
1638
+ function stepToYaml(step, index, options) {
1639
+ const result = {};
1640
+ if (step.id) result.id = step.id;
1641
+ if (step.name) result.name = step.name;
1642
+ else if (!step.uses && !step.run) result.name = `Step ${index + 1}`;
1643
+ if (step.uses) result.uses = options?.pinUses ? options.pinUses(step.uses) : step.uses;
1644
+ if (step.run) result.run = step.run;
1645
+ if (step.with) result.with = step.with;
1646
+ if (step.env) result.env = step.env;
1647
+ if (step.workingDirectory) result["working-directory"] = step.workingDirectory;
1648
+ if (step.shell) result.shell = step.shell;
1649
+ if (step.if) result.if = step.if;
1650
+ if (step.continueOnError !== void 0) result["continue-on-error"] = step.continueOnError;
1651
+ return result;
1652
+ }
1653
+ //#endregion
1654
+ //#region src/github/pinned-actions.json
1655
+ var pinned_actions_default = {
1656
+ "actions/checkout@v4": "11d5960a326750d5838078e36cf38b85af677262",
1657
+ "actions/setup-node@v4": "49933ea5288caeca8642d1e84afbd3f7d6820020",
1658
+ "actions/upload-artifact@v4": "ea165f8d65b6e75b540449e92b4886f43607fa02",
1659
+ "actions/download-artifact@v4": "d3f86a106a0bac45b974a628896c90dbdf5c8093",
1660
+ "actions/cache@v4": "0057852bfaa89a56745cba8c7296529d2fc39830",
1661
+ "actions/cache/restore@v4": "0057852bfaa89a56745cba8c7296529d2fc39830",
1662
+ "actions/cache/save@v4": "0057852bfaa89a56745cba8c7296529d2fc39830",
1663
+ "oven-sh/setup-bun@v2": "0c5077e51419868618aeaa5fe8019c62421857d6",
1664
+ "softprops/action-gh-release@v2": "3bb12739c298aeb8a4eeaf626c5b8d85266b0e65",
1665
+ "actions/upload-pages-artifact@v3": "56afc609e74202658d3ffba0e8f6dda462b719fa",
1666
+ "actions/deploy-pages@v4": "d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e",
1667
+ "dorny/test-reporter@v1": "3eeb9fc888e82e8be2fb356bbeec2750231672bc",
1668
+ "github/codeql-action/upload-sarif@v3": "6f5948dfacef28e207b48d0905cf90c03365536d"
1669
+ };
1670
+ //#endregion
1671
+ //#region src/github/pinning.ts
1672
+ const SHA_RE$1 = /^[0-9a-f]{40}$/;
1673
+ /**
1674
+ * Pin a single `uses:` ref to its commit SHA.
1675
+ *
1676
+ * Rules (spec 22 §"Data models"):
1677
+ * 1. Local action (`./…`) → unchanged.
1678
+ * 2. Already pinned (`@<40 hex>`) → unchanged.
1679
+ * 3. `org/name@vN` in registry → `org/name@<sha> # vN`.
1680
+ * 4. `org/name@vN` not in registry → unchanged (caller emits a diagnostic).
1681
+ *
1682
+ * Refs without `@` (malformed) are returned unchanged.
1683
+ */
1684
+ function pinActionRef(ref, registry) {
1685
+ if (ref.startsWith("./")) return ref;
1686
+ const at = ref.lastIndexOf("@");
1687
+ if (at < 0) return ref;
1688
+ const head = ref.slice(0, at);
1689
+ const tail = ref.slice(at + 1);
1690
+ if (SHA_RE$1.test(tail)) return ref;
1691
+ const sha = registry[ref];
1692
+ if (!sha) return ref;
1693
+ return `${head}@${sha} # ${tail}`;
1694
+ }
1695
+ /**
1696
+ * Load the bundled `pinned-actions.json` registry.
1697
+ */
1698
+ function loadBundledRegistry() {
1699
+ return pinned_actions_default;
1700
+ }
1701
+ //#endregion
1702
+ //#region src/github/target.ts
1703
+ const SHA_RE = /^[0-9a-f]{40}$/;
1704
+ /**
1705
+ * GitHub Actions target — native lowering from Definition Graph.
1706
+ * Implements the Target contract: analyze → lower → emit.
1707
+ */
1708
+ var GithubTarget = class {
1709
+ name = "github";
1710
+ capabilities = githubCapabilities;
1711
+ pinningMode;
1712
+ pinningRegistry;
1713
+ constructor(config) {
1714
+ const pinning = config?.pinning;
1715
+ this.pinningMode = pinning?.mode ?? "off";
1716
+ this.pinningRegistry = pinning?.registry ?? loadBundledRegistry();
1717
+ }
1718
+ /**
1719
+ * Analyze a Definition Graph against the GitHub capability manifest.
1720
+ * Returns diagnostics for unsupported or emulated capabilities, plus
1721
+ * `unpinned-action` diagnostics (spec 22) for third-party `uses:` refs
1722
+ * that are not pinned in the emitted output.
1723
+ */
1724
+ analyze(graph) {
1725
+ const capDiags = analyzeCapabilities(graph, [this.capabilities]);
1726
+ const targetGraph = this.lower(graph);
1727
+ const pinDiags = this.pinningDiagnostics(targetGraph);
1728
+ return [...capDiags, ...pinDiags];
1729
+ }
1730
+ /**
1731
+ * Lower a Definition Graph to one or more GithubTargetGraphs.
1732
+ * Single-pipeline graphs return one target graph; multi-pipeline graphs
1733
+ * with reusable workflow calls return one per pipeline.
1734
+ */
1735
+ lower(graph) {
1736
+ return lowerGithub(graph);
1737
+ }
1738
+ /**
1739
+ * Emit one or more GithubTargetGraphs as YAML artifacts.
1740
+ * Produces .github/workflows/<name>.yml files. When pinning is on (strict),
1741
+ * every `uses:` ref is routed through `pinActionRef`.
1742
+ */
1743
+ emit(targetGraph) {
1744
+ return emitGithub(targetGraph, this.pinningMode === "strict" ? { pinUses: (ref) => pinActionRef(ref, this.pinningRegistry) } : void 0);
1745
+ }
1746
+ /**
1747
+ * Compile a Definition Graph to generated artifacts and diagnostics.
1748
+ */
1749
+ compile(graph) {
1750
+ const targetGraph = this.lower(graph);
1751
+ const capDiags = analyzeCapabilities(graph, [this.capabilities]);
1752
+ const pinDiags = this.pinningDiagnostics(targetGraph);
1753
+ return {
1754
+ artifacts: this.emit(targetGraph),
1755
+ diagnostics: [...capDiags, ...pinDiags]
1756
+ };
1757
+ }
1758
+ /**
1759
+ * Scan a lowered target graph for third-party `uses:` refs and emit
1760
+ * `unpinned-action` diagnostics (spec 22). A ref is "unpinned" when it
1761
+ * remains on a mutable version tag in the emitted output:
1762
+ * - strict mode + missing registry entry → error
1763
+ * - off mode + missing registry entry → warning
1764
+ * - malformed ref (no `@`) → warning (both modes)
1765
+ * Refs in the registry produce no diagnostic (they are pinnable; strict
1766
+ * pins them, off leaves them unchanged by choice).
1767
+ */
1768
+ pinningDiagnostics(targetGraph) {
1769
+ const diags = [];
1770
+ const graphs = Array.isArray(targetGraph) ? targetGraph : [targetGraph];
1771
+ const seen = /* @__PURE__ */ new Set();
1772
+ for (const g of graphs) for (const job of g.jobs) for (const ref of collectUsesRefs(job)) {
1773
+ if (seen.has(ref)) continue;
1774
+ seen.add(ref);
1775
+ const diag = this.diagnosticFor(ref, job.id);
1776
+ if (diag) diags.push(diag);
1777
+ }
1778
+ return diags;
1779
+ }
1780
+ diagnosticFor(ref, jobId) {
1781
+ if (ref.startsWith("./")) return void 0;
1782
+ const at = ref.lastIndexOf("@");
1783
+ if (at < 0) return {
1784
+ capability: "unpinned-action",
1785
+ support: "unsupported",
1786
+ severity: "warning",
1787
+ message: `Action ref "${ref}" is malformed (no @version) and cannot be pinned.`,
1788
+ stepId: jobId
1789
+ };
1790
+ const tail = ref.slice(at + 1);
1791
+ if (SHA_RE.test(tail)) return void 0;
1792
+ if (this.pinningRegistry[ref]) return void 0;
1793
+ return {
1794
+ capability: "unpinned-action",
1795
+ support: "unsupported",
1796
+ severity: this.pinningMode === "strict" ? "error" : "warning",
1797
+ message: `Action "${ref}" is not pinned to a commit SHA (no registry entry).`,
1798
+ stepId: jobId
1799
+ };
1800
+ }
1801
+ };
1802
+ /** Collect every `uses:` ref from a job (step-level + reusable-job-level). */
1803
+ function collectUsesRefs(job) {
1804
+ const refs = [];
1805
+ if (job.uses) refs.push(job.uses);
1806
+ for (const step of job.steps) if (step.uses) refs.push(step.uses);
1807
+ return refs;
1808
+ }
1809
+ /**
1810
+ * Convenience function: analyze → lower → emit in one call.
1811
+ */
1812
+ function compileGithub(graph) {
1813
+ return new GithubTarget().compile(graph);
1814
+ }
1815
+ //#endregion
1816
+ //#region src/gitlab/capabilities.ts
1817
+ const gitlabCapabilities = {
1818
+ "trigger.push": "native",
1819
+ "trigger.changeRequest": "native",
1820
+ "trigger.manual": "native",
1821
+ "runtime.host": "native",
1822
+ "runtime.container": "native",
1823
+ "operation.shell": "native",
1824
+ "operation.import": "lowered",
1825
+ "output.scalar": "lowered",
1826
+ "output.artifact": "native",
1827
+ "graph.dependencies": "native",
1828
+ "graph.conditions": "partial",
1829
+ "graph.matrix": "native",
1830
+ "matrix.include": "lowered",
1831
+ "matrix.exclude": "emulated",
1832
+ "matrix.failFast": "unsupported",
1833
+ "matrix.maxParallel": "unsupported",
1834
+ "trigger.schedule": "native",
1835
+ "step.beforeScript": "native",
1836
+ "step.afterScript": "native",
1837
+ "step.continueOnError": "native",
1838
+ "policy.retry": "native",
1839
+ "policy.compensation": "emulated",
1840
+ "execution.workdir": "emulated",
1841
+ "execution.shell": "unsupported",
1842
+ "environment.variables": "native",
1843
+ "secrets.runtime": "native",
1844
+ "secrets.pipeline-input": "native",
1845
+ "concurrency.interruptible": "native",
1846
+ "environment.permissions": "unsupported",
1847
+ "runner.selection": "native",
1848
+ "runner.group": "unsupported",
1849
+ "secrets.oidc": "native",
1850
+ "secrets.oidc.multiAudience": "native",
1851
+ "workflow.rules": "native",
1852
+ "workflow.rules.changes": "native",
1853
+ "workflow.rules.exists": "native",
1854
+ "workflow.defaults": "native",
1855
+ "workflow.defaults.shell": "unsupported",
1856
+ "workflow.defaults.workdir": "unsupported",
1857
+ "workflow.defaults.env": "unsupported",
1858
+ "workflow.defaults.beforeScript": "native",
1859
+ "workflow.defaults.afterScript": "native",
1860
+ "workflow.defaults.timeout": "native",
1861
+ "workflow.defaults.retry": "native",
1862
+ "workflow.defaults.interruptible": "native",
1863
+ "workflow.inputs": "native",
1864
+ "workflow.inputs.choice": "native",
1865
+ "workflow.inputs.array": "native",
1866
+ "workflow.inputs.pattern": "native",
1867
+ "environment.services": "native",
1868
+ "environment.services.ports": "unsupported",
1869
+ "deployment.environment": "native",
1870
+ "deployment.environment.action": "native",
1871
+ "deployment.environment.tier": "native",
1872
+ "artifact.retention": "native",
1873
+ "artifact.access": "native",
1874
+ "cache": "native",
1875
+ "cache.policy": "native",
1876
+ "cache.fallbackKeys": "native",
1877
+ "concurrency.group": "native",
1878
+ "concurrency.cancelInProgress": "unsupported",
1879
+ "reusable.pipeline": "lowered",
1880
+ "reusable.pipeline.inputs": "native",
1881
+ "reusable.pipeline.outputs": "native",
1882
+ "reusable.component": "native",
1883
+ "reusable.component.versioning": "native",
1884
+ "reusable.childPipeline": "native",
1885
+ "reusable.downstream": "native",
1886
+ "deployment.release": "native",
1887
+ "deployment.pages": "native",
1888
+ "import.gitlab": "native",
1889
+ "import.include": "native",
1890
+ "scheduling.delay": "native",
1891
+ "execution.background": "emulated",
1892
+ "runtime.network": "emulated",
1893
+ "step.permissions": "native",
1894
+ "agent.step": "emulated",
1895
+ "run.query": "emulated"
1896
+ };
1897
+ //#endregion
1898
+ //#region src/gitlab/errors.ts
1899
+ var GitlabTargetError = class extends Error {
1900
+ code;
1901
+ cause;
1902
+ constructor(message, code, cause) {
1903
+ super(message);
1904
+ this.name = "GitlabTargetError";
1905
+ this.code = code;
1906
+ this.cause = cause;
1907
+ }
1908
+ };
1909
+ //#endregion
1910
+ //#region src/gitlab/lower.ts
1911
+ const DOTENV_REPORT_FILE = "sverka.env";
1912
+ /**
1913
+ * Lower a Definition Graph to a GitlabTargetGraph.
1914
+ * One GitLab job per reachable Step. Stages derived from dependency depth.
1915
+ * F-31: pipeline-call steps are inlined as namespaced jobs (v1; include:
1916
+ * file-reuse deferred).
1917
+ */
1918
+ function lowerGitlab(graph) {
1919
+ if (graph.project.pipelines.length === 0) throw new GitlabTargetError("graph has no pipelines", "INVALID_GRAPH");
1920
+ const rootPipelines = graph.project.pipelines.filter((p) => p.entries.length > 0);
1921
+ if (rootPipelines.length === 0) throw new GitlabTargetError("graph has no root pipelines (with entries)", "INVALID_GRAPH");
1922
+ if (rootPipelines.length > 1) {
1923
+ const dropped = rootPipelines.slice(1).map((p) => p.id);
1924
+ console.warn(`GitLab lowering: dropping ${dropped.length} additional root pipeline(s): ${dropped.join(", ")}. Multi-root GitLab support is not yet implemented.`);
1925
+ }
1926
+ const pipeline = rootPipelines[0];
1927
+ const reachableSteps = filterReachableSteps(pipeline);
1928
+ const expandedSteps = expandPipelineCalls(graph, reachableSteps);
1929
+ const includes = [];
1930
+ const stepsForJobs = expandedSteps.filter((step) => {
1931
+ if (step.component) {
1932
+ includes.push(lowerComponentInclude(step.component));
1933
+ return false;
1934
+ }
1935
+ return true;
1936
+ });
1937
+ const jobIdMap = buildJobIdMap(stepsForJobs);
1938
+ const { stageMap, stages } = computeStages(stepsForJobs, jobIdMap);
1939
+ const jobs = lowerSteps(stepsForJobs, jobIdMap, stageMap, buildJobRulesMap(pipeline, stepsForJobs, jobIdMap));
1940
+ const workflowRules = lowerWorkflowRules(pipeline);
1941
+ const localIncludes = lowerLocalIncludes(pipeline);
1942
+ if (pipeline.concurrency !== void 0) {
1943
+ const group = pipeline.concurrency.group;
1944
+ for (const job of jobs) if (job.resourceGroup === void 0) job.resourceGroup = group;
1945
+ }
1946
+ const autoCancel = jobs.some((job) => job.interruptible === true);
1947
+ return {
1948
+ name: pipeline.id,
1949
+ stages,
1950
+ jobs,
1951
+ variables: collectVariables(pipeline),
1952
+ ...autoCancel ? { autoCancel: true } : {},
1953
+ ...pipeline.defaults !== void 0 ? { default: lowerDefault(pipeline.defaults) } : {},
1954
+ ...Object.keys(pipeline.inputs).length > 0 ? { specInputs: lowerSpecInputs(pipeline.inputs) } : {},
1955
+ includes,
1956
+ ...localIncludes.length > 0 ? { localIncludes } : {},
1957
+ ...workflowRules.length > 0 ? { workflowRules } : {}
1958
+ };
1959
+ }
1960
+ /**
1961
+ * Lower pipeline inputs to GitLab spec:inputs.
1962
+ * choice type → type: string with options.
1963
+ */
1964
+ function lowerSpecInputs(inputs) {
1965
+ const result = {};
1966
+ for (const [name, input] of Object.entries(inputs)) result[name] = {
1967
+ type: input.type === "choice" ? "string" : input.type,
1968
+ ...input.description !== void 0 ? { description: input.description } : {},
1969
+ ...input.default !== void 0 && !input.secret ? { default: input.default } : {},
1970
+ ...input.options !== void 0 ? { options: input.options } : {},
1971
+ ...input.pattern !== void 0 ? { regex: input.pattern } : {}
1972
+ };
1973
+ return result;
1974
+ }
1975
+ /**
1976
+ * Lower pipeline defaults to GitLab default keyword.
1977
+ * shell/workdir are not supported by GitLab and are dropped.
1978
+ */
1979
+ function lowerDefault(defaults) {
1980
+ return {
1981
+ ...defaults.beforeScript !== void 0 ? { beforeScript: defaults.beforeScript } : {},
1982
+ ...defaults.afterScript !== void 0 ? { afterScript: defaults.afterScript } : {},
1983
+ ...defaults.timeout !== void 0 ? { timeout: `${Math.ceil(defaults.timeout / 6e4)}m` } : {},
1984
+ ...defaults.retry !== void 0 ? { retry: defaults.retry } : {},
1985
+ ...defaults.interruptible !== void 0 ? { interruptible: defaults.interruptible } : {}
1986
+ };
1987
+ }
1988
+ /**
1989
+ * Lower pipeline-level includes to GitLab local include directives.
1990
+ * F-44: includes → include: - local: <path>
1991
+ */
1992
+ function lowerLocalIncludes(pipeline) {
1993
+ if (!pipeline.includes || pipeline.includes.length === 0) return [];
1994
+ return pipeline.includes.map((inc) => ({
1995
+ local: inc.path,
1996
+ ...inc.inputs ? { inputs: { ...inc.inputs } } : {}
1997
+ }));
1998
+ }
1999
+ /**
2000
+ * Lower pipeline-level rules to GitLab workflow:rules.
2001
+ * F-42: direct 1:1 mapping since GitLab has native support.
2002
+ */
2003
+ function lowerWorkflowRules(pipeline) {
2004
+ if (!pipeline.rules || pipeline.rules.length === 0) return [];
2005
+ return pipeline.rules.map((rule) => ({
2006
+ ...rule.if ? { if: rule.if } : {},
2007
+ ...rule.changes ? { changes: [...rule.changes] } : {},
2008
+ ...rule.exists ? { exists: [...rule.exists] } : {},
2009
+ ...rule.variables ? { variables: { ...rule.variables } } : {},
2010
+ ...rule.when ? { when: rule.when } : {}
2011
+ }));
2012
+ }
2013
+ /**
2014
+ * Lower a ComponentRef to a GitLab CI/CD component include entry.
2015
+ */
2016
+ function lowerComponentInclude(ref) {
2017
+ const inputs = {};
2018
+ for (const [name, value] of Object.entries(ref.inputs)) if (typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value) {
2019
+ const r = value;
2020
+ if (r.kind === "step") inputs[name] = `$CI_JOB_${r.step.replace(/-/g, "_").toUpperCase()}_OUTPUT_${r.output}`;
2021
+ else if (r.kind === "context") inputs[name] = `$${r.field.toUpperCase()}`;
2022
+ } else inputs[name] = value;
2023
+ return {
2024
+ component: `${ref.name}@${ref.version}`,
2025
+ inputs
2026
+ };
2027
+ }
2028
+ /**
2029
+ * Return the ids of all producer steps referenced by a step, including
2030
+ * explicit dependencies and artifact imports.
2031
+ */
2032
+ function producerIds(step) {
2033
+ const ids = [];
2034
+ for (const dep of step.dependencies) ids.push(dep.producer);
2035
+ for (const op of step.operations) if (op.kind === "importArtifact") ids.push(op.from);
2036
+ return ids;
2037
+ }
2038
+ /**
2039
+ * Compute the set of step IDs reachable from the given roots.
2040
+ * Throws INVALID_GRAPH for unknown roots or producers.
2041
+ */
2042
+ function reachableStepIds(roots, pipeline) {
2043
+ const byId = new Map(pipeline.steps.map((step) => [step.id, step]));
2044
+ const reachable = /* @__PURE__ */ new Set();
2045
+ const queue = [];
2046
+ for (const root of roots) {
2047
+ if (!byId.has(root)) throw new GitlabTargetError(`entry references unknown root step '${root}'`, "INVALID_GRAPH");
2048
+ if (!reachable.has(root)) {
2049
+ reachable.add(root);
2050
+ queue.push(root);
2051
+ }
2052
+ }
2053
+ let head = 0;
2054
+ while (head < queue.length) {
2055
+ const id = queue[head];
2056
+ head++;
2057
+ const step = byId.get(id);
2058
+ if (!step) continue;
2059
+ for (const producer of producerIds(step)) {
2060
+ if (!byId.has(producer)) throw new GitlabTargetError(`step '${step.id}' references unknown producer '${producer}'`, "INVALID_GRAPH");
2061
+ if (!reachable.has(producer)) {
2062
+ reachable.add(producer);
2063
+ queue.push(producer);
2064
+ }
2065
+ }
2066
+ }
2067
+ return reachable;
2068
+ }
2069
+ /**
2070
+ * Return the steps reachable from any entry root by following dependencies
2071
+ * and artifact imports.
2072
+ */
2073
+ function filterReachableSteps(pipeline) {
2074
+ if (pipeline.steps.length === 0) {
2075
+ for (const entry of pipeline.entries) if (entry.roots.length > 0) throw new GitlabTargetError(`entry '${entry.id}' references root steps but pipeline has no steps`, "INVALID_GRAPH");
2076
+ return [];
2077
+ }
2078
+ const reachable = reachableStepIds(pipeline.entries.flatMap((entry) => [...entry.roots]), pipeline);
2079
+ return pipeline.steps.filter((step) => reachable.has(step.id));
2080
+ }
2081
+ /**
2082
+ * Build a map from each job ID to the rules of entries that reach that job.
2083
+ */
2084
+ function buildJobRulesMap(pipeline, reachableSteps, jobIdMap) {
2085
+ const map = /* @__PURE__ */ new Map();
2086
+ const entryReachable = /* @__PURE__ */ new Map();
2087
+ for (const entry of pipeline.entries) entryReachable.set(entry.id, reachableStepIds(entry.roots, pipeline));
2088
+ for (const step of reachableSteps) {
2089
+ const rules = [];
2090
+ for (const entry of pipeline.entries) if (entryReachable.get(entry.id)?.has(step.id)) rules.push(...lowerTriggers([entry]));
2091
+ const jobId = jobIdMap.get(step.id);
2092
+ map.set(jobId, rules);
2093
+ }
2094
+ return map;
2095
+ }
2096
+ /**
2097
+ * Build a mapping from full step IDs to GitLab-safe job IDs.
2098
+ */
2099
+ function buildJobIdMap(steps) {
2100
+ const map = /* @__PURE__ */ new Map();
2101
+ const used = /* @__PURE__ */ new Set();
2102
+ for (const step of steps) {
2103
+ const shortId = step.id.includes("/") ? step.id.split("/").pop() : step.id;
2104
+ let jobId = shortId;
2105
+ let suffix = 1;
2106
+ while (used.has(jobId)) {
2107
+ jobId = `${shortId}-${suffix}`;
2108
+ suffix++;
2109
+ }
2110
+ used.add(jobId);
2111
+ map.set(step.id, jobId);
2112
+ }
2113
+ return map;
2114
+ }
2115
+ /**
2116
+ * Compute stages by topological level.
2117
+ * Level 0 → "build", Level N → "stage-N".
2118
+ * Returns stages in dependency order (build first, then stage-1, etc.).
2119
+ * Throws when a dependency cycle is detected.
2120
+ */
2121
+ function computeStages(steps, jobIdMap) {
2122
+ return deriveStageResult(computeAllLevels(steps, jobIdMap, buildDepsByJob(steps, jobIdMap)));
2123
+ }
2124
+ function buildDepsByJob(steps, jobIdMap) {
2125
+ const depsByJob = /* @__PURE__ */ new Map();
2126
+ for (const step of steps) {
2127
+ const jobId = jobIdMap.get(step.id) ?? step.id;
2128
+ const deps = producerIds(step).map((id) => jobIdMap.get(id) ?? id);
2129
+ depsByJob.set(jobId, deps);
2130
+ }
2131
+ return depsByJob;
2132
+ }
2133
+ function computeAllLevels(steps, jobIdMap, depsByJob) {
2134
+ const levels = /* @__PURE__ */ new Map();
2135
+ for (const step of steps) {
2136
+ const jobId = jobIdMap.get(step.id) ?? step.id;
2137
+ const level = computeLevel(jobId, depsByJob, levels, /* @__PURE__ */ new Set());
2138
+ levels.set(jobId, level);
2139
+ }
2140
+ return levels;
2141
+ }
2142
+ function stageNameForLevel(level) {
2143
+ return level === 0 ? "build" : `stage-${level}`;
2144
+ }
2145
+ function deriveStageResult(levels) {
2146
+ const maxLevel = Math.max(0, ...levels.values());
2147
+ const usedLevels = new Set(levels.values());
2148
+ const stages = [];
2149
+ for (let level = 0; level <= maxLevel; level++) if (usedLevels.has(level)) stages.push(stageNameForLevel(level));
2150
+ const stageMap = /* @__PURE__ */ new Map();
2151
+ for (const [jobId, level] of levels) stageMap.set(jobId, stageNameForLevel(level));
2152
+ return {
2153
+ stageMap,
2154
+ stages
2155
+ };
2156
+ }
2157
+ function computeLevel(jobId, depsByJob, levels, visiting) {
2158
+ if (levels.has(jobId)) return levels.get(jobId);
2159
+ if (visiting.has(jobId)) throw new GitlabTargetError(`dependency cycle detected involving job '${jobId}'`, "LOWER_FAILED");
2160
+ visiting.add(jobId);
2161
+ const deps = depsByJob.get(jobId) ?? [];
2162
+ if (deps.length === 0) {
2163
+ visiting.delete(jobId);
2164
+ return 0;
2165
+ }
2166
+ let maxDepLevel = 0;
2167
+ for (const dep of deps) {
2168
+ const depLevel = computeLevel(dep, depsByJob, levels, visiting);
2169
+ maxDepLevel = Math.max(maxDepLevel, depLevel);
2170
+ }
2171
+ visiting.delete(jobId);
2172
+ return maxDepLevel + 1;
2173
+ }
2174
+ /**
2175
+ * Map Sverka triggers to GitLab rules.
2176
+ * Branch filters are preserved in the generated `if` expressions.
2177
+ */
2178
+ function lowerTriggers(entries) {
2179
+ const rules = [];
2180
+ for (const entry of entries) {
2181
+ const t = entry.trigger;
2182
+ switch (t.kind) {
2183
+ case "push":
2184
+ rules.push(buildFilterRule("$CI_PIPELINE_SOURCE == \"push\"", t.filter, false));
2185
+ break;
2186
+ case "changeRequest":
2187
+ rules.push(buildFilterRule("$CI_PIPELINE_SOURCE == \"merge_request_event\"", t.filter, true));
2188
+ break;
2189
+ case "manual":
2190
+ rules.push({ if: "$CI_PIPELINE_SOURCE == \"web\"" });
2191
+ break;
2192
+ case "schedule":
2193
+ rules.push({ if: "$CI_PIPELINE_SOURCE == \"schedule\"" });
2194
+ break;
2195
+ default: throw new GitlabTargetError(`unsupported trigger kind: ${JSON.stringify(t.kind)}`, "UNSUPPORTED_TRIGGER");
2196
+ }
2197
+ }
2198
+ return rules;
2199
+ }
2200
+ /**
2201
+ * Build a GitLab rule from a source condition and a trigger filter.
2202
+ * For push triggers: branches → $CI_COMMIT_BRANCH, tags → $CI_COMMIT_TAG
2203
+ * For MR triggers: branches → $CI_MERGE_REQUEST_TARGET_BRANCH_NAME (target branch)
2204
+ * Paths → changes: [paths] on the rule
2205
+ */
2206
+ function buildFilterRule(sourceCondition, filter, isMergeRequest) {
2207
+ const conditions = [sourceCondition];
2208
+ const refConditions = [];
2209
+ if (filter?.branches && filter.branches.length > 0) {
2210
+ const branchRef = isMergeRequest ? "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" : "$CI_COMMIT_BRANCH";
2211
+ refConditions.push(buildRefCondition(branchRef, filter.branches));
2212
+ }
2213
+ if (filter?.tags && filter.tags.length > 0 && !isMergeRequest) refConditions.push(buildRefCondition("$CI_COMMIT_TAG", filter.tags));
2214
+ if (refConditions.length === 1) conditions.push(refConditions[0]);
2215
+ else if (refConditions.length > 1) conditions.push(`(${refConditions.join(" || ")})`);
2216
+ const rule = { if: conditions.join(" && ") };
2217
+ if (filter?.paths && filter.paths.length > 0) return {
2218
+ ...rule,
2219
+ changes: [...filter.paths]
2220
+ };
2221
+ return rule;
2222
+ }
2223
+ function buildRefCondition(ref, values) {
2224
+ if (values.length === 1) return `${ref} == ${quoteJsonString(values[0])}`;
2225
+ return `(${values.map((v) => `${ref} == ${quoteJsonString(v)}`).join(" || ")})`;
2226
+ }
2227
+ /**
2228
+ * Lower steps to GitLab jobs.
2229
+ */
2230
+ function lowerSteps(steps, jobIdMap, stageMap, rulesMap) {
2231
+ return steps.map((step) => {
2232
+ if (step.childPipeline) return lowerChildPipelineStep(step, jobIdMap, stageMap, rulesMap);
2233
+ if (step.downstream) return lowerDownstreamStep(step, jobIdMap, stageMap, rulesMap);
2234
+ return lowerStep(step, jobIdMap, stageMap, rulesMap);
2235
+ });
2236
+ }
2237
+ /**
2238
+ * Lower a child pipeline step to a GitLab trigger job.
2239
+ * F-33: childPipeline → trigger: include: [{ artifact: <path>, job: <generator> }]
2240
+ */
2241
+ function lowerChildPipelineStep(step, jobIdMap, stageMap, rulesMap) {
2242
+ const jobId = jobIdMap.get(step.id) ?? step.id;
2243
+ const stage = stageMap.get(jobId) ?? "build";
2244
+ const rules = rulesMap.get(jobId) ?? [];
2245
+ const needs = step.dependencies.filter((d) => d.kind === "control").map((d) => jobIdMap.get(d.producer) ?? d.producer);
2246
+ const cp = step.childPipeline;
2247
+ const generatorJobId = jobIdMap.get(`${step.id.split("/").slice(0, -1).join("/")}/${cp.generator}`) ?? cp.generator;
2248
+ const trigger = { include: [{
2249
+ artifact: cp.artifact,
2250
+ job: generatorJobId
2251
+ }] };
2252
+ return {
2253
+ id: jobId,
2254
+ stage,
2255
+ needs,
2256
+ script: [],
2257
+ ...rules.length > 0 ? { rules } : {},
2258
+ trigger
2259
+ };
2260
+ }
2261
+ /**
2262
+ * Lower a downstream step to a GitLab multi-project trigger job.
2263
+ * F-34: downstream → trigger: project: <project>, branch: <branch>, strategy: depend
2264
+ */
2265
+ function lowerDownstreamStep(step, jobIdMap, stageMap, rulesMap) {
2266
+ const jobId = jobIdMap.get(step.id) ?? step.id;
2267
+ const stage = stageMap.get(jobId) ?? "build";
2268
+ const rules = rulesMap.get(jobId) ?? [];
2269
+ const needs = step.dependencies.filter((d) => d.kind === "control").map((d) => jobIdMap.get(d.producer) ?? d.producer);
2270
+ const ds = step.downstream;
2271
+ const trigger = {
2272
+ project: ds.project,
2273
+ ...ds.branch ? { branch: ds.branch } : {},
2274
+ strategy: "depend"
2275
+ };
2276
+ const variables = lowerDownstreamVariables(ds.inputs);
2277
+ return {
2278
+ id: jobId,
2279
+ stage,
2280
+ needs,
2281
+ script: [],
2282
+ ...rules.length > 0 ? { rules } : {},
2283
+ trigger,
2284
+ ...Object.keys(variables).length > 0 ? { variables } : {}
2285
+ };
2286
+ }
2287
+ /**
2288
+ * Lower downstream step inputs to GitLab trigger variables.
2289
+ */
2290
+ function lowerDownstreamVariables(inputs) {
2291
+ const variables = {};
2292
+ if (!inputs) return variables;
2293
+ for (const [name, value] of Object.entries(inputs)) variables[name] = lowerReferenceOrLiteral(value);
2294
+ return variables;
2295
+ }
2296
+ /**
2297
+ * Lower a reference binding or literal value to a GitLab variable string.
2298
+ * Reference bindings use GitLab variable interpolation; literals are stringified.
2299
+ */
2300
+ function lowerReferenceOrLiteral(value) {
2301
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value) {
2302
+ const r = value;
2303
+ if (r.kind === "step") return `$CI_JOB_${r.step.replace(/-/g, "_").toUpperCase()}_OUTPUT_${r.output}`;
2304
+ if (r.kind === "context") return `$${r.field.toUpperCase()}`;
2305
+ }
2306
+ return String(value);
2307
+ }
2308
+ /**
2309
+ * Lower a single Step to a GitLab job.
2310
+ */
2311
+ function lowerStep(step, jobIdMap, stageMap, rulesMap) {
2312
+ const jobId = jobIdMap.get(step.id) ?? step.id;
2313
+ const stage = stageMap.get(jobId) ?? "build";
2314
+ const mergedRules = mergeRules(rulesMap.get(jobId) ?? [], step.rules);
2315
+ const { script, artifacts, needs: importNeeds, variables, release, pages } = lowerOperations(step, jobId, jobIdMap);
2316
+ const needs = mergeNeeds(step, jobIdMap, importNeeds);
2317
+ if (script.length === 0) script.push("echo 'no operations'");
2318
+ const { image, variables: jobVariables } = lowerRuntime(step, variables);
2319
+ const rules = applyStepCondition(mergedRules, step, jobIdMap);
2320
+ const writeVariables = lowerWriteVariables(step);
2321
+ return {
2322
+ id: jobId,
2323
+ stage,
2324
+ needs,
2325
+ script,
2326
+ ...buildJobFields({
2327
+ image,
2328
+ artifacts,
2329
+ variables: {
2330
+ ...jobVariables,
2331
+ ...writeVariables
2332
+ },
2333
+ rules,
2334
+ timeout: step.timeout,
2335
+ interruptible: step.interruptible,
2336
+ runner: step.runner,
2337
+ identity: step.identity,
2338
+ services: step.services,
2339
+ environment: step.environment,
2340
+ cache: step.cache,
2341
+ concurrency: step.concurrency
2342
+ }),
2343
+ ...step.matrix !== void 0 ? { parallel: { matrix: lowerGitlabMatrix(step.matrix) } } : {},
2344
+ ...step.beforeScript ? { beforeScript: step.beforeScript } : {},
2345
+ ...step.afterScript ? { afterScript: step.afterScript } : {},
2346
+ ...resolveContinueOnError(step),
2347
+ ...resolveRetryConfig(step),
2348
+ ...release ? { release } : {},
2349
+ ...pages ? { pages } : {},
2350
+ ...step.delay ? {
2351
+ when: "delayed",
2352
+ start_in: step.delay
2353
+ } : {}
2354
+ };
2355
+ }
2356
+ /**
2357
+ * If the step has a condition, apply it to the merged rules.
2358
+ *
2359
+ * Status conditions (success/failure/always/never) map to GitLab `when`
2360
+ * values, not `if:` expressions. GitLab's `if:` does not have equivalents for
2361
+ * `on_success`/`on_failure` — those are `when` semantics. Emitting empty `if:`
2362
+ * strings (as the previous code did for success/failure) produces invalid YAML.
2363
+ *
2364
+ * Step-output conditions reference dotenv variables from a producer job's
2365
+ * artifacts. GitLab evaluates all job rules during pipeline creation, before
2366
+ * any job runs — so dotenv variables are not available at that point. Reject
2367
+ * step-output conditions with an error until runtime-safe handling exists.
2368
+ *
2369
+ * Context and expression conditions are AND'd with each rule's `if` expression.
2370
+ * GitLab evaluates rules in order and stops at the first match, so appending
2371
+ * the condition as a separate rule would bypass it when a trigger rule matches.
2372
+ */
2373
+ function applyStepCondition(mergedRules, step, jobIdMap) {
2374
+ if (step.condition === void 0) return mergedRules;
2375
+ if (step.condition.kind === "status") {
2376
+ const when = statusToGitlabWhen(step.condition.status);
2377
+ if (mergedRules.length === 0) return [{ when }];
2378
+ return mergedRules.map((rule) => ({
2379
+ ...rule,
2380
+ when
2381
+ }));
2382
+ }
2383
+ if (step.condition.kind === "step") throw new GitlabTargetError(`step '${step.id}' has a step-output condition (${step.condition.step}.${step.condition.output}), which GitLab cannot evaluate during pipeline creation — dotenv variables from producer jobs are not available when rules are evaluated`, "LOWER_FAILED");
2384
+ const condExpr = lowerGitlabConditionExpr(step.condition, jobIdMap);
2385
+ let rules = mergedRules.map((rule) => {
2386
+ if (condExpr === void 0) return rule;
2387
+ const ifExpr = rule.if !== void 0 ? `(${rule.if}) && (${condExpr})` : condExpr;
2388
+ return {
2389
+ ...rule,
2390
+ if: ifExpr
2391
+ };
2392
+ });
2393
+ if (rules.length === 0 && condExpr !== void 0) rules = [{ if: condExpr }];
2394
+ return rules;
2395
+ }
2396
+ /**
2397
+ * Map a Sverka step status condition to a GitLab rules `when` value.
2398
+ * - success → on_success (run only if all parent jobs succeed — GitLab default)
2399
+ * - failure → on_failure (run only if at least one parent job fails)
2400
+ * - always → always
2401
+ * - never → never
2402
+ */
2403
+ function statusToGitlabWhen(status) {
2404
+ switch (status) {
2405
+ case "always": return "always";
2406
+ case "never": return "never";
2407
+ case "success": return "on_success";
2408
+ case "failure": return "on_failure";
2409
+ }
2410
+ }
2411
+ /** Resolve continueOnError to GitLab allowFailure field. */
2412
+ function resolveContinueOnError(step) {
2413
+ if (step.continueOnError === void 0) return {};
2414
+ return { allowFailure: typeof step.continueOnError === "boolean" ? step.continueOnError : { exitCodes: step.continueOnError.exitCodes } };
2415
+ }
2416
+ /** Resolve retry configuration to GitLab retry field. */
2417
+ function resolveRetryConfig(step) {
2418
+ if (step.retry === void 0) return {};
2419
+ return { retry: {
2420
+ max: step.retry.max,
2421
+ ...step.retry.when ? { when: step.retry.when } : {},
2422
+ ...step.retry.exitCodes ? { exitCodes: step.retry.exitCodes } : {}
2423
+ } };
2424
+ }
2425
+ function mergeNeeds(step, jobIdMap, importNeeds) {
2426
+ return [.../* @__PURE__ */ new Set([...lowerDependencies(step.dependencies, jobIdMap), ...importNeeds])];
2427
+ }
2428
+ function lowerRuntime(step, baseVariables) {
2429
+ const runtime = step.runtime;
2430
+ const mode = runtime.mode ?? "host";
2431
+ if (mode === "container" && !runtime.image) throw new GitlabTargetError(`step '${step.id}' uses container mode without an image`, "LOWER_FAILED");
2432
+ const image = mode === "container" ? runtime.image : void 0;
2433
+ const variables = { ...baseVariables };
2434
+ if (runtime.env) Object.assign(variables, runtime.env);
2435
+ if (runtime.secrets) for (const secret of runtime.secrets) variables[secret] = `$${secret}`;
2436
+ if (runtime.network && runtime.network.allowed.length > 0) variables["SVERKA_NETWORK_ALLOWLIST"] = runtime.network.allowed.join(",");
2437
+ return {
2438
+ ...image ? { image } : {},
2439
+ variables
2440
+ };
2441
+ }
2442
+ /**
2443
+ * Safe-outputs (Spec 25): GitLab has no per-job permission block.
2444
+ * Steps with declared writes receive CI variables corresponding to their
2445
+ * declared write kinds (e.g. SV_WRITE_DEPLOY=true). Steps without writes
2446
+ * receive no SV_WRITE_* variables.
2447
+ */
2448
+ function lowerWriteVariables(step) {
2449
+ const writes = step.permissions?.write;
2450
+ if (!writes || writes.length === 0) return {};
2451
+ const vars = {};
2452
+ for (const decl of writes) {
2453
+ const varName = `SV_WRITE_${decl.kind.toUpperCase().replace(/-/g, "_")}`;
2454
+ vars[varName] = "true";
2455
+ }
2456
+ return vars;
2457
+ }
2458
+ /** Build core job fields: image, artifacts, variables, rules. */
2459
+ function buildJobCoreFields(ctx) {
2460
+ return {
2461
+ ...ctx.image ? { image: ctx.image } : {},
2462
+ ...ctx.artifacts ? { artifacts: ctx.artifacts } : {},
2463
+ ...Object.keys(ctx.variables).length > 0 ? { variables: ctx.variables } : {},
2464
+ ...ctx.rules.length > 0 ? { rules: ctx.rules } : {}
2465
+ };
2466
+ }
2467
+ /** Build runtime/execution fields: timeout, interruptible, runner tags, id_tokens. */
2468
+ function buildJobRuntimeFields(ctx) {
2469
+ return {
2470
+ ...ctx.timeout !== void 0 ? { timeout: `${Math.ceil(ctx.timeout / 6e4)}m` } : {},
2471
+ ...ctx.interruptible !== void 0 ? { interruptible: ctx.interruptible } : {},
2472
+ ...ctx.runner !== void 0 ? { tags: ctx.runner.labels } : {},
2473
+ ...lowerIdTokens(ctx.identity)
2474
+ };
2475
+ }
2476
+ /** Build environment-related fields: services, environment, cache, concurrency. */
2477
+ function buildJobEnvFields(ctx) {
2478
+ return {
2479
+ ...ctx.services !== void 0 && ctx.services.length > 0 ? { services: lowerGitlabServices(ctx.services) } : {},
2480
+ ...ctx.environment !== void 0 ? { environment: lowerGitlabEnvironment(ctx.environment) } : {},
2481
+ ...ctx.cache !== void 0 ? { cache: lowerGitlabCache(ctx.cache) } : {},
2482
+ ...ctx.concurrency !== void 0 ? { resourceGroup: ctx.concurrency.group } : {}
2483
+ };
2484
+ }
2485
+ function buildJobFields(ctx) {
2486
+ return {
2487
+ ...buildJobCoreFields(ctx),
2488
+ ...buildJobRuntimeFields(ctx),
2489
+ ...buildJobEnvFields(ctx)
2490
+ };
2491
+ }
2492
+ /** Lower identity tokens to GitLab id_tokens field. */
2493
+ function lowerIdTokens(identity) {
2494
+ if (identity === void 0) return {};
2495
+ const idTokens = {};
2496
+ for (const [name, spec] of Object.entries(identity.tokens)) idTokens[name] = { aud: spec.audience };
2497
+ return Object.keys(idTokens).length > 0 ? { idTokens } : {};
2498
+ }
2499
+ /**
2500
+ * Lower cache spec to GitLab cache keyword.
2501
+ */
2502
+ function lowerGitlabCache(cache) {
2503
+ return {
2504
+ paths: cache.paths,
2505
+ key: cache.key,
2506
+ ...cache.policy !== void 0 ? { policy: cache.policy } : {},
2507
+ ...cache.restoreKeys !== void 0 ? { fallbackKeys: cache.restoreKeys } : {}
2508
+ };
2509
+ }
2510
+ /**
2511
+ * Lower environment spec to GitLab environment map.
2512
+ */
2513
+ function lowerGitlabEnvironment(env) {
2514
+ return {
2515
+ name: env.name,
2516
+ ...env.url !== void 0 ? { url: env.url } : {},
2517
+ ...env.action !== void 0 ? { action: env.action } : {},
2518
+ ...env.tier !== void 0 ? { deploymentTier: env.tier } : {}
2519
+ };
2520
+ }
2521
+ /**
2522
+ * Lower service containers to GitLab services array.
2523
+ * ports are not supported by GitLab and are dropped.
2524
+ */
2525
+ function lowerGitlabServices(services) {
2526
+ return services.map((svc) => ({
2527
+ name: svc.image,
2528
+ alias: svc.alias ?? svc.name,
2529
+ ...svc.entrypoint !== void 0 ? { entrypoint: [...svc.entrypoint] } : {},
2530
+ ...svc.command !== void 0 ? { command: [...svc.command] } : {},
2531
+ ...svc.env !== void 0 ? { variables: { ...svc.env } } : {}
2532
+ }));
2533
+ }
2534
+ /**
2535
+ * Merge trigger-derived rules with step-level rules.
2536
+ * Step-level rules are appended after trigger rules.
2537
+ */
2538
+ function mergeRules(triggerRules, stepRules) {
2539
+ if (stepRules === void 0 || stepRules.length === 0) return triggerRules;
2540
+ const result = [...triggerRules];
2541
+ for (const rule of stepRules) {
2542
+ const gitlabRule = {
2543
+ ...rule.if !== void 0 ? { if: rule.if } : {},
2544
+ ...rule.when !== void 0 ? { when: rule.when } : {},
2545
+ ...rule.changes !== void 0 ? { changes: rule.changes } : {},
2546
+ ...rule.exists !== void 0 ? { exists: rule.exists } : {},
2547
+ ...rule.variables !== void 0 ? { variables: rule.variables } : {}
2548
+ };
2549
+ result.push(gitlabRule);
2550
+ }
2551
+ return result;
2552
+ }
2553
+ /**
2554
+ * Map dependencies to job needs.
2555
+ */
2556
+ function lowerDependencies(deps, jobIdMap) {
2557
+ const needs = /* @__PURE__ */ new Set();
2558
+ for (const dep of deps) {
2559
+ const jobId = jobIdMap.get(dep.producer) ?? dep.producer;
2560
+ needs.add(jobId);
2561
+ }
2562
+ return [...needs];
2563
+ }
2564
+ /**
2565
+ * Map operations to script entries, artifacts, and dependencies.
2566
+ * Scalar outputs are written to a dotenv report file.
2567
+ */
2568
+ function lowerOperations(step, jobId, jobIdMap) {
2569
+ const acc = {
2570
+ script: [],
2571
+ artifactPaths: [],
2572
+ importNeeds: [],
2573
+ reportEntries: {},
2574
+ inputs: step.inputs,
2575
+ hasDotenv: false,
2576
+ artifactRetention: void 0,
2577
+ artifactAccess: void 0,
2578
+ release: void 0,
2579
+ pages: void 0,
2580
+ stdoutTargetIndex: void 0,
2581
+ stdoutNames: [],
2582
+ stdoutArtifacts: false,
2583
+ workingDir: step.runtime.workingDir
2584
+ };
2585
+ for (const op of step.operations) lowerOperation(op, jobId, acc, jobIdMap);
2586
+ return assembleOperationResult(acc, step);
2587
+ }
2588
+ /**
2589
+ * Lower a shell operation, handling background execution.
2590
+ */
2591
+ function lowerShellOp(op, acc, jobIdMap) {
2592
+ sealStdoutCapture(acc);
2593
+ const translated = translateGitlabCommand(op.command, acc.inputs, jobIdMap);
2594
+ acc.script.push(op.background ? `${translated} &` : translated);
2595
+ acc.stdoutTargetIndex = acc.script.length - 1;
2596
+ }
2597
+ /**
2598
+ * Bind the pending stdout artifact names to the most recent shell op: wrap
2599
+ * that op's script line so its stdout is also written to the declared file(s)
2600
+ * and record the files as job artifacts.
2601
+ */
2602
+ function sealStdoutCapture(acc) {
2603
+ if (acc.stdoutTargetIndex === void 0 || acc.stdoutNames.length === 0) return;
2604
+ acc.script[acc.stdoutTargetIndex] = wrapStdoutCaptureLine(acc.script[acc.stdoutTargetIndex], acc.stdoutNames);
2605
+ acc.artifactPaths.push(...acc.stdoutNames.map((name) => acc.workingDir ? `${acc.workingDir}/${name}` : name));
2606
+ acc.stdoutArtifacts = true;
2607
+ acc.stdoutNames = [];
2608
+ }
2609
+ /**
2610
+ * Lower a single operation, mutating the accumulator.
2611
+ */
2612
+ function lowerOperation(op, stepId, acc, jobIdMap) {
2613
+ switch (op.kind) {
2614
+ case "shell":
2615
+ lowerShellOp(op, acc, jobIdMap);
2616
+ break;
2617
+ case "exportOutput":
2618
+ lowerExportOutput(op, stepId, acc);
2619
+ break;
2620
+ case "exportArtifact":
2621
+ lowerExportArtifact(op, acc);
2622
+ break;
2623
+ case "exportStdout":
2624
+ if (acc.stdoutTargetIndex === void 0) throw new GitlabTargetError(`step '${stepId}' declares stdout artifact '${op.name}' but no shell output was captured`, "LOWER_FAILED");
2625
+ acc.stdoutNames.push(op.name);
2626
+ break;
2627
+ case "importArtifact":
2628
+ acc.importNeeds.push(jobIdMap.get(op.from) ?? op.from);
2629
+ break;
2630
+ case "diagnostic":
2631
+ acc.script.push(`echo ${shellQuoteSingle(op.message)}`);
2632
+ break;
2633
+ case "report":
2634
+ lowerReport(op, acc);
2635
+ break;
2636
+ case "release":
2637
+ acc.release = lowerReleaseOp(op);
2638
+ break;
2639
+ case "deployPages":
2640
+ lowerDeployPages(op, acc);
2641
+ break;
2642
+ default: throw new GitlabTargetError(`unsupported operation kind: ${JSON.stringify(op.kind)}`, "LOWER_FAILED");
2643
+ }
2644
+ }
2645
+ /**
2646
+ * Lower an exportOutput operation into a dotenv echo line.
2647
+ */
2648
+ function lowerExportOutput(op, stepId, acc) {
2649
+ const name = shellEscapeDoubleQuoted(`${stepId}_${op.name}`);
2650
+ acc.script.push(`echo "${name}=\${${op.name}}" >> ${DOTENV_REPORT_FILE}`);
2651
+ acc.hasDotenv = true;
2652
+ }
2653
+ /**
2654
+ * Lower an exportArtifact operation, recording paths and retention metadata.
2655
+ */
2656
+ function lowerExportArtifact(op, acc) {
2657
+ acc.artifactPaths.push(op.path);
2658
+ if (op.retention !== void 0) acc.artifactRetention = op.retention;
2659
+ if (op.access !== void 0) acc.artifactAccess = op.access;
2660
+ }
2661
+ /**
2662
+ * Lower a report operation into a GitLab artifacts:reports entry.
2663
+ */
2664
+ function lowerReport(op, acc) {
2665
+ const key = gitlabReportKey(op.spec.type);
2666
+ if (op.spec.type === "coverage") acc.reportEntries[key] = {
2667
+ coverage_format: op.spec.format ?? "cobertura",
2668
+ path: op.spec.path
2669
+ };
2670
+ else acc.reportEntries[key] = op.spec.path;
2671
+ }
2672
+ /**
2673
+ * Lower a release operation to a GitLab release keyword.
2674
+ * Assets are file paths → emit as links with placeholder URLs
2675
+ * (GitLab requires URLs, not file paths).
2676
+ */
2677
+ function lowerReleaseOp(op) {
2678
+ const links = (op.assets ?? []).map((path) => ({
2679
+ name: path.split("/").pop() ?? path,
2680
+ url: `$CI_PROJECT_URL/-/jobs/$CI_JOB_ID/artifacts/file/${path}`
2681
+ }));
2682
+ return {
2683
+ tag_name: op.tag,
2684
+ ...op.name ? { name: op.name } : {},
2685
+ ...op.description ? { description: op.description } : {},
2686
+ ...links.length > 0 ? { assets: { links } } : {},
2687
+ ...op.draft !== void 0 ? { draft: op.draft } : {}
2688
+ };
2689
+ }
2690
+ /**
2691
+ * Lower a deployPages operation to a GitLab pages keyword.
2692
+ * GitLab 17.6+ accepts any job name when the job sets the `pages` keyword.
2693
+ * The path is also added as an artifact.
2694
+ */
2695
+ function lowerDeployPages(op, acc) {
2696
+ acc.pages = {
2697
+ publish: op.path,
2698
+ ...op.prefix ? { path_prefix: op.prefix } : {}
2699
+ };
2700
+ acc.artifactPaths.push(op.path);
2701
+ }
2702
+ /**
2703
+ * Assemble the final lowerOperations result from the accumulator.
2704
+ */
2705
+ function assembleOperationResult(acc, step) {
2706
+ sealStdoutCapture(acc);
2707
+ const artifacts = {};
2708
+ if (acc.artifactPaths.length > 0) artifacts.paths = acc.artifactPaths;
2709
+ if (acc.stdoutArtifacts) artifacts.when = "always";
2710
+ if (acc.hasDotenv) acc.reportEntries.dotenv = DOTENV_REPORT_FILE;
2711
+ if (Object.keys(acc.reportEntries).length > 0) artifacts.reports = acc.reportEntries;
2712
+ if (acc.artifactRetention !== void 0) artifacts.expireIn = parseGitlabExpireIn(acc.artifactRetention);
2713
+ if (acc.artifactAccess !== void 0) artifacts.access = acc.artifactAccess;
2714
+ if (step.runtime.workingDir && acc.script.length > 0) {
2715
+ const escapedDir = shellQuoteSingle(step.runtime.workingDir);
2716
+ acc.script.unshift(`cd ${escapedDir}`);
2717
+ }
2718
+ return {
2719
+ script: acc.script,
2720
+ ...Object.keys(artifacts).length > 0 ? { artifacts } : {},
2721
+ needs: acc.importNeeds,
2722
+ variables: {},
2723
+ ...acc.release ? { release: acc.release } : {},
2724
+ ...acc.pages ? { pages: acc.pages } : {}
2725
+ };
2726
+ }
2727
+ /**
2728
+ * Convert Sverka retention duration to GitLab expire_in format.
2729
+ * "7d" → "7 days", "1h" → "1 hours", "30m" → "30 minutes", "never" → "never".
2730
+ */
2731
+ function parseGitlabExpireIn(retention) {
2732
+ if (retention === "never") return "never";
2733
+ const match = /^(\d+)([dhm])$/.exec(retention);
2734
+ if (match === null) return retention;
2735
+ const value = match[1];
2736
+ const unit = match[2];
2737
+ if (unit === "d") return `${value} days`;
2738
+ if (unit === "h") return `${value} hours`;
2739
+ if (unit === "m") return `${value} minutes`;
2740
+ return retention;
2741
+ }
2742
+ /**
2743
+ * Map Sverka report type to GitLab artifacts:reports key.
2744
+ */
2745
+ function gitlabReportKey(type) {
2746
+ return {
2747
+ junit: "junit",
2748
+ coverage: "coverage_report",
2749
+ dotenv: "dotenv",
2750
+ sast: "sast",
2751
+ dast: "dast",
2752
+ dependencyScanning: "dependency_scanning",
2753
+ containerScanning: "container_scanning",
2754
+ licenseScanning: "license_scanning",
2755
+ performance: "performance",
2756
+ metrics: "metrics",
2757
+ terraform: "terraform",
2758
+ quality: "quality",
2759
+ sarif: "sast"
2760
+ }[type] ?? type;
2761
+ }
2762
+ /**
2763
+ * Produce a deterministic JSON string literal (quoted) for a value.
2764
+ */
2765
+ function quoteJsonString(value) {
2766
+ return `"${value.replace(/[\\"\n\r\t\b\f\u0000-\u001f]/g, (ch) => {
2767
+ return JSON_STRING_ESCAPES[ch] ?? `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
2768
+ })}"`;
2769
+ }
2770
+ const JSON_STRING_ESCAPES = {
2771
+ "\"": "\\\"",
2772
+ "\\": "\\\\",
2773
+ "\n": "\\n",
2774
+ "\r": "\\r",
2775
+ " ": "\\t",
2776
+ "\b": "\\b",
2777
+ "\f": "\\f"
2778
+ };
2779
+ /**
2780
+ * Escape a string for use inside double quotes in a POSIX shell.
2781
+ */
2782
+ function shellEscapeDoubleQuoted(value) {
2783
+ return value.replace(/[\\"`$]/g, "\\$&");
2784
+ }
2785
+ /**
2786
+ * Collect pipeline-level variables from inputs.
2787
+ * Secret inputs are omitted from the generated variables block.
2788
+ */
2789
+ function collectVariables(pipeline) {
2790
+ const vars = {};
2791
+ for (const [name, input] of Object.entries(pipeline.inputs)) {
2792
+ if (input.secret) continue;
2793
+ if (input.default !== void 0) vars[name] = String(input.default);
2794
+ }
2795
+ return vars;
2796
+ }
2797
+ /**
2798
+ * Lower a MatrixSpec to a GitLab parallel:matrix array.
2799
+ * GitLab has no native include/exclude — exclude is emulated by filtering
2800
+ * combinations at synthesis time, include is appended to the array.
2801
+ */
2802
+ function lowerGitlabMatrix(spec) {
2803
+ validateMatrixKeys(spec);
2804
+ const combinations = computeMatrixCombinations(spec.dimensions, spec.exclude ?? []);
2805
+ const totalRows = combinations.length + (spec.include?.length ?? 0);
2806
+ if (totalRows > 200) throw new GitlabTargetError(`GitLab matrix has ${totalRows} rows, exceeding the 200-row limit`, "MATRIX_TOO_LARGE");
2807
+ const includeEntries = (spec.include ?? []).map((entry) => ({ ...entry }));
2808
+ return [...combinations.map((c) => ({ ...c })), ...includeEntries];
2809
+ }
2810
+ /**
2811
+ * Validate that matrix dimension and include keys contain only valid
2812
+ * GitLab variable name characters: letters, digits, underscores.
2813
+ */
2814
+ function validateMatrixKeys(spec) {
2815
+ const validKey = /^[A-Za-z_]\w*$/;
2816
+ for (const key of Object.keys(spec.dimensions)) if (!validKey.test(key)) throw new GitlabTargetError(String.raw`invalid matrix dimension key '${key}': must match [A-Za-z_]\w*`, "INVALID_MATRIX");
2817
+ if (spec.include) {
2818
+ for (const entry of spec.include) for (const key of Object.keys(entry)) if (!validKey.test(key)) throw new GitlabTargetError(String.raw`invalid matrix include key '${key}': must match [A-Za-z_]\w*`, "INVALID_MATRIX");
2819
+ }
2820
+ }
2821
+ /**
2822
+ * Compute the cross-product of matrix dimensions, then filter out
2823
+ * combinations matching any exclude rule (partial match).
2824
+ */
2825
+ function computeMatrixCombinations(dimensions, exclude) {
2826
+ const keys = Object.keys(dimensions);
2827
+ if (keys.length === 0) return [];
2828
+ let combinations = [{}];
2829
+ for (const key of keys) {
2830
+ const values = dimensions[key];
2831
+ if (!values || values.length === 0) continue;
2832
+ const next = [];
2833
+ for (const combo of combinations) for (const v of values) next.push({
2834
+ ...combo,
2835
+ [key]: v
2836
+ });
2837
+ combinations = next;
2838
+ }
2839
+ if (exclude.length === 0) return combinations;
2840
+ return combinations.filter((combo) => !matchesAny(combo, exclude));
2841
+ }
2842
+ /**
2843
+ * Check if a combination matches any exclude rule (partial match).
2844
+ */
2845
+ function matchesAny(combo, rules) {
2846
+ for (const rule of rules) {
2847
+ const ruleEntries = Object.entries(rule);
2848
+ if (ruleEntries.length === 0) continue;
2849
+ if (ruleEntries.every(([k, v]) => combo[k] === v)) return true;
2850
+ }
2851
+ return false;
2852
+ }
2853
+ const GITLAB_CONTEXT_MAP = {
2854
+ "git.sha": "$CI_COMMIT_SHA",
2855
+ "git.branch": "$CI_COMMIT_BRANCH",
2856
+ "git.tag": "$CI_COMMIT_TAG",
2857
+ "change.id": "$CI_MERGE_REQUEST_IID",
2858
+ "change.source": "$CI_PIPELINE_SOURCE",
2859
+ "change.target": "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME",
2860
+ "change.draft": "$CI_MERGE_REQUEST_DRAFT",
2861
+ "event.type": "$CI_PIPELINE_SOURCE",
2862
+ "run.id": "$CI_PIPELINE_ID",
2863
+ "run.attempt": "$run_attempt"
2864
+ };
2865
+ /**
2866
+ * Translate a context ref (namespace.field) to GitLab variable syntax.
2867
+ */
2868
+ function translateGitlabContextRef(namespace, field) {
2869
+ const key = `${namespace}.${field}`;
2870
+ const mapped = GITLAB_CONTEXT_MAP[key];
2871
+ if (mapped) return mapped;
2872
+ if (namespace === "env" || namespace === "secrets") return `$${field}`;
2873
+ if (namespace === "matrix") return `$${field.toUpperCase()}`;
2874
+ if (namespace === "inputs") return `$[[ inputs.${field} ]]`;
2875
+ throw new GitlabTargetError(`unsupported context namespace '${namespace}' in GitLab lowering`, "LOWER_FAILED");
2876
+ }
2877
+ /**
2878
+ * Translate a step ref to GitLab variable syntax: $<output> (from dotenv artifact).
2879
+ */
2880
+ function translateGitlabStepRef(ref) {
2881
+ return `$${ref.output}`;
2882
+ }
2883
+ /**
2884
+ * Build a lookup map from placeholder key to Reference from the step's inputs.
2885
+ */
2886
+ function buildGitlabInputLookup(inputs) {
2887
+ const map = /* @__PURE__ */ new Map();
2888
+ for (const ref of inputs) if (ref.kind === "context") map.set(`${ref.namespace}.${ref.field}`, ref);
2889
+ else if (ref.kind === "step") map.set(`${ref.step}.${ref.output}`, ref);
2890
+ return map;
2891
+ }
2892
+ /**
2893
+ * Translate ${...} placeholders in a command string to GitLab variable syntax.
2894
+ */
2895
+ function translateGitlabCommand(command, inputs, _jobIdMap) {
2896
+ const lookup = buildGitlabInputLookup(inputs);
2897
+ return command.replace(/\$\{([^{}]+)\}/g, (_, key) => {
2898
+ const ref = lookup.get(key);
2899
+ if (ref === void 0) return `\${${key}}`;
2900
+ if (ref.kind === "context") return translateGitlabContextRef(ref.namespace, ref.field);
2901
+ return translateGitlabStepRef(ref);
2902
+ });
2903
+ }
2904
+ /**
2905
+ * Lower a step condition to a GitLab if: expression string (without wrapping in a rule).
2906
+ * Returns the raw expression that can be combined with trigger rule conditions.
2907
+ */
2908
+ function lowerGitlabConditionExpr(condition, _jobIdMap) {
2909
+ if (condition.kind === "status") {
2910
+ if (condition.status === "always") return "true";
2911
+ if (condition.status === "never") return "false";
2912
+ return "";
2913
+ }
2914
+ if (condition.kind === "context") return translateGitlabContextRef(condition.namespace, condition.field);
2915
+ if (condition.kind === "step") return translateGitlabStepRef(condition);
2916
+ const lookup = buildGitlabInputLookup(condition.refs);
2917
+ return condition.template.replace(/\$\{([^{}]+)\}/g, (_, key) => {
2918
+ const ref = lookup.get(key);
2919
+ if (ref === void 0) return `\${${key}}`;
2920
+ if (ref.kind === "context") return translateGitlabContextRef(ref.namespace, ref.field);
2921
+ return translateGitlabStepRef(ref);
2922
+ });
2923
+ }
2924
+ //#endregion
2925
+ //#region src/gitlab/emit.ts
2926
+ const RESERVED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
2927
+ "stages",
2928
+ "variables",
2929
+ "workflow",
2930
+ "include",
2931
+ "default",
2932
+ "image",
2933
+ "services",
2934
+ "before_script",
2935
+ "after_script",
2936
+ "cache",
2937
+ "spec",
2938
+ "pages:deploy"
2939
+ ]);
2940
+ /**
2941
+ * Emit a GitlabTargetGraph as YAML artifacts.
2942
+ * Produces one .gitlab-ci.yml file.
2943
+ */
2944
+ function emitGitlab(targetGraph) {
2945
+ return [{
2946
+ path: ".gitlab-ci.yml",
2947
+ content: stringifyTargetGraph(targetGraph)
2948
+ }];
2949
+ }
2950
+ /**
2951
+ * Convert a GitlabTargetGraph to a YAML string.
2952
+ */
2953
+ function stringifyTargetGraph(graph) {
2954
+ const doc = { stages: graph.stages };
2955
+ if (graph.autoCancel) doc.workflow = { auto_cancel: { on_new_commit: "interruptible" } };
2956
+ if (Object.keys(graph.variables).length > 0) doc.variables = graph.variables;
2957
+ if (graph.default) doc.default = defaultToYaml(graph.default);
2958
+ if (graph.specInputs && Object.keys(graph.specInputs).length > 0) doc.spec = { inputs: graph.specInputs };
2959
+ const allIncludes = collectIncludes(graph);
2960
+ if (allIncludes.length > 0) doc.include = allIncludes;
2961
+ if (graph.workflowRules && graph.workflowRules.length > 0) doc.workflow = {
2962
+ ...doc.workflow,
2963
+ rules: graph.workflowRules.map(workflowRuleToYaml)
2964
+ };
2965
+ for (const job of graph.jobs) {
2966
+ if (RESERVED_TOP_LEVEL_KEYS.has(job.id)) throw new GitlabTargetError(`job id '${job.id}' conflicts with a reserved top-level GitLab CI key`, "EMIT_FAILED");
2967
+ doc[job.id] = jobToYaml(job);
2968
+ }
2969
+ return stringify(doc, { sortMapEntries: false });
2970
+ }
2971
+ /**
2972
+ * Convert a GitlabDefault to a YAML-compatible object.
2973
+ */
2974
+ function defaultToYaml(def) {
2975
+ const result = {};
2976
+ if (def.beforeScript !== void 0) result.before_script = [...def.beforeScript];
2977
+ if (def.afterScript !== void 0) result.after_script = [...def.afterScript];
2978
+ if (def.timeout !== void 0) result.timeout = def.timeout;
2979
+ if (def.retry !== void 0) {
2980
+ const retry = { max: def.retry.max };
2981
+ if (def.retry.exitCodes !== void 0) retry.exit_codes = [...def.retry.exitCodes];
2982
+ result.retry = retry;
2983
+ }
2984
+ if (def.interruptible !== void 0) result.interruptible = def.interruptible;
2985
+ return result;
2986
+ }
2987
+ /**
2988
+ * Collect all include entries (component + local) from a GitlabTargetGraph.
2989
+ */
2990
+ function collectIncludes(graph) {
2991
+ const allIncludes = [];
2992
+ for (const inc of graph.includes) allIncludes.push({
2993
+ component: inc.component,
2994
+ ...Object.keys(inc.inputs).length > 0 ? { inputs: inc.inputs } : {}
2995
+ });
2996
+ if (graph.localIncludes) for (const inc of graph.localIncludes) allIncludes.push({
2997
+ local: inc.local,
2998
+ ...inc.inputs && Object.keys(inc.inputs).length > 0 ? { inputs: inc.inputs } : {}
2999
+ });
3000
+ return allIncludes;
3001
+ }
3002
+ /**
3003
+ * Convert a workflow rule to a YAML-compatible object.
3004
+ */
3005
+ function workflowRuleToYaml(rule) {
3006
+ const r = {};
3007
+ if (rule.if) r.if = rule.if;
3008
+ if (rule.changes) r.changes = rule.changes;
3009
+ if (rule.exists) r.exists = rule.exists;
3010
+ if (rule.variables) r.variables = rule.variables;
3011
+ if (rule.when) r.when = rule.when;
3012
+ return r;
3013
+ }
3014
+ /**
3015
+ * Convert a GitlabJob to a YAML-compatible object.
3016
+ */
3017
+ function jobToYaml(job) {
3018
+ const result = {
3019
+ stage: job.stage,
3020
+ script: job.script
3021
+ };
3022
+ const simpleFields = [
3023
+ ["image", job.image],
3024
+ ["timeout", job.timeout],
3025
+ ["interruptible", job.interruptible],
3026
+ ["resource_group", job.resourceGroup],
3027
+ ["trigger", job.trigger],
3028
+ ["release", job.release],
3029
+ ["pages", job.pages],
3030
+ ["when", job.when],
3031
+ ["start_in", job.start_in]
3032
+ ];
3033
+ for (const [yamlKey, value] of simpleFields) if (value !== void 0 && value !== null) result[yamlKey] = value;
3034
+ addComplexJobFields(result, job);
3035
+ return result;
3036
+ }
3037
+ /** Add artifacts, variables, and rules fields to the result object. */
3038
+ function assignJobArtifactsVarsRules(result, job) {
3039
+ if (job.artifacts) result.artifacts = artifactsToYaml(job.artifacts);
3040
+ if (job.variables) result.variables = job.variables;
3041
+ if (job.rules && job.rules.length > 0) result.rules = job.rules.map(ruleToYaml);
3042
+ }
3043
+ /** Add parallel/matrix field to the result object. */
3044
+ function assignJobParallel(result, job) {
3045
+ if (job.parallel?.matrix) result.parallel = { matrix: job.parallel.matrix.map((row) => {
3046
+ const wrapped = {};
3047
+ for (const [k, v] of Object.entries(row)) wrapped[k] = Array.isArray(v) ? v : [v];
3048
+ return wrapped;
3049
+ }) };
3050
+ else assignOptional(result, "parallel", job.parallel);
3051
+ }
3052
+ /** Add tags, id_tokens, services, environment, and cache fields. */
3053
+ function assignJobContainerFields(result, job) {
3054
+ if (job.tags && job.tags.length > 0) result.tags = [...job.tags];
3055
+ if (job.idTokens && Object.keys(job.idTokens).length > 0) result.id_tokens = idTokensToYaml(job.idTokens);
3056
+ if (job.services && job.services.length > 0) result.services = job.services.map(serviceToYaml);
3057
+ if (job.environment) result.environment = environmentToYaml(job.environment);
3058
+ if (job.cache) result.cache = cacheToYaml(job.cache);
3059
+ }
3060
+ /** Add fields requiring transformation to the result object. */
3061
+ function addComplexJobFields(result, job) {
3062
+ if (job.needs.length > 0) result.needs = [...job.needs];
3063
+ assignOptionalList(result, "before_script", job.beforeScript);
3064
+ assignOptionalList(result, "after_script", job.afterScript);
3065
+ assignJobArtifactsVarsRules(result, job);
3066
+ assignAllowFailure(result, job.allowFailure);
3067
+ assignRetry(result, job.retry);
3068
+ assignJobParallel(result, job);
3069
+ assignJobContainerFields(result, job);
3070
+ }
3071
+ function assignOptional(result, key, value) {
3072
+ if (value !== void 0) result[key] = value;
3073
+ }
3074
+ function assignOptionalList(result, key, value) {
3075
+ if (value && value.length > 0) result[key] = [...value];
3076
+ }
3077
+ function assignAllowFailure(result, value) {
3078
+ if (value === void 0) return;
3079
+ if (typeof value === "boolean") result.allow_failure = value;
3080
+ else result.allow_failure = { exit_codes: [...value.exitCodes] };
3081
+ }
3082
+ function assignRetry(result, value) {
3083
+ if (value === void 0) return;
3084
+ const retry = { max: Math.min(value.max, 2) };
3085
+ if (value.when && value.when.length > 0) retry.when = [...value.when];
3086
+ if (value.exitCodes && value.exitCodes.length > 0) retry.exit_codes = [...value.exitCodes];
3087
+ result.retry = retry;
3088
+ }
3089
+ /**
3090
+ * Convert an artifacts spec to a YAML-compatible object.
3091
+ */
3092
+ function artifactsToYaml(artifacts) {
3093
+ const result = {};
3094
+ if (artifacts.paths !== void 0) result.paths = [...artifacts.paths];
3095
+ if (artifacts.reports !== void 0) result.reports = artifacts.reports;
3096
+ if (artifacts.expireIn !== void 0) result.expire_in = artifacts.expireIn;
3097
+ if (artifacts.access !== void 0) result.access = artifacts.access;
3098
+ if (artifacts.when !== void 0) result.when = artifacts.when;
3099
+ return result;
3100
+ }
3101
+ /**
3102
+ * Convert a job rule to a YAML-compatible object.
3103
+ */
3104
+ function ruleToYaml(rule) {
3105
+ const r = {};
3106
+ if (rule.if !== void 0) r.if = rule.if;
3107
+ if (rule.when !== void 0) r.when = rule.when;
3108
+ if (rule.changes !== void 0) r.changes = [...rule.changes];
3109
+ if (rule.exists !== void 0) r.exists = [...rule.exists];
3110
+ if (rule.variables !== void 0) r.variables = rule.variables;
3111
+ return r;
3112
+ }
3113
+ /**
3114
+ * Convert id tokens to a YAML-compatible object.
3115
+ */
3116
+ function idTokensToYaml(idTokens) {
3117
+ const result = {};
3118
+ for (const [name, spec] of Object.entries(idTokens)) result[name] = { aud: spec.aud };
3119
+ return result;
3120
+ }
3121
+ /**
3122
+ * Convert a service to a YAML-compatible object.
3123
+ */
3124
+ function serviceToYaml(svc) {
3125
+ const s = { name: svc.name };
3126
+ if (svc.alias !== void 0) s.alias = svc.alias;
3127
+ if (svc.entrypoint !== void 0) s.entrypoint = [...svc.entrypoint];
3128
+ if (svc.command !== void 0) s.command = [...svc.command];
3129
+ if (svc.variables !== void 0) s.variables = svc.variables;
3130
+ return s;
3131
+ }
3132
+ /**
3133
+ * Convert an environment spec to a YAML-compatible object.
3134
+ */
3135
+ function environmentToYaml(env) {
3136
+ const result = { name: env.name };
3137
+ if (env.url !== void 0) result.url = env.url;
3138
+ if (env.action !== void 0) result.action = env.action;
3139
+ if (env.deploymentTier !== void 0) result.deployment_tier = env.deploymentTier;
3140
+ if (env.onStop !== void 0) result.on_stop = env.onStop;
3141
+ return result;
3142
+ }
3143
+ /**
3144
+ * Convert a cache spec to a YAML-compatible object.
3145
+ */
3146
+ function cacheToYaml(cache) {
3147
+ const result = {
3148
+ paths: [...cache.paths],
3149
+ key: cache.key
3150
+ };
3151
+ if (cache.policy !== void 0) result.policy = cache.policy;
3152
+ if (cache.fallbackKeys !== void 0) result.fallback_keys = [...cache.fallbackKeys];
3153
+ return result;
3154
+ }
3155
+ //#endregion
3156
+ //#region src/gitlab/target.ts
3157
+ /**
3158
+ * GitLab CI target — native lowering from Definition Graph.
3159
+ * Implements the Target contract: analyze → lower → emit.
3160
+ */
3161
+ var GitlabTarget = class {
3162
+ name = "gitlab";
3163
+ capabilities = gitlabCapabilities;
3164
+ /**
3165
+ * Analyze a Definition Graph against the GitLab capability manifest.
3166
+ * Returns diagnostics for unsupported or emulated capabilities.
3167
+ */
3168
+ analyze(graph) {
3169
+ return analyzeCapabilities(graph, [this.capabilities]);
3170
+ }
3171
+ /**
3172
+ * Lower a Definition Graph to a GitlabTargetGraph.
3173
+ * One GitLab job per reachable Step with needs, stages, and operation mapping.
3174
+ */
3175
+ lower(graph) {
3176
+ return lowerGitlab(graph);
3177
+ }
3178
+ /**
3179
+ * Emit a GitlabTargetGraph as YAML artifacts.
3180
+ * Produces one .gitlab-ci.yml file.
3181
+ */
3182
+ emit(targetGraph) {
3183
+ return emitGitlab(targetGraph);
3184
+ }
3185
+ /**
3186
+ * Compile a Definition Graph to generated artifacts and diagnostics.
3187
+ */
3188
+ compile(graph) {
3189
+ const diagnostics = this.analyze(graph);
3190
+ const targetGraph = this.lower(graph);
3191
+ return {
3192
+ artifacts: this.emit(targetGraph),
3193
+ diagnostics
3194
+ };
3195
+ }
3196
+ };
3197
+ /**
3198
+ * Convenience function: analyze → lower → emit in one call.
3199
+ */
3200
+ function compileGitlab(graph) {
3201
+ return new GitlabTarget().compile(graph);
3202
+ }
3203
+ //#endregion
3204
+ export { PluginError, analyzeCapabilities, compileGithub, compileGitlab, createPluginRegistry, defineSverkaPlugin, detectCapabilities, loadBundledRegistry, pinActionRef };
3205
+
3206
+ //# sourceMappingURL=index.mjs.map