pipecraft 0.0.0-releaseit → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +126 -108
  2. package/dist/cli/index.d.ts +69 -0
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +94 -22
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/generators/init.tpl.d.ts +70 -0
  7. package/dist/generators/init.tpl.d.ts.map +1 -1
  8. package/dist/generators/init.tpl.js +100 -8
  9. package/dist/generators/init.tpl.js.map +1 -1
  10. package/dist/generators/workflows.tpl.d.ts +86 -0
  11. package/dist/generators/workflows.tpl.d.ts.map +1 -1
  12. package/dist/generators/workflows.tpl.js +101 -9
  13. package/dist/generators/workflows.tpl.js.map +1 -1
  14. package/dist/templates/actions/calculate-version.yml.tpl.d.ts +47 -0
  15. package/dist/templates/actions/calculate-version.yml.tpl.d.ts.map +1 -1
  16. package/dist/templates/actions/calculate-version.yml.tpl.js +72 -3
  17. package/dist/templates/actions/calculate-version.yml.tpl.js.map +1 -1
  18. package/dist/templates/actions/create-pr.yml.tpl.d.ts +14 -0
  19. package/dist/templates/actions/create-pr.yml.tpl.d.ts.map +1 -1
  20. package/dist/templates/actions/create-pr.yml.tpl.js +22 -2
  21. package/dist/templates/actions/create-pr.yml.tpl.js.map +1 -1
  22. package/dist/templates/actions/create-release.yml.tpl.d.ts +18 -0
  23. package/dist/templates/actions/create-release.yml.tpl.d.ts.map +1 -0
  24. package/dist/templates/actions/create-release.yml.tpl.js +141 -0
  25. package/dist/templates/actions/create-release.yml.tpl.js.map +1 -0
  26. package/dist/templates/actions/create-tag.yml.tpl.d.ts +14 -0
  27. package/dist/templates/actions/create-tag.yml.tpl.d.ts.map +1 -1
  28. package/dist/templates/actions/create-tag.yml.tpl.js +22 -2
  29. package/dist/templates/actions/create-tag.yml.tpl.js.map +1 -1
  30. package/dist/templates/actions/detect-changes.yml.tpl.d.ts +70 -0
  31. package/dist/templates/actions/detect-changes.yml.tpl.d.ts.map +1 -1
  32. package/dist/templates/actions/detect-changes.yml.tpl.js +98 -2
  33. package/dist/templates/actions/detect-changes.yml.tpl.js.map +1 -1
  34. package/dist/templates/actions/manage-branch.yml.tpl.d.ts +14 -0
  35. package/dist/templates/actions/manage-branch.yml.tpl.d.ts.map +1 -1
  36. package/dist/templates/actions/manage-branch.yml.tpl.js +22 -2
  37. package/dist/templates/actions/manage-branch.yml.tpl.js.map +1 -1
  38. package/dist/templates/actions/promote-branch.yml.tpl.d.ts +14 -0
  39. package/dist/templates/actions/promote-branch.yml.tpl.d.ts.map +1 -1
  40. package/dist/templates/actions/promote-branch.yml.tpl.js +50 -7
  41. package/dist/templates/actions/promote-branch.yml.tpl.js.map +1 -1
  42. package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts +114 -1
  43. package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts.map +1 -1
  44. package/dist/templates/workflows/pipeline-path-based.yml.tpl.js +479 -74
  45. package/dist/templates/workflows/pipeline-path-based.yml.tpl.js.map +1 -1
  46. package/dist/types/index.d.ts +231 -8
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/index.js +9 -0
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/ast-path-operations.d.ts.map +1 -1
  51. package/dist/utils/ast-path-operations.js +24 -11
  52. package/dist/utils/ast-path-operations.js.map +1 -1
  53. package/dist/utils/config.d.ts +63 -0
  54. package/dist/utils/config.d.ts.map +1 -1
  55. package/dist/utils/config.js +71 -0
  56. package/dist/utils/config.js.map +1 -1
  57. package/dist/utils/github-setup.d.ts +323 -2
  58. package/dist/utils/github-setup.d.ts.map +1 -1
  59. package/dist/utils/github-setup.js +551 -23
  60. package/dist/utils/github-setup.js.map +1 -1
  61. package/dist/utils/idempotency.d.ts +147 -7
  62. package/dist/utils/idempotency.d.ts.map +1 -1
  63. package/dist/utils/idempotency.js +143 -15
  64. package/dist/utils/idempotency.js.map +1 -1
  65. package/dist/utils/logger.d.ts +134 -0
  66. package/dist/utils/logger.d.ts.map +1 -0
  67. package/dist/utils/logger.js +153 -0
  68. package/dist/utils/logger.js.map +1 -0
  69. package/dist/utils/preflight.d.ts +223 -12
  70. package/dist/utils/preflight.d.ts.map +1 -1
  71. package/dist/utils/preflight.js +216 -16
  72. package/dist/utils/preflight.js.map +1 -1
  73. package/dist/utils/versioning.d.ts +160 -8
  74. package/dist/utils/versioning.d.ts.map +1 -1
  75. package/dist/utils/versioning.js +179 -15
  76. package/dist/utils/versioning.js.map +1 -1
  77. package/package.json +26 -5
  78. package/.claude/settings.local.json +0 -35
  79. package/.github/actions/calculate-version/action.yml +0 -106
  80. package/.github/actions/create-pr/action.yml +0 -122
  81. package/.github/actions/create-release/action.yml +0 -74
  82. package/.github/actions/create-tag/action.yml +0 -94
  83. package/.github/actions/detect-changes/action.yml +0 -62
  84. package/.github/actions/manage-branch/action.yml +0 -113
  85. package/.github/actions/promote-branch/action.yml +0 -352
  86. package/.github/example/workflows/job.analyze.code.yml +0 -26
  87. package/.github/example/workflows/job.analyze.docker.yml +0 -32
  88. package/.github/example/workflows/job.app.api.deploy.yml +0 -127
  89. package/.github/example/workflows/job.app.api.test.yml +0 -102
  90. package/.github/example/workflows/job.app.docs.deploy.yml +0 -40
  91. package/.github/example/workflows/job.app.docs.test.yml +0 -50
  92. package/.github/example/workflows/job.app.web.deploy.yml +0 -96
  93. package/.github/example/workflows/job.app.web.test.yml +0 -49
  94. package/.github/example/workflows/job.changes.yml +0 -82
  95. package/.github/example/workflows/job.create-pr.yml +0 -96
  96. package/.github/example/workflows/job.env-check.yml +0 -43
  97. package/.github/example/workflows/job.fast-forward.yml +0 -103
  98. package/.github/example/workflows/job.lint.yml +0 -64
  99. package/.github/example/workflows/job.pr-name.yml +0 -70
  100. package/.github/example/workflows/job.pr-squash-name.yml +0 -27
  101. package/.github/example/workflows/job.tag.yml +0 -38
  102. package/.github/example/workflows/job.version.yml +0 -99
  103. package/.github/example/workflows/lib.plugin-base.publish.yml +0 -59
  104. package/.github/example/workflows/pipe.yml +0 -178
  105. package/.github/example/workflows/scripts/fastforward.sh +0 -51
  106. package/.github/example/workflows/scripts/pipe-cleaner.sh +0 -70
  107. package/.github/scripts/fastforward.sh +0 -51
  108. package/.github/scripts/pipe-cleaner.sh +0 -70
  109. package/.github/workflows/pipeline.yml +0 -220
  110. package/.github/workflows/pr-title-check.yml +0 -70
  111. package/.github/workflows/publish.yml +0 -39
  112. package/.pipecraftrc.json +0 -78
  113. package/.release-it.cjs +0 -71
  114. package/PIPELINE_TESTING_PLAN.md +0 -499
  115. package/TRUNK_FLOW_PLAN.md +0 -401
  116. package/assets/logo_banner.png +0 -0
  117. package/assets/logo_banner.webp +0 -0
  118. package/docs/USER_JOURNEY_ERRORS.md +0 -352
  119. package/eslint.config.js +0 -77
  120. package/examples/basic-config.json +0 -41
  121. package/examples/monorepo-config.json +0 -49
  122. package/examples/usage.md +0 -289
  123. package/scripts/debug-workflows.sh +0 -339
  124. package/scripts/parse-pipeline.js +0 -29
  125. package/scripts/test-job-order.cjs +0 -140
  126. package/scripts/validate-pipeline.cjs +0 -379
  127. package/scripts/verify-job-order.sh +0 -30
  128. package/src/cli/index.ts +0 -414
  129. package/src/generators/init.tpl.ts +0 -126
  130. package/src/generators/workflows.tpl.ts +0 -80
  131. package/src/templates/actions/calculate-version.yml.tpl.ts +0 -124
  132. package/src/templates/actions/create-pr.yml.tpl.ts +0 -147
  133. package/src/templates/actions/create-tag.yml.tpl.ts +0 -112
  134. package/src/templates/actions/detect-changes.yml.tpl.ts +0 -87
  135. package/src/templates/actions/manage-branch.yml.tpl.ts +0 -132
  136. package/src/templates/actions/promote-branch.yml.tpl.ts +0 -364
  137. package/src/templates/workflows/pipeline-path-based.yml.tpl.ts +0 -706
  138. package/src/types/index.ts +0 -64
  139. package/src/utils/README-ast-path-operations.md +0 -390
  140. package/src/utils/ast-path-operations.ts +0 -581
  141. package/src/utils/config.ts +0 -64
  142. package/src/utils/github-setup.ts +0 -558
  143. package/src/utils/idempotency.ts +0 -215
  144. package/src/utils/preflight.ts +0 -306
  145. package/src/utils/versioning.ts +0 -244
  146. package/tests/README.md +0 -229
  147. package/tests/TEST_STRUCTURE.md +0 -256
  148. package/tests/act/run-act-tests.sh +0 -345
  149. package/tests/debugging/debug-utils.ts +0 -538
  150. package/tests/debugging/debug-workflow.test.ts +0 -339
  151. package/tests/debugging/debug-workflows.sh +0 -339
  152. package/tests/debugging/iterative-debug.ts +0 -652
  153. package/tests/debugging/run-debug-tests.sh +0 -431
  154. package/tests/fixtures/basic-config.json +0 -51
  155. package/tests/fixtures/invalid-config.json +0 -9
  156. package/tests/fixtures/pipeline-generated.yml +0 -235
  157. package/tests/fixtures/pipeline-preserve-comments.yml +0 -245
  158. package/tests/fixtures/pipeline-user-modified.yml +0 -245
  159. package/tests/fixtures/test-config.json +0 -58
  160. package/tests/github-live/README.md +0 -250
  161. package/tests/github-local/JOB_WORKFLOW_TESTS.md +0 -296
  162. package/tests/github-local/README.md +0 -240
  163. package/tests/github-local/run-all-tests.sh +0 -422
  164. package/tests/github-local/test-job-workflows.sh +0 -631
  165. package/tests/github-local/test-pipeline-workflow.sh +0 -440
  166. package/tests/integration/generators.test.ts +0 -578
  167. package/tests/integration/path-based-template.test.ts +0 -510
  168. package/tests/integration/simple-path-based.test.ts +0 -415
  169. package/tests/setup.ts +0 -56
  170. package/tests/unit/ast-path-operations-extended.test.ts +0 -302
  171. package/tests/unit/cli.test.ts +0 -541
  172. package/tests/unit/config-extended.test.ts +0 -412
  173. package/tests/unit/config.test.ts +0 -152
  174. package/tests/unit/github-setup.test.ts +0 -189
  175. package/tests/unit/idempotency-isolated.test.ts +0 -297
  176. package/tests/unit/job-order.test.ts +0 -157
  177. package/tests/unit/pipeline-path-based.test.ts +0 -511
  178. package/tests/unit/validate-pipeline.test.ts +0 -384
  179. package/tests/unit/versioning-extended.test.ts +0 -279
  180. package/tests/unit/versioning.test.ts +0 -241
  181. package/tsconfig.debug.json +0 -17
  182. package/tsconfig.json +0 -28
  183. package/vitest.config.ts +0 -51
