@synergenius/flow-weaver 0.17.3 → 0.17.5

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.
@@ -148,14 +148,17 @@ export function parseTriggerLine(input, warnings) {
148
148
  parserInstance.input = lexResult.tokens;
149
149
  const cst = parserInstance.triggerLine();
150
150
  if (parserInstance.errors.length > 0) {
151
- const firstError = parserInstance.errors[0];
152
- const truncatedInput = input.length > 60 ? input.substring(0, 60) + '...' : input;
153
- warnings.push(`Failed to parse trigger line: "${truncatedInput}"\n` +
154
- ` Error: ${firstError.message}\n` +
155
- ` Expected format: @trigger event="name" or @trigger cron="expr"`);
151
+ // Don't warn here — return null so domain-specific handlers (e.g. CI/CD)
152
+ // get a chance to parse the trigger. The caller can warn if nothing handles it.
156
153
  return null;
157
154
  }
158
155
  const result = visitorInstance.visit(cst);
156
+ // Empty result means the parser consumed @trigger but found no event=/cron= assignments.
157
+ // Return null so the caller can delegate to domain-specific handlers (e.g. CI/CD triggers
158
+ // like @trigger push, @trigger pull_request, etc.)
159
+ if (!result.event && !result.cron) {
160
+ return null;
161
+ }
159
162
  // Validate cron expression
160
163
  if (result.cron && !CRON_REGEX.test(result.cron)) {
161
164
  warnings.push(`Invalid cron expression: "${result.cron}". Expected 5 fields (minute hour day month weekday).`);
@@ -103,7 +103,9 @@ export async function exportCommand(input, options) {
103
103
  logger.section('Handler Preview');
104
104
  const handlerFile = result.files.find((f) => f.path.endsWith('handler.ts') ||
105
105
  f.path.endsWith(`${result.workflow}.ts`) ||
106
- f.path.endsWith('index.ts'));
106
+ f.path.endsWith('index.ts') ||
107
+ f.path.endsWith('.yml') ||
108
+ f.path.endsWith('.yaml'));
107
109
  if (handlerFile) {
108
110
  // Show first 40 lines of handler
109
111
  const lines = handlerFile.content.split('\n');
@@ -114,6 +116,14 @@ export async function exportCommand(input, options) {
114
116
  }
115
117
  }
116
118
  }
119
+ // Show warnings about unsupported annotations
120
+ if (result.warnings && result.warnings.length > 0) {
121
+ logger.newline();
122
+ logger.section('Warnings');
123
+ for (const warning of result.warnings) {
124
+ logger.warn(warning);
125
+ }
126
+ }
117
127
  // Get deploy instructions from the target
118
128
  logger.newline();
119
129
  logger.section('Next Steps');
@@ -17051,16 +17051,12 @@ function parseTriggerLine(input, warnings) {
17051
17051
  parserInstance8.input = lexResult.tokens;
17052
17052
  const cst = parserInstance8.triggerLine();
17053
17053
  if (parserInstance8.errors.length > 0) {
17054
- const firstError = parserInstance8.errors[0];
17055
- const truncatedInput = input.length > 60 ? input.substring(0, 60) + "..." : input;
17056
- warnings.push(
17057
- `Failed to parse trigger line: "${truncatedInput}"
17058
- Error: ${firstError.message}
17059
- Expected format: @trigger event="name" or @trigger cron="expr"`
17060
- );
17061
17054
  return null;
17062
17055
  }
17063
17056
  const result = visitorInstance8.visit(cst);
17057
+ if (!result.event && !result.cron) {
17058
+ return null;
17059
+ }
17064
17060
  if (result.cron && !CRON_REGEX.test(result.cron)) {
17065
17061
  warnings.push(`Invalid cron expression: "${result.cron}". Expected 5 fields (minute hour day month weekday).`);
17066
17062
  }
@@ -25611,7 +25607,7 @@ var VERSION2;
25611
25607
  var init_generated_version = __esm({
25612
25608
  "src/generated-version.ts"() {
25613
25609
  "use strict";
25614
- VERSION2 = "0.17.3";
25610
+ VERSION2 = "0.17.5";
25615
25611
  }
25616
25612
  });
25617
25613
 
@@ -35963,6 +35959,12 @@ var init_jsdoc_parser = __esm({
35963
35959
  const comment = (tag.getCommentText() || "").trim();
35964
35960
  const result = parseTriggerLine(`@trigger ${comment}`, warnings);
35965
35961
  if (result) {
35962
+ const cicdKeywords = ["push", "pull_request", "dispatch", "tag", "schedule"];
35963
+ if (result.event && cicdKeywords.includes(result.event)) {
35964
+ warnings.push(
35965
+ `@trigger event="${result.event}" is treated as an Inngest event trigger, not a CI/CD trigger. For CI/CD, use: @trigger ${result.event}`
35966
+ );
35967
+ }
35966
35968
  config2.trigger = config2.trigger || {};
35967
35969
  if (result.event) config2.trigger.event = result.event;
35968
35970
  if (result.cron) config2.trigger.cron = result.cron;
@@ -36677,7 +36679,8 @@ var init_parser2 = __esm({
36677
36679
  nodeTypes.push(...inferredNodeTypes);
36678
36680
  const workflows = this.extractWorkflows(sourceFile, nodeTypes, filePath, errors2, warnings);
36679
36681
  const patterns = this.extractPatterns(sourceFile, nodeTypes, filePath, errors2, warnings);
36680
- const result = { workflows, nodeTypes, patterns, errors: errors2, warnings };
36682
+ const dedupedWarnings = [...new Set(warnings)];
36683
+ const result = { workflows, nodeTypes, patterns, errors: errors2, warnings: dedupedWarnings };
36681
36684
  this.project.removeSourceFile(sourceFile);
36682
36685
  if (!externalNodeTypes?.length) {
36683
36686
  this.parseCache.set(filePath, {
@@ -36717,12 +36720,13 @@ var init_parser2 = __esm({
36717
36720
  const workflows = this.extractWorkflows(sourceFile, nodeTypes, virtualPath, errors2, warnings);
36718
36721
  const patterns = this.extractPatterns(sourceFile, nodeTypes, virtualPath, errors2, warnings);
36719
36722
  this.project.removeSourceFile(sourceFile);
36723
+ const dedupedWarnings = [...new Set(warnings)];
36720
36724
  return {
36721
36725
  workflows,
36722
36726
  nodeTypes,
36723
36727
  patterns,
36724
36728
  errors: errors2,
36725
- warnings
36729
+ warnings: dedupedWarnings
36726
36730
  };
36727
36731
  }
36728
36732
  clearCache() {
@@ -71463,7 +71467,7 @@ var init_base_target = __esm({
71463
71467
  },
71464
71468
  "slack-notify": {
71465
71469
  githubAction: "slackapi/slack-github-action@v1",
71466
- gitlabScript: ['curl -X POST -H "Content-type: application/json" --data "{\\"text\\":\\"Pipeline complete\\"}" $SLACK_WEBHOOK_URL'],
71470
+ gitlabScript: [`curl -X POST -H 'Content-type: application/json' --data '{"text":"Pipeline complete"}' $SLACK_WEBHOOK_URL`],
71467
71471
  label: "Send Slack notification"
71468
71472
  },
71469
71473
  "health-check": {
@@ -71575,11 +71579,10 @@ var init_base_target = __esm({
71575
71579
  const stages = ast.options?.cicd?.stages;
71576
71580
  if (stages && stages.length > 0) {
71577
71581
  const depthMap = this.computeJobDepths(jobs);
71578
- for (const jc of jobConfigs || []) {
71579
- const job = jobs.find((j) => j.id === jc.id);
71580
- if (job && !job.stage) {
71582
+ for (const job of jobs) {
71583
+ if (!job.stage) {
71581
71584
  for (const s of stages) {
71582
- if (jc.id === s.name || jc.id.startsWith(s.name + "-") || jc.id.startsWith(s.name + "_")) {
71585
+ if (job.id === s.name || job.id.startsWith(s.name + "-") || job.id.startsWith(s.name + "_")) {
71583
71586
  job.stage = s.name;
71584
71587
  break;
71585
71588
  }
@@ -73354,6 +73357,18 @@ function parseJob(text, d, warnings) {
73354
73357
  jc.rules = jc.rules || [];
73355
73358
  jc.rules.push({ if: value2 });
73356
73359
  break;
73360
+ case "when": {
73361
+ jc.rules = jc.rules || [];
73362
+ if (jc.rules.length === 0) jc.rules.push({});
73363
+ jc.rules[jc.rules.length - 1].when = value2;
73364
+ break;
73365
+ }
73366
+ case "changes": {
73367
+ jc.rules = jc.rules || [];
73368
+ if (jc.rules.length === 0) jc.rules.push({});
73369
+ jc.rules[jc.rules.length - 1].changes = value2.split(",").map((s) => s.trim()).filter(Boolean);
73370
+ break;
73371
+ }
73357
73372
  case "reports": {
73358
73373
  jc.reports = jc.reports || [];
73359
73374
  for (const pair of value2.split(",")) {
@@ -73794,6 +73809,18 @@ function deploySsh(sshKey: string = ''): { result: string } { return { result: '
73794
73809
  */
73795
73810
  function deployS3(accessKey: string = '', secretKey: string = ''): { result: string } { return { result: 'deployed' }; }
73796
73811
  ` : "";
73812
+ const stageAnnotations = hasDeploy ? ` * @stage test
73813
+ * @stage build
73814
+ * @stage deploy
73815
+ ` : ` * @stage test
73816
+ * @stage build
73817
+ `;
73818
+ const jobAnnotations = hasDeploy ? ` * @job test retry=1
73819
+ * @job build timeout="10m"
73820
+ * @job deploy allow_failure=false
73821
+ ` : ` * @job test retry=1
73822
+ * @job build timeout="10m"
73823
+ `;
73797
73824
  return `/** @flowWeaver nodeType
73798
73825
  * @expression
73799
73826
  * @label Checkout code
@@ -73832,6 +73859,7 @@ ${deployStub}
73832
73859
  * @secret NPM_TOKEN - npm auth token${deploySecret}
73833
73860
  * @cache npm key="package-lock.json"
73834
73861
  *
73862
+ ${stageAnnotations}${jobAnnotations} *
73835
73863
  * @node co checkout [job: "test"] [position: 270 0]
73836
73864
  * @node setup setupNode [job: "test"] [position: 540 0]
73837
73865
  * @node install npmInstall [job: "test"] [position: 810 0]
@@ -73996,6 +74024,15 @@ var cicdMultiEnvTemplate = {
73996
74024
  const name = opts.workflowName || "multiEnvPipeline";
73997
74025
  const envs = (opts.config?.environments || "staging,production").split(",").map((e) => e.trim());
73998
74026
  const envAnnotations = envs.map((env) => ` * @environment ${env} url="https://${env}.example.com"`).join("\n");
74027
+ const stageAnnotations = ` * @stage test
74028
+ * @stage build
74029
+ * @stage deploy
74030
+ `;
74031
+ const jobAnnotations = [
74032
+ ` * @job test retry=1`,
74033
+ ` * @job build timeout="10m"`,
74034
+ ...envs.map((env) => ` * @job deploy-${env} allow_failure=${env === "staging" ? "true" : "false"}`)
74035
+ ].join("\n");
73999
74036
  let x = 270;
74000
74037
  const nodeAnnotations = [];
74001
74038
  const pathParts = ["Start"];
@@ -74048,6 +74085,8 @@ ${envAnnotations}
74048
74085
  * @cache npm key="package-lock.json"
74049
74086
  * @artifact dist path="dist/" retention=3
74050
74087
  *
74088
+ ${stageAnnotations}${jobAnnotations}
74089
+ *
74051
74090
  ${nodeAnnotations.join("\n")}
74052
74091
  *
74053
74092
  * @path ${pathParts.join(" -> ")}
@@ -107919,7 +107958,8 @@ async function exportSingleWorkflowViaRegistry(target, inputPath, outputDir, isD
107919
107958
  target: options.target,
107920
107959
  files,
107921
107960
  workflow: workflow.name,
107922
- description: workflow.description
107961
+ description: workflow.description,
107962
+ warnings: artifacts.warnings
107923
107963
  };
107924
107964
  }
107925
107965
  async function exportMultiWorkflowViaRegistry(target, inputPath, outputDir, isDryRun, options) {
@@ -107981,7 +108021,8 @@ async function exportMultiWorkflowViaRegistry(target, inputPath, outputDir, isDr
107981
108021
  target: options.target,
107982
108022
  files,
107983
108023
  workflow: serviceName,
107984
- workflows: selectedWorkflows.map((w) => w.name)
108024
+ workflows: selectedWorkflows.map((w) => w.name),
108025
+ warnings: artifacts.warnings
107985
108026
  };
107986
108027
  }
107987
108028
  async function compileToOutput(inputPath, functionName, outputDir, production) {
@@ -108070,7 +108111,7 @@ async function exportCommand(input, options) {
108070
108111
  logger.newline();
108071
108112
  logger.section("Handler Preview");
108072
108113
  const handlerFile = result.files.find(
108073
- (f) => f.path.endsWith("handler.ts") || f.path.endsWith(`${result.workflow}.ts`) || f.path.endsWith("index.ts")
108114
+ (f) => f.path.endsWith("handler.ts") || f.path.endsWith(`${result.workflow}.ts`) || f.path.endsWith("index.ts") || f.path.endsWith(".yml") || f.path.endsWith(".yaml")
108074
108115
  );
108075
108116
  if (handlerFile) {
108076
108117
  const lines = handlerFile.content.split("\n");
@@ -108081,6 +108122,13 @@ async function exportCommand(input, options) {
108081
108122
  }
108082
108123
  }
108083
108124
  }
108125
+ if (result.warnings && result.warnings.length > 0) {
108126
+ logger.newline();
108127
+ logger.section("Warnings");
108128
+ for (const warning of result.warnings) {
108129
+ logger.warn(warning);
108130
+ }
108131
+ }
108084
108132
  logger.newline();
108085
108133
  logger.section("Next Steps");
108086
108134
  const { createTargetRegistry: createTargetRegistry2 } = await Promise.resolve().then(() => (init_deployment(), deployment_exports));
@@ -109257,7 +109305,7 @@ function displayInstalledPackage(pkg) {
109257
109305
 
109258
109306
  // src/cli/index.ts
109259
109307
  init_error_utils();
109260
- var version2 = true ? "0.17.3" : "0.0.0-dev";
109308
+ var version2 = true ? "0.17.5" : "0.0.0-dev";
109261
109309
  var program2 = new Command();
109262
109310
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
109263
109311
  logger.banner(version2);
@@ -67,6 +67,8 @@ export interface ExportArtifacts {
67
67
  workflowName: string;
68
68
  /** Entry point file */
69
69
  entryPoint: string;
70
+ /** Warnings about unsupported or dropped annotations */
71
+ warnings?: string[];
70
72
  }
71
73
  /**
72
74
  * Deployment instructions
@@ -55,6 +55,8 @@ export interface ExportResult {
55
55
  workflows?: string[];
56
56
  /** OpenAPI spec (if generated) */
57
57
  openApiSpec?: object;
58
+ /** Warnings about unsupported or dropped annotations */
59
+ warnings?: string[];
58
60
  }
59
61
  /**
60
62
  * Export a workflow for deployment.
@@ -112,6 +112,7 @@ async function exportSingleWorkflowViaRegistry(target, inputPath, outputDir, isD
112
112
  files,
113
113
  workflow: workflow.name,
114
114
  description: workflow.description,
115
+ warnings: artifacts.warnings,
115
116
  };
116
117
  }
117
118
  /**
@@ -177,6 +178,7 @@ async function exportMultiWorkflowViaRegistry(target, inputPath, outputDir, isDr
177
178
  files,
178
179
  workflow: serviceName,
179
180
  workflows: selectedWorkflows.map((w) => w.name),
181
+ warnings: artifacts.warnings,
180
182
  };
181
183
  }
182
184
  /**
@@ -79,7 +79,7 @@ export const NODE_ACTION_MAP = {
79
79
  },
80
80
  'slack-notify': {
81
81
  githubAction: 'slackapi/slack-github-action@v1',
82
- gitlabScript: ['curl -X POST -H "Content-type: application/json" --data "{\\\"text\\\":\\\"Pipeline complete\\\"}" $SLACK_WEBHOOK_URL'],
82
+ gitlabScript: ["curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Pipeline complete\"}' $SLACK_WEBHOOK_URL"],
83
83
  label: 'Send Slack notification',
84
84
  },
85
85
  'health-check': {
@@ -224,11 +224,11 @@ export class BaseCICDTarget extends BaseExportTarget {
224
224
  const stages = ast.options?.cicd?.stages;
225
225
  if (stages && stages.length > 0) {
226
226
  const depthMap = this.computeJobDepths(jobs);
227
- for (const jc of jobConfigs || []) {
228
- const job = jobs.find(j => j.id === jc.id);
229
- if (job && !job.stage) {
227
+ // Match ALL jobs by name prefix to stages, not just configured ones
228
+ for (const job of jobs) {
229
+ if (!job.stage) {
230
230
  for (const s of stages) {
231
- if (jc.id === s.name || jc.id.startsWith(s.name + '-') || jc.id.startsWith(s.name + '_')) {
231
+ if (job.id === s.name || job.id.startsWith(s.name + '-') || job.id.startsWith(s.name + '_')) {
232
232
  job.stage = s.name;
233
233
  break;
234
234
  }
@@ -326,6 +326,22 @@ function parseJob(text, d, warnings) {
326
326
  jc.rules = jc.rules || [];
327
327
  jc.rules.push({ if: value });
328
328
  break;
329
+ case 'when': {
330
+ // Modifier: sets `when` on the last rule, or creates a standalone rule
331
+ jc.rules = jc.rules || [];
332
+ if (jc.rules.length === 0)
333
+ jc.rules.push({});
334
+ jc.rules[jc.rules.length - 1].when = value;
335
+ break;
336
+ }
337
+ case 'changes': {
338
+ // Modifier: sets `changes` on the last rule
339
+ jc.rules = jc.rules || [];
340
+ if (jc.rules.length === 0)
341
+ jc.rules.push({});
342
+ jc.rules[jc.rules.length - 1].changes = value.split(',').map(s => s.trim()).filter(Boolean);
343
+ break;
344
+ }
329
345
  case 'reports': {
330
346
  jc.reports = jc.reports || [];
331
347
  for (const pair of value.split(',')) {
@@ -35,6 +35,12 @@ export const cicdMultiEnvTemplate = {
35
35
  const envAnnotations = envs
36
36
  .map((env) => ` * @environment ${env} url="https://${env}.example.com"`)
37
37
  .join('\n');
38
+ const stageAnnotations = ` * @stage test\n * @stage build\n * @stage deploy\n`;
39
+ const jobAnnotations = [
40
+ ` * @job test retry=1`,
41
+ ` * @job build timeout="10m"`,
42
+ ...envs.map((env) => ` * @job deploy-${env} allow_failure=${env === 'staging' ? 'true' : 'false'}`),
43
+ ].join('\n');
38
44
  let x = 270;
39
45
  const nodeAnnotations = [];
40
46
  const pathParts = ['Start'];
@@ -89,6 +95,8 @@ ${envAnnotations}
89
95
  * @cache npm key="package-lock.json"
90
96
  * @artifact dist path="dist/" retention=3
91
97
  *
98
+ ${stageAnnotations}${jobAnnotations}
99
+ *
92
100
  ${nodeAnnotations.join('\n')}
93
101
  *
94
102
  * @path ${pathParts.join(' -> ')}
@@ -78,6 +78,12 @@ function deploySsh(sshKey: string = ''): { result: string } { return { result: '
78
78
  function deployS3(accessKey: string = '', secretKey: string = ''): { result: string } { return { result: 'deployed' }; }
79
79
  `
80
80
  : '';
81
+ const stageAnnotations = hasDeploy
82
+ ? ` * @stage test\n * @stage build\n * @stage deploy\n`
83
+ : ` * @stage test\n * @stage build\n`;
84
+ const jobAnnotations = hasDeploy
85
+ ? ` * @job test retry=1\n * @job build timeout="10m"\n * @job deploy allow_failure=false\n`
86
+ : ` * @job test retry=1\n * @job build timeout="10m"\n`;
81
87
  return `/** @flowWeaver nodeType
82
88
  * @expression
83
89
  * @label Checkout code
@@ -116,6 +122,7 @@ ${deployStub}
116
122
  * @secret NPM_TOKEN - npm auth token${deploySecret}
117
123
  * @cache npm key="package-lock.json"
118
124
  *
125
+ ${stageAnnotations}${jobAnnotations} *
119
126
  * @node co checkout [job: "test"] [position: 270 0]
120
127
  * @node setup setupNode [job: "test"] [position: 540 0]
121
128
  * @node install npmInstall [job: "test"] [position: 810 0]
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.17.3";
1
+ export declare const VERSION = "0.17.5";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.17.3';
2
+ export const VERSION = '0.17.5';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -1055,6 +1055,12 @@ export class JSDocParser {
1055
1055
  // Try core FW trigger parsing first (event= and/or cron=)
1056
1056
  const result = parseTriggerLine(`@trigger ${comment}`, warnings);
1057
1057
  if (result) {
1058
+ // Warn if the event name matches a CI/CD trigger keyword (likely user error)
1059
+ const cicdKeywords = ['push', 'pull_request', 'dispatch', 'tag', 'schedule'];
1060
+ if (result.event && cicdKeywords.includes(result.event)) {
1061
+ warnings.push(`@trigger event="${result.event}" is treated as an Inngest event trigger, not a CI/CD trigger. ` +
1062
+ `For CI/CD, use: @trigger ${result.event}`);
1063
+ }
1058
1064
  // Merge: multiple @trigger tags accumulate (event + cron can be separate tags)
1059
1065
  config.trigger = config.trigger || {};
1060
1066
  if (result.event)
package/dist/parser.js CHANGED
@@ -187,7 +187,9 @@ export class AnnotationParser {
187
187
  nodeTypes.push(...inferredNodeTypes);
188
188
  const workflows = this.extractWorkflows(sourceFile, nodeTypes, filePath, errors, warnings);
189
189
  const patterns = this.extractPatterns(sourceFile, nodeTypes, filePath, errors, warnings);
190
- const result = { workflows, nodeTypes, patterns, errors, warnings };
190
+ // Deduplicate warnings (extractWorkflowSignatures + extractWorkflows both parse JSDoc)
191
+ const dedupedWarnings = [...new Set(warnings)];
192
+ const result = { workflows, nodeTypes, patterns, errors, warnings: dedupedWarnings };
191
193
  // Clean up source file to prevent ts-morph Project bloat
192
194
  // (results are captured in the returned AST, source file is no longer needed)
193
195
  this.project.removeSourceFile(sourceFile);
@@ -238,12 +240,14 @@ export class AnnotationParser {
238
240
  // Clean up virtual source file to prevent memory bloat
239
241
  // (tests create many unique virtual paths that accumulate)
240
242
  this.project.removeSourceFile(sourceFile);
243
+ // Deduplicate warnings (extractWorkflowSignatures + extractWorkflows both parse JSDoc)
244
+ const dedupedWarnings = [...new Set(warnings)];
241
245
  return {
242
246
  workflows,
243
247
  nodeTypes,
244
248
  patterns,
245
249
  errors,
246
- warnings,
250
+ warnings: dedupedWarnings,
247
251
  };
248
252
  }
249
253
  clearCache() {
@@ -572,7 +572,9 @@ Configures per-job settings. The name must match a `[job: "name"]` attribute use
572
572
  jobTag ::= "@job" IDENTIFIER { IDENTIFIER "=" ( STRING | IDENTIFIER | INTEGER ) }
573
573
  ```
574
574
 
575
- Recognized keys: `retry` (number), `allow_failure` (boolean), `timeout` (string), `runner` (string), `tags` (comma-list), `coverage` (string), `reports` (comma-list of type=path), `rules` (string), `extends` (string), `before_script` (comma-list), `variables` (comma-list of KEY=VALUE).
575
+ Recognized keys: `retry` (number), `allow_failure` (boolean), `timeout` (string), `runner` (string), `tags` (comma-list), `coverage` (string), `reports` (comma-list of type=path), `rules` (string), `when` (rule modifier), `changes` (rule modifier, comma-list), `extends` (string), `before_script` (comma-list), `variables` (comma-list of KEY=VALUE).
576
+
577
+ The `when` and `changes` keys are rule modifiers: they apply to the most recently declared `rules` entry for that job. If no `rules` entry exists yet, one is created automatically.
576
578
 
577
579
  **Examples:**
578
580
 
@@ -580,6 +582,8 @@ Recognized keys: `retry` (number), `allow_failure` (boolean), `timeout` (string)
580
582
  @job build retry=2 timeout="10m"
581
583
  @job test-unit coverage='/Coverage: (\d+)%/' reports="junit=test-results.xml"
582
584
  @job deploy allow_failure=true rules="$CI_COMMIT_BRANCH == main"
585
+ @job deploy rules="$CI_COMMIT_BRANCH == main" when=manual
586
+ @job deploy rules="$CI_COMMIT_TAG" changes="src/**,lib/**"
583
587
  @job lint tags="docker,linux" extends=".base-lint"
584
588
  ```
585
589
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.17.3",
3
+ "version": "0.17.5",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",