@synergenius/flowweaver-pack-gitlab-ci 0.1.2 → 0.1.4

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/target.d.ts CHANGED
@@ -17,6 +17,8 @@ import type { ExportOptions, ExportArtifacts, DeployInstructions } from '@synerg
17
17
  export declare class GitLabCITarget extends BaseCICDTarget {
18
18
  readonly name = "gitlab-ci";
19
19
  readonly description = "GitLab CI/CD pipeline (.gitlab-ci.yml)";
20
+ /** Accumulated warnings for the current export run */
21
+ private _warnings;
20
22
  readonly deploySchema: {
21
23
  runner: {
22
24
  type: "string";
@@ -42,14 +44,25 @@ export declare class GitLabCITarget extends BaseCICDTarget {
42
44
  getDeployInstructions(_artifacts: ExportArtifacts): DeployInstructions;
43
45
  private renderPipelineYAML;
44
46
  /**
45
- * Derive stages from job dependency order.
46
- * Jobs with no deps → stage 1, jobs depending on stage 1 → stage 2, etc.
47
+ * Derive stages from @stage annotations or job dependency depth.
48
+ *
49
+ * When @stage annotations exist, jobs are grouped into named stages:
50
+ * - Jobs with an explicit `stage` field (set by buildJobGraph from @stage/@job)
51
+ * use that stage name directly.
52
+ * - The returned list preserves @stage declaration order.
53
+ *
54
+ * Without @stage annotations, falls back to using each job ID as its own stage
55
+ * (ordered by dependency).
47
56
  */
48
57
  private deriveStages;
49
58
  /**
50
- * Derive default image from @deploy annotations or built-in mappings.
59
+ * Derive default image from @runner annotation, @deploy annotations, or built-in mappings.
51
60
  */
52
61
  private deriveDefaultImage;
62
+ /**
63
+ * Map GitHub-style runner labels to Docker images.
64
+ */
65
+ private mapRunnerToImage;
53
66
  /**
54
67
  * Convert CI/CD triggers to GitLab CI workflow rules.
55
68
  */
package/dist/target.js CHANGED
@@ -20,6 +20,8 @@ import * as path from 'path';
20
20
  export class GitLabCITarget extends BaseCICDTarget {
21
21
  name = 'gitlab-ci';
22
22
  description = 'GitLab CI/CD pipeline (.gitlab-ci.yml)';
23
+ /** Accumulated warnings for the current export run */
24
+ _warnings = [];
23
25
  deploySchema = {
24
26
  runner: { type: 'string', description: 'Default Docker image', default: 'ubuntu:latest' },
25
27
  };
@@ -29,6 +31,7 @@ export class GitLabCITarget extends BaseCICDTarget {
29
31
  label: { type: 'string', description: 'Step display name' },
30
32
  };
31
33
  async generate(options) {
34
+ this._warnings = [];
32
35
  const filePath = path.resolve(options.sourceFile);
33
36
  const outputDir = path.resolve(options.outputDir);
34
37
  // Parse the workflow file to get AST
@@ -64,11 +67,18 @@ export class GitLabCITarget extends BaseCICDTarget {
64
67
  files.push(this.createFile(outputDir, 'SECRETS_SETUP.md', secretsDoc, 'other'));
65
68
  }
66
69
  }
70
+ // Warn about @concurrency (no direct GitLab equivalent)
71
+ for (const ast of targetWorkflows) {
72
+ if (ast.options?.cicd?.concurrency) {
73
+ this._warnings.push(`@concurrency: GitLab CI has no direct concurrency group equivalent. Use resource_group for serial execution or interruptible for auto-cancellation.`);
74
+ }
75
+ }
67
76
  return {
68
77
  files,
69
78
  target: this.name,
70
79
  workflowName: options.displayName || targetWorkflows[0].name,
71
80
  entryPoint: files[0].relativePath,
81
+ warnings: this._warnings.length > 0 ? this._warnings : undefined,
72
82
  };
73
83
  }
74
84
  getDeployInstructions(_artifacts) {
@@ -99,14 +109,40 @@ export class GitLabCITarget extends BaseCICDTarget {
99
109
  // ---------------------------------------------------------------------------
100
110
  renderPipelineYAML(ast, jobs) {
101
111
  const doc = {};
102
- // stages (derived from job dependency order)
103
- const stages = this.deriveStages(jobs);
112
+ // include: directive (from @includes)
113
+ const includes = ast.options?.cicd?.includes;
114
+ if (includes && includes.length > 0) {
115
+ doc.include = includes.map(inc => {
116
+ switch (inc.type) {
117
+ case 'local': return { local: inc.file };
118
+ case 'template': return { template: inc.file };
119
+ case 'remote': return { remote: inc.file };
120
+ case 'project': {
121
+ const obj = { project: inc.project || '', file: inc.file };
122
+ if (inc.ref)
123
+ obj.ref = inc.ref;
124
+ return obj;
125
+ }
126
+ default: return { local: inc.file };
127
+ }
128
+ });
129
+ }
130
+ // stages (from @stage annotations or derived from dependency depth)
131
+ const stages = this.deriveStages(jobs, ast);
104
132
  doc.stages = stages;
105
133
  // Default image
106
134
  const defaultImage = this.deriveDefaultImage(ast, jobs);
107
135
  if (defaultImage) {
108
136
  doc.default = { image: defaultImage };
109
137
  }
138
+ // Workflow-level variables
139
+ if (ast.options?.cicd?.variables && Object.keys(ast.options.cicd.variables).length > 0) {
140
+ doc.variables = { ...ast.options.cicd.variables };
141
+ }
142
+ // Workflow-level before_script
143
+ if (ast.options?.cicd?.beforeScript && ast.options.cicd.beforeScript.length > 0) {
144
+ doc.before_script = ast.options.cicd.beforeScript;
145
+ }
110
146
  // Workflow-level rules (from triggers)
111
147
  const rules = this.renderWorkflowRules(ast.options?.cicd?.triggers || []);
112
148
  if (rules.length > 0) {
@@ -123,29 +159,32 @@ export class GitLabCITarget extends BaseCICDTarget {
123
159
  });
124
160
  }
125
161
  /**
126
- * Derive stages from job dependency order.
127
- * Jobs with no deps → stage 1, jobs depending on stage 1 → stage 2, etc.
162
+ * Derive stages from @stage annotations or job dependency depth.
163
+ *
164
+ * When @stage annotations exist, jobs are grouped into named stages:
165
+ * - Jobs with an explicit `stage` field (set by buildJobGraph from @stage/@job)
166
+ * use that stage name directly.
167
+ * - The returned list preserves @stage declaration order.
168
+ *
169
+ * Without @stage annotations, falls back to using each job ID as its own stage
170
+ * (ordered by dependency).
128
171
  */
129
- deriveStages(jobs) {
130
- const stages = [];
131
- const assigned = new Map();
132
- // Assign stages based on dependency depth
133
- function getStage(jobId, jobs) {
134
- if (assigned.has(jobId))
135
- return assigned.get(jobId);
136
- const job = jobs.find((j) => j.id === jobId);
137
- if (!job || job.needs.length === 0) {
138
- assigned.set(jobId, jobId);
139
- return jobId;
172
+ deriveStages(jobs, ast) {
173
+ const declaredStages = ast?.options?.cicd?.stages;
174
+ // If @stage annotations exist, use them
175
+ if (declaredStages && declaredStages.length > 0) {
176
+ const stageNames = declaredStages.map(s => s.name);
177
+ // Collect any stages referenced by jobs that aren't in the declared list
178
+ for (const job of jobs) {
179
+ if (job.stage && !stageNames.includes(job.stage)) {
180
+ stageNames.push(job.stage);
181
+ }
140
182
  }
141
- // Stage is one after the latest dependency
142
- const depStages = job.needs.map((dep) => getStage(dep, jobs));
143
- assigned.set(jobId, jobId);
144
- return jobId;
183
+ return stageNames;
145
184
  }
146
- // Simple: use job IDs as stage names, ordered by dependency
185
+ // Fallback: use job IDs as stage names, ordered by dependency
186
+ const stages = [];
147
187
  for (const job of jobs) {
148
- getStage(job.id, jobs);
149
188
  if (!stages.includes(job.id)) {
150
189
  stages.push(job.id);
151
190
  }
@@ -153,10 +192,15 @@ export class GitLabCITarget extends BaseCICDTarget {
153
192
  return stages;
154
193
  }
155
194
  /**
156
- * Derive default image from @deploy annotations or built-in mappings.
195
+ * Derive default image from @runner annotation, @deploy annotations, or built-in mappings.
157
196
  */
158
- deriveDefaultImage(_ast, jobs) {
159
- // Check steps for @deploy gitlab-ci image or built-in mapping
197
+ deriveDefaultImage(ast, jobs) {
198
+ // Check @runner annotation first
199
+ const runner = ast.options?.cicd?.runner;
200
+ if (runner) {
201
+ return this.mapRunnerToImage(runner);
202
+ }
203
+ // Fall back to NODE_ACTION_MAP step images
160
204
  for (const job of jobs) {
161
205
  for (const step of job.steps) {
162
206
  const mapping = this.resolveActionMapping(step, 'gitlab-ci');
@@ -166,6 +210,17 @@ export class GitLabCITarget extends BaseCICDTarget {
166
210
  }
167
211
  return undefined;
168
212
  }
213
+ /**
214
+ * Map GitHub-style runner labels to Docker images.
215
+ */
216
+ mapRunnerToImage(runner) {
217
+ const imageMap = {
218
+ 'ubuntu-latest': 'ubuntu:latest',
219
+ 'ubuntu-22.04': 'ubuntu:22.04',
220
+ 'ubuntu-20.04': 'ubuntu:20.04',
221
+ };
222
+ return imageMap[runner] || runner;
223
+ }
169
224
  /**
170
225
  * Convert CI/CD triggers to GitLab CI workflow rules.
171
226
  */
@@ -215,24 +270,72 @@ export class GitLabCITarget extends BaseCICDTarget {
215
270
  }
216
271
  renderJob(job, ast, stages) {
217
272
  const jobObj = {};
218
- // stage
219
- jobObj.stage = job.id;
220
- // image (from runner or default)
221
- if (job.runner && job.runner !== 'ubuntu-latest') {
222
- // GitLab uses Docker images, not runner labels
223
- // Map common GitHub runners to Docker images
224
- const imageMap = {
225
- 'ubuntu-latest': 'ubuntu:latest',
226
- 'ubuntu-22.04': 'ubuntu:22.04',
227
- 'ubuntu-20.04': 'ubuntu:20.04',
228
- };
229
- const image = imageMap[job.runner] || job.runner;
230
- jobObj.image = image;
273
+ // extends (from @job extends=".template-name")
274
+ if (job.extends) {
275
+ jobObj.extends = job.extends;
276
+ }
277
+ // stage (use explicit stage from @stage assignment, or fall back to job ID)
278
+ jobObj.stage = job.stage || job.id;
279
+ // image (from per-job runner override via @job X runner=Y)
280
+ if (job.runner) {
281
+ jobObj.image = this.mapRunnerToImage(job.runner);
282
+ }
283
+ // tags (from @job tags or @tags)
284
+ if (job.tags && job.tags.length > 0) {
285
+ jobObj.tags = job.tags;
286
+ }
287
+ // matrix strategy (parallel: matrix:)
288
+ if (job.matrix) {
289
+ const matrixEntries = [];
290
+ if (job.matrix.dimensions && Object.keys(job.matrix.dimensions).length > 0) {
291
+ matrixEntries.push(job.matrix.dimensions);
292
+ }
293
+ if (job.matrix.include) {
294
+ for (const inc of job.matrix.include) {
295
+ matrixEntries.push(Object.fromEntries(Object.entries(inc).map(([k, v]) => [k, [v]])));
296
+ }
297
+ }
298
+ if (matrixEntries.length > 0) {
299
+ jobObj.parallel = { matrix: matrixEntries };
300
+ }
231
301
  }
232
302
  // needs (for DAG mode instead of stage-based ordering)
233
303
  if (job.needs.length > 0) {
234
304
  jobObj.needs = job.needs;
235
305
  }
306
+ // retry (from @job retry)
307
+ if (job.retry !== undefined) {
308
+ jobObj.retry = { max: job.retry };
309
+ }
310
+ // allow_failure (from @job allow_failure)
311
+ if (job.allowFailure) {
312
+ jobObj.allow_failure = true;
313
+ }
314
+ // timeout (from @job timeout)
315
+ if (job.timeout) {
316
+ jobObj.timeout = job.timeout;
317
+ }
318
+ // rules (from @job rules)
319
+ if (job.rules && job.rules.length > 0) {
320
+ jobObj.rules = job.rules.map(rule => {
321
+ const ruleObj = {};
322
+ if (rule.if)
323
+ ruleObj.if = rule.if;
324
+ if (rule.when)
325
+ ruleObj.when = rule.when;
326
+ if (rule.allowFailure)
327
+ ruleObj.allow_failure = true;
328
+ if (rule.changes)
329
+ ruleObj.changes = rule.changes;
330
+ if (rule.variables)
331
+ ruleObj.variables = rule.variables;
332
+ return ruleObj;
333
+ });
334
+ }
335
+ // coverage (from @job coverage)
336
+ if (job.coverage) {
337
+ jobObj.coverage = job.coverage;
338
+ }
236
339
  // environment
237
340
  if (job.environment) {
238
341
  const envConfig = ast.options?.cicd?.environments?.find((e) => e.name === job.environment);
@@ -242,7 +345,6 @@ export class GitLabCITarget extends BaseCICDTarget {
242
345
  if (envConfig?.reviewers)
243
346
  envObj.deployment_tier = 'production';
244
347
  jobObj.environment = envObj;
245
- // Protected environments require manual approval in GitLab
246
348
  if (envConfig?.reviewers) {
247
349
  jobObj.when = 'manual';
248
350
  }
@@ -251,46 +353,54 @@ export class GitLabCITarget extends BaseCICDTarget {
251
353
  if (job.services && job.services.length > 0) {
252
354
  jobObj.services = job.services.map((svc) => {
253
355
  const svcObj = { name: svc.image };
254
- if (svc.ports) {
255
- // GitLab services expose the first port automatically
256
- // Additional port mapping needs alias
257
- }
258
356
  return svcObj;
259
357
  });
260
358
  }
261
- // variables (from secrets)
359
+ // variables (merge secrets + job-level variables)
360
+ const variables = {};
262
361
  if (job.secrets.length > 0) {
263
- // In GitLab, CI/CD variables are automatically available
264
- // But we document them for clarity
265
- const variables = {};
266
362
  for (const secret of job.secrets) {
267
363
  variables[secret] = `$${secret}`;
268
364
  }
365
+ }
366
+ if (job.variables) {
367
+ Object.assign(variables, job.variables);
368
+ }
369
+ if (Object.keys(variables).length > 0) {
269
370
  jobObj.variables = variables;
270
371
  }
372
+ // before_script (from @job or @before_script)
373
+ if (job.beforeScript && job.beforeScript.length > 0) {
374
+ jobObj.before_script = job.beforeScript;
375
+ }
271
376
  // cache
272
377
  if (job.cache) {
273
378
  jobObj.cache = this.renderCache(job.cache);
274
379
  }
275
- // artifacts (upload)
380
+ // artifacts (upload + reports)
381
+ const artifactsObj = {};
276
382
  if (job.uploadArtifacts && job.uploadArtifacts.length > 0) {
277
- const paths = job.uploadArtifacts.map((a) => a.path);
383
+ artifactsObj.paths = job.uploadArtifacts.map((a) => a.path);
278
384
  const expiry = job.uploadArtifacts[0].retention
279
385
  ? `${job.uploadArtifacts[0].retention} days`
280
386
  : '1 week';
281
- jobObj.artifacts = {
282
- paths,
283
- expire_in: expiry,
284
- };
387
+ artifactsObj.expire_in = expiry;
388
+ }
389
+ if (job.reports && job.reports.length > 0) {
390
+ const reports = {};
391
+ for (const report of job.reports) {
392
+ reports[report.type] = report.path;
393
+ }
394
+ artifactsObj.reports = reports;
395
+ }
396
+ if (Object.keys(artifactsObj).length > 0) {
397
+ jobObj.artifacts = artifactsObj;
285
398
  }
286
399
  // script (the actual steps)
287
400
  const script = [];
288
- // Download artifacts (GitLab handles this automatically via `needs:`)
289
- // but we add a comment for clarity if explicit artifacts are expected
290
401
  if (job.downloadArtifacts && job.downloadArtifacts.length > 0) {
291
402
  script.push(`# Artifacts from: ${job.downloadArtifacts.join(', ')} (downloaded automatically via needs:)`);
292
403
  }
293
- // Step scripts
294
404
  for (const step of job.steps) {
295
405
  const stepScript = this.renderStepScript(step);
296
406
  script.push(...stepScript);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flowweaver-pack-gitlab-ci",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "GitLab CI/CD export target for Flow Weaver",
5
5
  "keywords": [
6
6
  "flowweaver-marketplace-pack",
@@ -35,9 +35,9 @@
35
35
  "prepublishOnly": "npm run build"
36
36
  },
37
37
  "devDependencies": {
38
- "typescript": "^5.0.0",
39
- "@synergenius/flow-weaver": "^0.17.1",
40
- "@types/node": "^20.0.0"
38
+ "@synergenius/flow-weaver": "^0.17.4",
39
+ "@types/node": "^20.0.0",
40
+ "typescript": "^5.0.0"
41
41
  },
42
42
  "license": "SEE LICENSE IN LICENSE",
43
43
  "repository": {