@@ -1,16 +1,73 @@
1
+ /**
2
+ * Path-Based Pipeline Template Generator
3
+ *
4
+ * The core template that generates the main CI/CD pipeline workflow for PipeCraft.
5
+ * This is the most complex template in the system, responsible for creating a
6
+ * GitHub Actions workflow that orchestrates the entire trunk-based development flow.
7
+ *
8
+ * ## Key Responsibilities
9
+ *
10
+ * 1. **Change Detection**: Generates jobs to detect which domains (api, web, libs, etc.) changed
11
+ * 2. **Test Execution**: Creates domain-specific test jobs based on changes
12
+ * 3. **Version Management**: Integrates semantic versioning for staging/production branches
13
+ * 4. **Branch Promotion**: Auto-promotes code through branch flow (develop → staging → main)
14
+ * 5. **User Job Preservation**: Maintains user-added custom jobs during regeneration
15
+ * 6. **Comment Preservation**: Retains user comments when updating workflows
16
+ *
17
+ * ## Intelligent Merging
18
+ *
19
+ * The generator distinguishes between:
20
+ * - **Pipecraft-owned jobs**: `changes`, `version`, `tag`, `promote`, `release`, `test-*`, `deploy-*`
21
+ * - **User jobs**: Any jobs not owned by Pipecraft
22
+ *
23
+ * During regeneration:
24
+ * - Pipecraft jobs are completely replaced with template versions
25
+ * - User jobs are preserved exactly as-is
26
+ * - User comments are maintained
27
+ * - Job order is intelligently managed (Pipecraft jobs first, then user jobs)
28
+ *
29
+ * ## Architecture
30
+ *
31
+ * Uses AST-based path operations for surgical YAML manipulation:
32
+ * - Parse existing workflow into AST
33
+ * - Apply precise path-based operations
34
+ * - Preserve formatting and comments
35
+ * - Rebuild YAML maintaining structure
36
+ *
37
+ * @module templates/workflows/pipeline-path-based.yml.tpl
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * import { generate } from './templates/workflows/pipeline-path-based.yml.tpl.js'
42
+ *
43
+ * // Initial generation
44
+ * await generate({
45
+ * cwd: '/path/to/project',
46
+ * branchFlow: ['develop', 'staging', 'main'],
47
+ * domains: {
48
+ * api: { paths: ['src/api/**'], test: true },
49
+ * web: { paths: ['src/web/**'], test: true }
50
+ * }
51
+ * })
52
+ *
53
+ * // Incremental update (preserves user jobs)
54
+ * await generate({
55
+ * cwd: '/path/to/project',
56
+ * existingPipeline: parsedYAML,
57
+ * existingPipelineContent: rawYAMLString,
58
+ * branchFlow: ['develop', 'staging', 'main'],
59
+ * domains: { ... }
60
+ * })
61
+ * ```
62
+ *
63
+ * @see {@link module:utils/ast-path-operations} for YAML manipulation details
64
+ */
1
65
  import { toFile, renderTemplate } from '@featherscloud/pinion';
