@synergenius/flowweaver-pack-gitlab-ci 0.1.1 → 0.1.3

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/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @synergenius/flowweaver-pack-gitlab-ci
2
+
3
+ GitLab CI/CD export target for [Flow Weaver](https://github.com/synergenius-fw/flow-weaver).
4
+
5
+ Generates native `.gitlab-ci.yml` files from Flow Weaver CI/CD workflows. No runtime dependency — outputs pure GitLab CI YAML.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @synergenius/flowweaver-pack-gitlab-ci
11
+ ```
12
+
13
+ This package is a **marketplace pack** — once installed, Flow Weaver automatically discovers it via `createTargetRegistry()`.
14
+
15
+ ## Usage
16
+
17
+ ### CLI
18
+
19
+ ```bash
20
+ # Export a CI/CD workflow as GitLab CI YAML
21
+ npx flow-weaver export my-pipeline.ts --target gitlab-ci
22
+ ```
23
+
24
+ ### Programmatic
25
+
26
+ ```typescript
27
+ import { createTargetRegistry } from '@synergenius/flow-weaver/deployment';
28
+
29
+ const registry = await createTargetRegistry(process.cwd());
30
+ const gitlab = registry.get('gitlab-ci');
31
+
32
+ const artifacts = await gitlab.generate({
33
+ sourceFile: 'my-pipeline.ts',
34
+ workflowName: 'myPipeline',
35
+ displayName: 'my-pipeline',
36
+ outputDir: './dist/gitlab-ci',
37
+ });
38
+ ```
39
+
40
+ ## What it generates
41
+
42
+ - `.gitlab-ci.yml` — Native GitLab CI configuration
43
+ - `SECRETS_SETUP.md` — Documentation for required CI/CD variables
44
+
45
+ ### Mapping
46
+
47
+ | Flow Weaver | GitLab CI |
48
+ |-------------|-----------|
49
+ | `[job: "name"]` annotation | Job with `stage:` |
50
+ | `@path` dependencies | Stage ordering |
51
+ | `@secret NAME` | `$NAME` variable |
52
+ | `@cache` | Native `cache:` keyword |
53
+ | `@artifact` | Native `artifacts:` keyword |
54
+ | `@trigger push` | `rules:` conditions |
55
+ | `@environment` | `environment:` with optional `when: manual` |
56
+
57
+ ## Requirements
58
+
59
+ - `@synergenius/flow-weaver` >= 0.14.0
60
+
61
+ ## License
62
+
63
+ See [LICENSE](./LICENSE).
package/dist/target.d.ts CHANGED
@@ -42,8 +42,15 @@ export declare class GitLabCITarget extends BaseCICDTarget {
42
42
  getDeployInstructions(_artifacts: ExportArtifacts): DeployInstructions;
43
43
  private renderPipelineYAML;
44
44
  /**
45
- * Derive stages from job dependency order.
46
- * Jobs with no deps → stage 1, jobs depending on stage 1 → stage 2, etc.
45
+ * Derive stages from @stage annotations or job dependency depth.
46
+ *
47
+ * When @stage annotations exist, jobs are grouped into named stages:
48
+ * - Jobs with an explicit `stage` field (set by buildJobGraph from @stage/@job)
49
+ * use that stage name directly.
50
+ * - The returned list preserves @stage declaration order.
51
+ *
52
+ * Without @stage annotations, falls back to using each job ID as its own stage
53
+ * (ordered by dependency).
47
54
  */
48
55
  private deriveStages;
49
56
  /**
package/dist/target.js CHANGED
@@ -99,14 +99,40 @@ export class GitLabCITarget extends BaseCICDTarget {
99
99
  // ---------------------------------------------------------------------------
100
100
  renderPipelineYAML(ast, jobs) {
101
101
  const doc = {};
102
- // stages (derived from job dependency order)
103
- const stages = this.deriveStages(jobs);
102
+ // include: directive (from @includes)
103
+ const includes = ast.options?.cicd?.includes;
104
+ if (includes && includes.length > 0) {
105
+ doc.include = includes.map(inc => {
106
+ switch (inc.type) {
107
+ case 'local': return { local: inc.file };
108
+ case 'template': return { template: inc.file };
109
+ case 'remote': return { remote: inc.file };
110
+ case 'project': {
111
+ const obj = { project: inc.project || '', file: inc.file };
112
+ if (inc.ref)
113
+ obj.ref = inc.ref;
114
+ return obj;
115
+ }
116
+ default: return { local: inc.file };
117
+ }
118
+ });
119
+ }
120
+ // stages (from @stage annotations or derived from dependency depth)
121
+ const stages = this.deriveStages(jobs, ast);
104
122
  doc.stages = stages;
105
123
  // Default image
106
124
  const defaultImage = this.deriveDefaultImage(ast, jobs);
107
125
  if (defaultImage) {
108
126
  doc.default = { image: defaultImage };
109
127
  }
128
+ // Workflow-level variables
129
+ if (ast.options?.cicd?.variables && Object.keys(ast.options.cicd.variables).length > 0) {
130
+ doc.variables = { ...ast.options.cicd.variables };
131
+ }
132
+ // Workflow-level before_script
133
+ if (ast.options?.cicd?.beforeScript && ast.options.cicd.beforeScript.length > 0) {
134
+ doc.before_script = ast.options.cicd.beforeScript;
135
+ }
110
136
  // Workflow-level rules (from triggers)
111
137
  const rules = this.renderWorkflowRules(ast.options?.cicd?.triggers || []);
112
138
  if (rules.length > 0) {
@@ -123,29 +149,32 @@ export class GitLabCITarget extends BaseCICDTarget {
123
149
  });
124
150
  }
125
151
  /**
126
- * Derive stages from job dependency order.
127
- * Jobs with no deps → stage 1, jobs depending on stage 1 → stage 2, etc.
152
+ * Derive stages from @stage annotations or job dependency depth.
153
+ *
154
+ * When @stage annotations exist, jobs are grouped into named stages:
155
+ * - Jobs with an explicit `stage` field (set by buildJobGraph from @stage/@job)
156
+ * use that stage name directly.
157
+ * - The returned list preserves @stage declaration order.
158
+ *
159
+ * Without @stage annotations, falls back to using each job ID as its own stage
160
+ * (ordered by dependency).
128
161
  */
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;
162
+ deriveStages(jobs, ast) {
163
+ const declaredStages = ast?.options?.cicd?.stages;
164
+ // If @stage annotations exist, use them
165
+ if (declaredStages && declaredStages.length > 0) {
166
+ const stageNames = declaredStages.map(s => s.name);
167
+ // Collect any stages referenced by jobs that aren't in the declared list
168
+ for (const job of jobs) {
169
+ if (job.stage && !stageNames.includes(job.stage)) {
170
+ stageNames.push(job.stage);
171
+ }
140
172
  }
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;
173
+ return stageNames;
145
174
  }
146
- // Simple: use job IDs as stage names, ordered by dependency
175
+ // Fallback: use job IDs as stage names, ordered by dependency
176
+ const stages = [];
147
177
  for (const job of jobs) {
148
- getStage(job.id, jobs);
149
178
  if (!stages.includes(job.id)) {
150
179
  stages.push(job.id);
151
180
  }
@@ -215,12 +244,14 @@ export class GitLabCITarget extends BaseCICDTarget {
215
244
  }
216
245
  renderJob(job, ast, stages) {
217
246
  const jobObj = {};
218
- // stage
219
- jobObj.stage = job.id;
247
+ // extends (from @job extends=".template-name")
248
+ if (job.extends) {
249
+ jobObj.extends = job.extends;
250
+ }
251
+ // stage (use explicit stage from @stage assignment, or fall back to job ID)
252
+ jobObj.stage = job.stage || job.id;
220
253
  // image (from runner or default)
221
254
  if (job.runner && job.runner !== 'ubuntu-latest') {
222
- // GitLab uses Docker images, not runner labels
223
- // Map common GitHub runners to Docker images
224
255
  const imageMap = {
225
256
  'ubuntu-latest': 'ubuntu:latest',
226
257
  'ubuntu-22.04': 'ubuntu:22.04',
@@ -229,10 +260,47 @@ export class GitLabCITarget extends BaseCICDTarget {
229
260
  const image = imageMap[job.runner] || job.runner;
230
261
  jobObj.image = image;
231
262
  }
263
+ // tags (from @job tags or @tags)
264
+ if (job.tags && job.tags.length > 0) {
265
+ jobObj.tags = job.tags;
266
+ }
232
267
  // needs (for DAG mode instead of stage-based ordering)
233
268
  if (job.needs.length > 0) {
234
269
  jobObj.needs = job.needs;
235
270
  }
271
+ // retry (from @job retry)
272
+ if (job.retry !== undefined) {
273
+ jobObj.retry = { max: job.retry };
274
+ }
275
+ // allow_failure (from @job allow_failure)
276
+ if (job.allowFailure) {
277
+ jobObj.allow_failure = true;
278
+ }
279
+ // timeout (from @job timeout)
280
+ if (job.timeout) {
281
+ jobObj.timeout = job.timeout;
282
+ }
283
+ // rules (from @job rules)
284
+ if (job.rules && job.rules.length > 0) {
285
+ jobObj.rules = job.rules.map(rule => {
286
+ const ruleObj = {};
287
+ if (rule.if)
288
+ ruleObj.if = rule.if;
289
+ if (rule.when)
290
+ ruleObj.when = rule.when;
291
+ if (rule.allowFailure)
292
+ ruleObj.allow_failure = true;
293
+ if (rule.changes)
294
+ ruleObj.changes = rule.changes;
295
+ if (rule.variables)
296
+ ruleObj.variables = rule.variables;
297
+ return ruleObj;
298
+ });
299
+ }
300
+ // coverage (from @job coverage)
301
+ if (job.coverage) {
302
+ jobObj.coverage = job.coverage;
303
+ }
236
304
  // environment
237
305
  if (job.environment) {
238
306
  const envConfig = ast.options?.cicd?.environments?.find((e) => e.name === job.environment);
@@ -242,7 +310,6 @@ export class GitLabCITarget extends BaseCICDTarget {
242
310
  if (envConfig?.reviewers)
243
311
  envObj.deployment_tier = 'production';
244
312
  jobObj.environment = envObj;
245
- // Protected environments require manual approval in GitLab
246
313
  if (envConfig?.reviewers) {
247
314
  jobObj.when = 'manual';
248
315
  }
@@ -251,46 +318,54 @@ export class GitLabCITarget extends BaseCICDTarget {
251
318
  if (job.services && job.services.length > 0) {
252
319
  jobObj.services = job.services.map((svc) => {
253
320
  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
321
  return svcObj;
259
322
  });
260
323
  }
261
- // variables (from secrets)
324
+ // variables (merge secrets + job-level variables)
325
+ const variables = {};
262
326
  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
327
  for (const secret of job.secrets) {
267
328
  variables[secret] = `$${secret}`;
268
329
  }
330
+ }
331
+ if (job.variables) {
332
+ Object.assign(variables, job.variables);
333
+ }
334
+ if (Object.keys(variables).length > 0) {
269
335
  jobObj.variables = variables;
270
336
  }
337
+ // before_script (from @job or @before_script)
338
+ if (job.beforeScript && job.beforeScript.length > 0) {
339
+ jobObj.before_script = job.beforeScript;
340
+ }
271
341
  // cache
272
342
  if (job.cache) {
273
343
  jobObj.cache = this.renderCache(job.cache);
274
344
  }
275
- // artifacts (upload)
345
+ // artifacts (upload + reports)
346
+ const artifactsObj = {};
276
347
  if (job.uploadArtifacts && job.uploadArtifacts.length > 0) {
277
- const paths = job.uploadArtifacts.map((a) => a.path);
348
+ artifactsObj.paths = job.uploadArtifacts.map((a) => a.path);
278
349
  const expiry = job.uploadArtifacts[0].retention
279
350
  ? `${job.uploadArtifacts[0].retention} days`
280
351
  : '1 week';
281
- jobObj.artifacts = {
282
- paths,
283
- expire_in: expiry,
284
- };
352
+ artifactsObj.expire_in = expiry;
353
+ }
354
+ if (job.reports && job.reports.length > 0) {
355
+ const reports = {};
356
+ for (const report of job.reports) {
357
+ reports[report.type] = report.path;
358
+ }
359
+ artifactsObj.reports = reports;
360
+ }
361
+ if (Object.keys(artifactsObj).length > 0) {
362
+ jobObj.artifacts = artifactsObj;
285
363
  }
286
364
  // script (the actual steps)
287
365
  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
366
  if (job.downloadArtifacts && job.downloadArtifacts.length > 0) {
291
367
  script.push(`# Artifacts from: ${job.downloadArtifacts.join(', ')} (downloaded automatically via needs:)`);
292
368
  }
293
- // Step scripts
294
369
  for (const step of job.steps) {
295
370
  const stepScript = this.renderStepScript(step);
296
371
  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.1",
3
+ "version": "0.1.3",
4
4
  "description": "GitLab CI/CD export target for Flow Weaver",
5
5
  "keywords": [
6
6
  "flowweaver-marketplace-pack",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "typescript": "^5.0.0",
39
- "@synergenius/flow-weaver": "^0.14.2",
39
+ "@synergenius/flow-weaver": "^0.17.3",
40
40
  "@types/node": "^20.0.0"
41
41
  },
42
42
  "license": "SEE LICENSE IN LICENSE",