2
66
  import { parseDocument, stringify, Scalar } from 'yaml';
3
67
  import fs from 'fs';
4
68
  import { applyPathOperations, createValueFromString } from '../../utils/ast-path-operations.js';
5
69
  import dedent from 'dedent';
6
- /**
7
- * Path-based pipeline generator
8
- *
9
- * Uses precise AST path operations to:
10
- * - Ensure required paths exist
11
- * - Set/merge/overwrite specific values
12
- * - Preserve user customizations while ensuring template requirements
13
- */
70
+ import { logger } from '../../utils/logger.js';
14
71
  /**
15
72
  * Get minimal base template - just enough structure to be parsed
16
73
  * All actual content is defined via operations list
@@ -30,7 +87,7 @@ const getPipecraftOwnedJobs = (branchFlow, domains = {}) => {
30
87
  'changes',
31
88
  'version',
32
89
  'tag',
33
- 'promote', // Single promote job instead of multiple promote-to-{target} jobs
90
+ 'promote', // Promotion job - triggers workflow on next branch
34
91
  'release' // GitHub release creation on final branch
35
92
  ]);
36
93
  // Add domain-based jobs (test-*, deploy-*, remote-test-*) based on flags
@@ -56,51 +113,95 @@ const isPipecraftJob = (jobName, branchFlow) => {
56
113
  */
57
114
  export const createPathBasedPipeline = (ctx) => {
58
115
  const branchFlow = ctx.branchFlow || ['develop', 'staging', 'main'];
59
- console.log('🔍 Branch flow from context:', branchFlow);
60
- console.log('🔍 Context keys:', Object.keys(ctx));
116
+ logger.debug('🔍 Branch flow from context:', branchFlow);
117
+ logger.debug('🔍 Context keys:', Object.keys(ctx));
61
118
  // Use existing pipeline from context or start with base template
62
119
  let doc;
63
120
  let hasExistingPipeline = false;
64
121
  if (ctx.existingPipelineContent) {
65
- // Parse the original YAML content WITHOUT source tokens
66
- // This prevents old comments from being preserved when we rebuild
122
+ // Parse the original YAML content to preserve structure and comments
67
123
  doc = parseDocument(ctx.existingPipelineContent);
124
+ // Note: We save document-level comments later and selectively restore them
125
+ // Don't clear doc.commentBefore here - we'll handle it after saving
68
126
  hasExistingPipeline = true;
69
- console.log('🔄 Merging with existing pipeline from existingPipelineContent');
127
+ logger.verbose('🔄 Merging with existing pipeline');
70
128
  }
71
129
  else if (ctx.existingPipeline) {
72
130
  // Convert existing pipeline object to YAML string first
73
131
  const existingYaml = stringify(ctx.existingPipeline);
74
132
  doc = parseDocument(existingYaml);
75
133
  hasExistingPipeline = true;
76
- console.log('🔄 Merging with existing pipeline from existingPipeline object');
134
+ logger.verbose('🔄 Merging with existing pipeline');
77
135
  }
78
136
  else {
79
137
  doc = parseDocument(getBaseTemplate(ctx));
80
- console.log('📝 Creating new pipeline');
138
+ logger.verbose('📝 Creating new pipeline');
81
139
  }
82
140
  if (!doc.contents) {
83
141
  throw new Error('Failed to parse pipeline document');
84
142
  }
85
143
  // Apply path-based operations
86
144
  const operations = [
145
+ // =============================================================================
146
+ // WORKFLOW HEADER COMMENT
147
+ // Note: This comment should be preserved if user has custom comments at
148
+ // document level, only job-level managed headers should replace user comments
149
+ // =============================================================================
150
+ {
151
+ path: 'name',
152
+ operation: 'preserve',
153
+ value: (() => {
154
+ const nameScalar = new Scalar('Pipeline');
155
+ nameScalar.type = Scalar.QUOTE_DOUBLE;
156
+ return nameScalar;
157
+ })(),
158
+ required: true
159
+ // Note: No commentBefore here - we'll add it only if no user comment exists
160
+ },
87
161
  // =============================================================================
88
162
  // WORKFLOW METADATA - Name and run identification
89
163
  // =============================================================================
90
164
  {
91
165
  path: 'run-name',
92
- operation: 'set',
93
- value: `\${{ github.ref_name }} #\${{ inputs.run_number || github.run_number }}\${{ inputs.version && format(' - {0}', inputs.version) || '' }}`,
94
- required: true
166
+ operation: 'preserve',
167
+ value: (() => {
168
+ const branchList = branchFlow.join(',');
169
+ const runNameScalar = new Scalar(`\${{ github.event_name == 'pull_request' && !contains('${branchList}', github.head_ref) && github.event.pull_request.title || github.ref_name }} #\${{ inputs.run_number || github.run_number }}\${{ inputs.version && format(' - {0}', inputs.version) || '' }}`);
170
+ runNameScalar.type = Scalar.QUOTE_DOUBLE;
171
+ return runNameScalar;
172
+ })(),
173
+ required: true,
174
+ spaceBefore: true
95
175
  },
96
176
  // =============================================================================
97
177
  // WORKFLOW TRIGGERS - Define when the pipeline runs
98
178
  // =============================================================================
99
- // The pipeline should only run on:
100
- // 1. Push to develop/staging/main (from PR merge or promotion)
101
- // 2. Manual trigger via workflow_dispatch
179
+ // The pipeline runs on:
180
+ // 1. pull_request (opened/synchronize/reopened) targeting initial branch only
181
+ // - Excludes 'closed' type to avoid duplicate runs when PR is merged
182
+ // - Only targets initial branch (e.g., develop) to avoid duplicates
183
+ // - Automated PRs (develop→staging, staging→main) don't trigger (wrong target)
184
+ // - Only runs changes detection + tests (no versioning/tagging/promotion)
185
+ // 2. push to branch flow branches - Runs full pipeline after PR merge
186
+ // - Includes versioning, tagging, PR creation to next branch, and promotion
187
+ // 3. workflow_dispatch - Manual trigger with full pipeline
188
+ // 4. workflow_call - Can be called from other workflows
102
189
  //
103
- // NOT on pull_request events - we only want to run after the PR is merged
190
+ // Flow example:
191
+ // feature/xyz → PR to develop → Tests run (targets develop ✓)
192
+ // PR merged → Push to develop → Full pipeline (version + tag + createpr)
193
+ // Pipecraft creates PR: develop → staging (targets staging, skipped ✓)
194
+ // Auto-merge → Push to staging → Full pipeline continues
195
+ // Pipecraft creates PR: staging → main (targets main, skipped ✓)
196
+ // Auto-merge → Push to main → Full pipeline completes
197
+ // Ensure 'on' key exists with proper spacing (nested operations below will populate it)
198
+ {
199
+ path: 'on',
200
+ operation: 'set',
201
+ value: {},
202
+ required: true,
203
+ spaceBefore: true
204
+ },
104
205
  {
105
206
  path: 'on.workflow_dispatch.inputs.version',
106
207
  operation: 'set',
@@ -131,18 +232,88 @@ export const createPathBasedPipeline = (ctx) => {
131
232
  },
132
233
  required: true
133
234
  },
235
+ {
236
+ path: 'on.workflow_dispatch.inputs.commitSha',
237
+ operation: 'set',
238
+ value: {
239
+ description: 'The exact commit SHA to checkout and test',
240
+ required: false,
241
+ type: 'string'
242
+ },
243
+ required: true
244
+ },
245
+ {
246
+ path: 'on.workflow_call.inputs.version',
247
+ operation: 'set',
248
+ value: {
249
+ description: 'The version to deploy',
250
+ required: false,
251
+ type: 'string'
252
+ },
253
+ required: true
254
+ },
255
+ {
256
+ path: 'on.workflow_call.inputs.baseRef',
257
+ operation: 'set',
258
+ value: {
259
+ description: 'The base reference for comparison',
260
+ required: false,
261
+ type: 'string'
262
+ },
263
+ required: true
264
+ },
265
+ {
266
+ path: 'on.workflow_call.inputs.run_number',
267
+ operation: 'set',
268
+ value: {
269
+ description: 'The original run number from develop branch',
270
+ required: false,
271
+ type: 'string'
272
+ },
273
+ required: true
274
+ },
275
+ {
276
+ path: 'on.workflow_call.inputs.commitSha',
277
+ operation: 'set',
278
+ value: {
279
+ description: 'The exact commit SHA to checkout and test',
280
+ required: false,
281
+ type: 'string'
282
+ },
283
+ required: true
284
+ },
134
285
  {
135
286
  path: 'on.push.branches',
136
287
  operation: 'set',
137
288
  value: branchFlow,
138
289
  required: true
139
290
  },
291
+ {
292
+ path: 'on.pull_request.types',
293
+ operation: 'set',
294
+ value: ['opened', 'synchronize', 'reopened'],
295
+ required: true
296
+ },
297
+ {
298
+ path: 'on.pull_request.branches',
299
+ operation: 'set',
300
+ value: [ctx.initialBranch || branchFlow[0]],
301
+ required: true
302
+ },
140
303
  // =============================================================================
141
304
  // CORE PIPECRAFT JOBS - Template-managed jobs that get updates
142
305
  // =============================================================================
143
306
  // These are the core Pipecraft jobs that should always use the latest template
144
307
  // version. Using 'overwrite' operation ensures users get bug fixes and improvements.
145
308
  // These jobs are essential for Pipecraft functionality and should not be customized.
309
+ // Ensure 'jobs' key exists with proper spacing (nested operations below will populate it)
310
+ {
311
+ path: 'jobs',
312
+ operation: 'set',
313
+ value: {},
314
+ required: true,
315
+ spaceBefore: true
316
+ },
146
317
  {
147
318
  path: 'jobs.changes',
148
319
  operation: 'overwrite',
@@ -150,6 +321,8 @@ export const createPathBasedPipeline = (ctx) => {
150
321
  runs-on: ubuntu-latest
151
322
  steps:
152
323
  - uses: actions/checkout@v4
324
+ with:
325
+ ref: \${{ inputs.commitSha || github.sha }}
153
326
  - uses: ./.github/actions/detect-changes
154
327
  id: detect
155
328
  with:
@@ -159,8 +332,9 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
159
332
  `, ctx),
160
333
  commentBefore: dedent `
161
334
  =============================================================================
162
- CHANGES DETECTION
335
+ CHANGES DETECTION (⚠️ Managed by Pipecraft - do not modify)
163
336
  =============================================================================
337
+ This job detects which domains have changed and sets outputs for downstream jobs.
164
338
  `,
165
339
  required: true
166
340
  },
@@ -190,8 +364,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
190
364
 
191
365
 
192
366
  =============================================================================
193
- TESTING JOBS
367
+ TESTING JOBS (✅ Customize these with your test logic)
194
368
  =============================================================================
369
+ These jobs run tests for each domain when changes are detected.
370
+ Replace the TODO comments with your actual test commands.
195
371
  ` : undefined,
196
372
  required: true
197
373
  })),
@@ -200,19 +376,24 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
200
376
  operation: 'overwrite',
201
377
  commentBefore: dedent `
202
378
  =============================================================================
203
- VERSIONING
379
+ VERSIONING (⚠️ Managed by Pipecraft - do not modify)
204
380
  =============================================================================
381
+ Calculates the next version based on conventional commits and semver rules.
382
+ Only runs on push events (skipped on pull requests).
205
383
  `,
206
384
  value: createValueFromString(`
207
- if: \${{ always() && (${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result == 'success'`).join(' || ')}) && ${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result != 'failure'`).join(' && ')} }}
385
+ if: \${{ always() && github.event_name != 'pull_request' && (${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result == 'success'`).join(' || ')}) && ${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result != 'failure'`).join(' && ')} }}
208
386
  needs: [ changes, ${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `test-${domain}`).join(', ')} ]
209
387
  runs-on: ubuntu-latest
210
388
  steps:
211
389
  - uses: actions/checkout@v4
390
+ with:
391
+ ref: \${{ inputs.commitSha || github.sha }}
212
392
  - uses: ./.github/actions/calculate-version
213
393
  id: version
214
394
  with:
215
395
  baseRef: \${{ inputs.baseRef || '${ctx.finalBranch || "main"}' }}
396
+ commitSha: \${{ inputs.commitSha || github.sha }}
216
397
  outputs:
217
398
  version: \${{ steps.version.outputs.version }}
218
399
  `, ctx),
@@ -225,8 +406,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
225
406
  operation: 'overwrite',
226
407
  commentBefore: index === 0 ? dedent `
227
408
  =============================================================================
228
- DEPLOYMENT JOBS
409
+ DEPLOYMENT JOBS (✅ Customize these with your deploy logic)
229
410
  =============================================================================
411
+ These jobs deploy each domain when changes are detected and tests pass.
412
+ Replace the TODO comments with your actual deployment commands.
230
413
  ` : undefined,
231
414
  spaceBefore: index === 0 ? true : undefined,
232
415
  value: createValueFromString(`
@@ -248,8 +431,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
248
431
  operation: 'overwrite',
249
432
  commentBefore: index === 0 ? dedent `
250
433
  =============================================================================
251
- REMOTE TESTING JOBS
434
+ REMOTE TESTING JOBS (✅ Customize these with your remote test logic)
252
435
  =============================================================================
436
+ These jobs test deployed services remotely after deployment succeeds.
437
+ Replace the TODO comments with your actual remote testing commands.
253
438
  ` : undefined,
254
439
  spaceBefore: index === 0 ? true : undefined,
255
440
  value: createValueFromString(`
@@ -289,8 +474,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
289
474
  needs: [ ${needsArray.join(', ')} ]
290
475
  if: \${{
291
476
  always() &&
477
+ github.event_name != 'pull_request' &&
292
478
  github.ref_name == '${ctx.initialBranch || branchFlow[0]}' &&
293
479
  needs.version.result == 'success' &&
480
+ needs.version.outputs.version != '' &&
294
481
  (
295
482
  ${noFailures}
296
483
  ) &&
@@ -301,6 +488,8 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
301
488
  runs-on: ubuntu-latest
302
489
  steps:
303
490
  - uses: actions/checkout@v4
491
+ with:
492
+ ref: \${{ inputs.commitSha || github.sha }}
304
493
  - uses: ./.github/actions/create-tag
305
494
  with:
306
495
  version: \${{ needs.version.outputs.version }}
@@ -309,8 +498,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
309
498
  spaceBefore: true,
310
499
  commentBefore: dedent `
311
500
  =============================================================================
312
- TAG & PROMOTE
501
+ TAG & PROMOTE (⚠️ Managed by Pipecraft - do not modify)
313
502
  =============================================================================
503
+ Creates a git tag with the calculated version on the initial branch.
504
+ Only runs on push events after successful tests and deployments.
314
505
  `,
315
506
  required: true
316
507
  },
@@ -320,12 +511,14 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
320
511
  operation: 'overwrite',
321
512
  value: createValueFromString(`
322
513
  # Only runs on push or manual workflow_dispatch events to branches that can promote
323
- # Waits for version/tag if they run, but doesn't fail if they're skipped
514
+ # Requires version to succeed (which means tests passed)
515
+ # Requires a version to have been calculated (skip promotion for non-versioned commits)
324
516
  # Needs all deploy and/or remote test jobs to succeed
325
517
  if: \${{
326
518
  always() &&
327
519
  (github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
328
- (needs.version.result == 'success' || needs.version.result == 'skipped') &&
520
+ needs.version.result == 'success' &&
521
+ needs.version.outputs.version != '' &&
329
522
  (needs.tag.result == 'success' || needs.tag.result == 'skipped') &&
330
523
  (
331
524
  ${branchFlow.slice(0, -1).map((branch) => `github.ref_name == '${branch}'`).join(' || \n ')}
@@ -335,6 +528,8 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
335
528
  runs-on: ubuntu-latest
336
529
  steps:
337
530
  - uses: actions/checkout@v4
531
+ with:
532
+ ref: \${{ inputs.commitSha || github.sha }}
338
533
  - uses: ./.github/actions/promote-branch
339
534
  with:
340
535
  sourceBranch: \${{ github.ref_name }}
@@ -343,11 +538,11 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
343
538
  token: \${{ secrets.GITHUB_TOKEN }}
344
539
  `, ctx),
345
540
  spaceBefore: true,
346
- commentBefore: dedent `
347
- =============================================================================
348
- PROMOTION JOB
349
- =============================================================================
350
- `,
541
+ commentBefore: `=============================================================================
542
+ PROMOTION JOB (⚠️ Managed by Pipecraft - do not modify)
543
+ =============================================================================
544
+ Triggers the next branch's workflow after successful versioning and tagging.
545
+ Passes version and run_number to maintain traceability across branches.`,
351
546
  required: true
352
547
  },
353
548
  // Generate release job for final branch (main)
@@ -361,23 +556,26 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
361
556
  github.ref_name == '${ctx.finalBranch || branchFlow[branchFlow.length - 1]}' &&
362
557
  (github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
363
558
  needs.version.result == 'success' &&
559
+ needs.version.outputs.version != '' &&
364
560
  (needs.tag.result == 'success' || needs.tag.result == 'skipped')
365
561
  }}
366
562
  needs: [ version, tag ]
367
563
  runs-on: ubuntu-latest
368
564
  steps:
369
565
  - uses: actions/checkout@v4
566
+ with:
567
+ ref: \${{ inputs.commitSha || github.sha }}
370
568
  - uses: ./.github/actions/create-release
371
569
  with:
372
570
  version: \${{ needs.version.outputs.version }}
373
571
  token: \${{ secrets.GITHUB_TOKEN }}
374
572
  `, ctx),
375
573
  spaceBefore: true,
376
- commentBefore: dedent `
377
- =============================================================================
378
- RELEASE JOB (Main Branch Only)
379
- =============================================================================
380
- `,
574
+ commentBefore: `=============================================================================
575
+ RELEASE JOB (⚠️ Managed by Pipecraft - do not modify)
576
+ =============================================================================
577
+ Creates a GitHub release on the final branch with release notes.
578
+ Only runs after successful versioning and tagging on the final branch.`,
381
579
  required: true
382
580
  },
383
581
  ];
@@ -401,44 +599,178 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
401
599
  const jobsNode = doc.contents.get('jobs');
402
600
  if (jobsNode && jobsNode.items) {
403
601
  originalJobOrder = getJobKeysInOrder(jobsNode);
404
- console.log('📋 Original job order:', originalJobOrder);
602
+ logger.debug('📋 Original job order:', originalJobOrder);
405
603
  }
406
604
  }
605
+ // Deprecated jobs that should be removed (old promotion strategy)
606
+ const DEPRECATED_JOBS = new Set(['createpr', 'branch']);
407
607
  // Collect user jobs (non-Pipecraft jobs) to preserve them
608
+ // Save the entire item (key, value, and comments) not just the value
408
609
  const userJobs = new Map();
409
610
  if (doc.contents.get('jobs')) {
410
611
  const jobsNode = doc.contents.get('jobs');
411
612
  if (jobsNode && jobsNode.items) {
412
613
  for (const item of jobsNode.items) {
413
614
  const jobName = item.key?.toString() || item.key?.value;
414
- if (jobName && !PIPECRAFT_OWNED_JOBS.has(jobName)) {
415
- userJobs.set(jobName, item.value);
615
+ // Skip deprecated jobs - don't preserve them
616
+ if (jobName && !PIPECRAFT_OWNED_JOBS.has(jobName) && !DEPRECATED_JOBS.has(jobName)) {
617
+ // Save the entire item to preserve comments on keys
618
+ userJobs.set(jobName, item);
416
619
  }
417
620
  }
418
621
  if (userJobs.size > 0) {
419
- console.log(`📋 Preserving ${userJobs.size} user jobs: ${Array.from(userJobs.keys()).join(', ')}`);
622
+ logger.verbose(`📋 Preserving ${userJobs.size} user jobs: ${Array.from(userJobs.keys()).join(', ')}`);
623
+ }
624
+ }
625
+ }
626
+ // To ensure proper key order (name, run-name, on, jobs), we need to:
627
+ // 1. Extract all values we care about
628
+ // 2. Clear the document
629
+ // 3. Re-add them in the correct order
630
+ // Save existing values and comments that should be preserved
631
+ const existingName = doc.contents.get('name');
632
+ const existingRunName = doc.contents.get('run-name');
633
+ const existingOn = doc.contents.get('on');
634
+ const existingJobs = doc.contents.get('jobs');
635
+ // Save jobs node comments (before we rebuild)
636
+ const jobsNodeCommentBefore = existingJobs ? existingJobs.commentBefore : undefined;
637
+ const jobsNodeComment = existingJobs ? existingJobs.comment : undefined;
638
+ // Save document-level comments (user comments at the top)
639
+ // Note: YAML stores document-level comments on doc.commentBefore, not doc.contents.commentBefore
640
+ const docCommentBefore = doc.commentBefore;
641
+ const docComment = doc.comment;
642
+ // Save user comments on keys (but not Pipecraft-managed comments)
643
+ const savedComments = new Map();
644
+ const isPipecraftComment = (comment) => {
645
+ if (!comment)
646
+ return false;
647
+ const lowerComment = comment.toLowerCase();
648
+ return lowerComment.includes('pipecraft') ||
649
+ lowerComment.includes('managed by pipecraft') ||
650
+ lowerComment.includes('do not modify');
651
+ };
652
+ for (const item of doc.contents.items) {
653
+ const keyName = typeof item.key === 'string' ? item.key : item.key?.value;
654
+ if (keyName) {
655
+ const keyCommentBefore = item.key?.commentBefore;
656
+ const keyComment = item.key?.comment;
657
+ // Only save non-Pipecraft comments
658
+ if ((keyCommentBefore && !isPipecraftComment(keyCommentBefore)) ||
659
+ (keyComment && !isPipecraftComment(keyComment))) {
660
+ savedComments.set(keyName, {
661
+ commentBefore: keyCommentBefore && !isPipecraftComment(keyCommentBefore) ? keyCommentBefore : undefined,
662
+ comment: keyComment && !isPipecraftComment(keyComment) ? keyComment : undefined
663
+ });
420
664
  }
421
665
  }
422
666
  }
423
- // Clear the entire jobs section to rebuild in correct order
667
+ // Clear the document to rebuild with correct key order
668
+ doc.contents.items = [];
669
+ // Apply root-level operations to set name, run-name, on
670
+ const rootOperations = operations.filter(op => !op.path.startsWith('jobs.'));
671
+ applyPathOperations(doc.contents, rootOperations, doc);
672
+ // Restore document-level comments (user comments at the top of file)
673
+ // Only restore if they're not Pipecraft comments
674
+ const hasUserDocComment = docCommentBefore && !isPipecraftComment(docCommentBefore);
675
+ if (hasUserDocComment) {
676
+ ;
677
+ doc.commentBefore = docCommentBefore;
678
+ }
679
+ else {
680
+ // No user comment at document level, add Pipecraft workflow header
681
+ const pipecraftHeader = `=============================================================================
682
+ PIPECRAFT MANAGED WORKFLOW
683
+ =============================================================================
684
+
685
+ ✅ YOU CAN CUSTOMIZE:
686
+ - test-*** jobs for each domain
687
+ - deploy-*** jobs for each domain
688
+ - remote-test-*** jobs for each domain
689
+ - Workflow name
690
+
691
+ ⚠️ PIPECRAFT MANAGES (do not modify):
692
+ - Workflow triggers, job dependencies, and conditionals
693
+ - Changes detection, version calculation, and tag creation
694
+ - CreatePR, branch management, promote, and release jobs
695
+
696
+ 📌 VERSION PROMOTION BEHAVIOR:
697
+ - Only commits that trigger a version bump will promote to staging/main
698
+ - Non-versioned commits (test, build, etc.) remain on develop
699
+ - This keeps staging/main aligned with tagged releases
700
+
701
+ Running 'pipecraft generate' updates managed sections while preserving
702
+ your customizations in test/deploy/remote-test jobs.
703
+
704
+ 📖 Learn more: https://docs.pipecraft.dev
705
+ =============================================================================`;
706
+ doc.commentBefore = pipecraftHeader;
707
+ }
708
+ if (docComment && !isPipecraftComment(docComment)) {
709
+ ;
710
+ doc.comment = docComment;
711
+ }
712
+ // Replace values for preserve operations that had existing values
713
+ // This maintains the order and comments from operations, but uses the preserved values
714
+ // Only do this if there was an actual existing pipeline (not from base template)
715
+ if (hasExistingPipeline) {
716
+ if (existingName !== undefined && existingName !== null) {
717
+ // Find the 'name' key and replace its value
718
+ const nameIndex = doc.contents.items.findIndex((item) => {
719
+ const key = item.key;
720
+ if (typeof key === 'string')
721
+ return key === 'name';
722
+ if (key && typeof key.value === 'string')
723
+ return key.value === 'name';
724
+ return false;
725
+ });
726
+ if (nameIndex >= 0) {
727
+ doc.contents.items[nameIndex].value = existingName;
728
+ }
729
+ }
730
+ if (existingRunName !== undefined && existingRunName !== null) {
731
+ // Find the 'run-name' key and replace its value
732
+ const runNameIndex = doc.contents.items.findIndex((item) => {
733
+ const key = item.key;
734
+ if (typeof key === 'string')
735
+ return key === 'run-name';
736
+ if (key && typeof key.value === 'string')
737
+ return key.value === 'run-name';
738
+ return false;
739
+ });
740
+ if (runNameIndex >= 0) {
741
+ doc.contents.items[runNameIndex].value = existingRunName;
742
+ }
743
+ }
744
+ }
745
+ // Restore user comments on keys (but don't overwrite Pipecraft comments)
746
+ for (const item of doc.contents.items) {
747
+ const keyName = typeof item.key === 'string' ? item.key : item.key?.value;
748
+ if (keyName && savedComments.has(keyName)) {
749
+ const saved = savedComments.get(keyName);
750
+ const currentKey = item.key;
751
+ // Only restore user comments if current comment is Pipecraft-managed or missing
752
+ const currentCommentBefore = currentKey?.commentBefore;
753
+ const currentComment = currentKey?.comment;
754
+ if (saved.commentBefore && (!currentCommentBefore || !isPipecraftComment(currentCommentBefore))) {
755
+ ;
756
+ currentKey.commentBefore = saved.commentBefore;
757
+ }
758
+ if (saved.comment && (!currentComment || !isPipecraftComment(currentComment))) {
759
+ ;
760
+ currentKey.comment = saved.comment;
761
+ }
762
+ }
763
+ }
764
+ // Note: We don't restore 'on' or 'jobs' here because the operations already created them
765
+ // with proper spacing. Restoring them would overwrite the Scalar keys and lose spacing.
766
+ // Clear the jobs section to rebuild in correct order
424
767
  const jobsNode = doc.contents.get('jobs');
425
768
  if (jobsNode && jobsNode.items) {
426
769
  jobsNode.items = [];
427
- // Clear any orphaned comments that were attached to the jobs node
428
- // When we parse YAML with comments and clear items, comments can become orphaned on the parent
429
- delete jobsNode.commentBefore;
430
- delete jobsNode.comment;
431
- }
432
- // Apply all operations in order - this creates/overwrites Pipecraft jobs
433
- // The 'overwrite' operation handles both create and update cases automatically
434
- applyPathOperations(doc.contents, operations, doc);
435
- // Remove old trigger types that are no longer used (workflow_call, pull_request)
436
- // We only want push and workflow_dispatch triggers
437
- const onNode = doc.contents.get('on');
438
- if (onNode && onNode.delete) {
439
- onNode.delete('workflow_call');
440
- onNode.delete('pull_request');
441
770
  }
771
+ // Apply job operations
772
+ const jobOperations = operations.filter(op => op.path.startsWith('jobs.'));
773
+ applyPathOperations(doc.contents, jobOperations, doc);
442
774
  // Now we need to reorder jobs to match the original order
443
775
  // Collect all current jobs (Pipecraft jobs that were just created)
444
776
  const currentJobs = new Map();
@@ -458,8 +790,14 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
458
790
  // For each job in the original order:
459
791
  // - If it's a Pipecraft job, use the newly created version from currentJobs
460
792
  // - If it's a user job, use the preserved version from userJobs
793
+ // - Skip deprecated jobs
461
794
  // IMPORTANT: Ensure all keys are Scalars (not strings) so we can add comments later
462
795
  for (const jobName of originalJobOrder) {
796
+ // Skip deprecated jobs
797
+ if (DEPRECATED_JOBS.has(jobName)) {
798
+ logger.verbose(`🗑️ Removing deprecated job: ${jobName}`);
799
+ continue;
800
+ }
463
801
  if (PIPECRAFT_OWNED_JOBS.has(jobName)) {
464
802
  // It's a Pipecraft job - use the newly created version
465
803
  const item = currentJobs.get(jobName);
@@ -472,10 +810,14 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
472
810
  }
473
811
  }
474
812
  else {
475
- // It's a user job - re-insert from preserved values
476
- const jobValue = userJobs.get(jobName);
477
- if (jobValue && jobsNode) {
478
- jobsNode.set(jobName, jobValue);
813
+ // It's a user job - re-insert the entire item (with comments)
814
+ const jobItem = userJobs.get(jobName);
815
+ if (jobItem && jobsNode) {
816
+ // Ensure the key is a Scalar, not a string
817
+ if (typeof jobItem.key === 'string') {
818
+ jobItem.key = new Scalar(jobItem.key);
819
+ }
820
+ jobsNode.items.push(jobItem);
479
821
  }
480
822
  }
481
823
  }
@@ -489,20 +831,34 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
489
831
  jobsNode.items.push(item);
490
832
  }
491
833
  }
492
- // Log final job order (after operations are applied)
834
+ // Get the final jobs node to restore comments
493
835
  const finalJobsNode = doc.contents.get('jobs');
836
+ // Restore user comments on the jobs node (if they weren't Pipecraft comments)
837
+ if (jobsNodeCommentBefore && !isPipecraftComment(jobsNodeCommentBefore) && finalJobsNode) {
838
+ ;
839
+ finalJobsNode.commentBefore = jobsNodeCommentBefore;
840
+ logger.debug('✅ Restored comment to jobs node');
841
+ }
842
+ if (jobsNodeComment && !isPipecraftComment(jobsNodeComment) && finalJobsNode) {
843
+ ;
844
+ finalJobsNode.comment = jobsNodeComment;
845
+ }
846
+ // Log final job order (after operations are applied)
494
847
  if (finalJobsNode && finalJobsNode.items && finalJobsNode.items.length > 0) {
495
848
  const jobNames = getJobKeysInOrder(finalJobsNode);
496
849
  if (jobNames.length > 0) {
497
- console.log('📋 Final job order:', jobNames);
850
+ logger.debug('📋 Final job order:', jobNames);
498
851
  }
499
- // Remove duplicate comment headers
500
- // When we reuse existing job values, they may carry old comments from parsing
501
- // We want to keep only the comments we explicitly set on keys
852
+ // Remove duplicate Pipecraft comment headers from values
853
+ // When we reuse existing job values, they may carry old Pipecraft comments from parsing
854
+ // We want to clear Pipecraft comments but preserve user comments
502
855
  for (const item of finalJobsNode.items) {
503
856
  if (item.value && item.value.commentBefore) {
504
- // Clear commentBefore from values - comments should only be on keys
505
- delete item.value.commentBefore;
857
+ const valueComment = item.value.commentBefore;
858
+ // Only clear if it's a Pipecraft-managed comment
859
+ if (isPipecraftComment(valueComment)) {
860
+ delete item.value.commentBefore;
861
+ }
506
862
  }
507
863
  }
508
864
  // Add section headers and spacing to domain-based jobs
@@ -630,7 +986,56 @@ const loadExistingPipeline = (filePath) => {
630
986
  return fs.readFileSync(filePath, 'utf8');
631
987
  };
632
988
  /**
633
- * Main export - path-based pipeline generator
989
+ * Main pipeline generator entry point.
990
+ *
991
+ * Generates the complete GitHub Actions pipeline workflow with intelligent
992
+ * merging of existing user customizations.
993
+ *
994
+ * @param {PinionContext & { existingPipeline?: any, outputPipelinePath?: string }} ctx - Generator context
995
+ * @param {any} [ctx.existingPipeline] - Parsed existing pipeline YAML for merging
996
+ * @param {string} [ctx.existingPipelineContent] - Raw existing pipeline content for comment preservation
997
+ * @param {string} [ctx.outputPipelinePath] - Custom output path (default: .github/workflows/pipeline.yml)
998
+ * @param {string[]} ctx.branchFlow - Branch flow sequence (e.g., ['develop', 'staging', 'main'])
999
+ * @param {Record<string, DomainConfig>} ctx.domains - Domain configurations for change detection
1000
+ * @param {string} [ctx.ciProvider] - CI provider ('github' or 'gitlab')
1001
+ * @param {string} [ctx.mergeStrategy] - Merge strategy ('fast-forward' or 'merge')
1002
+ * @returns {Promise<PinionContext>} Updated context with generated YAML
1003
+ *
1004
+ * @throws {Error} If pipeline file cannot be written
1005
+ * @throws {Error} If existing pipeline cannot be parsed
1006
+ *
1007
+ * @example
1008
+ * ```typescript
1009
+ * // Generate new pipeline
1010
+ * await generate({
1011
+ * cwd: '/path/to/project',
1012
+ * branchFlow: ['develop', 'main'],
1013
+ * domains: {
1014
+ * api: { paths: ['src/api/**'], test: true }
1015
+ * }
1016
+ * })
1017
+ *
1018
+ * // Update existing pipeline (preserves user jobs)
1019
+ * const existing = parseDocument(readFileSync('pipeline.yml', 'utf8'))
1020
+ * await generate({
1021
+ * cwd: '/path/to/project',
1022
+ * existingPipeline: existing,
1023
+ * existingPipelineContent: readFileSync('pipeline.yml', 'utf8'),
1024
+ * branchFlow: ['develop', 'staging', 'main'],
1025
+ * domains: { ... }
1026
+ * })
1027
+ * ```
1028
+ *
1029
+ * @note The generator performs these steps:
1030
+ * 1. Calls `createPathBasedPipeline()` to build the workflow
1031
+ * 2. Logs merge status (new vs. merged)
1032
+ * 3. Writes the final YAML to the output path
1033
+ *
1034
+ * The heavy lifting is done by `createPathBasedPipeline()` which handles:
1035
+ * - Job generation based on domains and branch flow
1036
+ * - User job preservation and merging
1037
+ * - Comment preservation from existing pipeline
1038
+ * - Intelligent job ordering
634
1039
  */
635
1040
  export const generate = (ctx) => Promise.resolve(ctx)
636
1041
  .then((ctx) => {
@@ -645,7 +1050,7 @@ export const generate = (ctx) => Promise.resolve(ctx)
645
1050
  // Provide user feedback about file operation
646
1051
  const outputPath = ctx.outputPipelinePath || '.github/workflows/pipeline.yml';
647
1052
  const status = ctx.mergeStatus === 'merged' ? '🔄 Merged with existing' : '📝 Created new';
648
- console.log(`${status} ${outputPath}`);
1053
+ logger.verbose(`${status} ${outputPath}`);
649
1054
  return ctx;
650
1055
  })
651
1056
  .then(renderTemplate((ctx) => ctx.yamlContent, toFile((ctx) => ctx.outputPipelinePath || '.github/workflows/pipeline.yml')));