pipecraft 0.0.0-releaseit

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 (176) hide show
  1. package/.claude/settings.local.json +35 -0
  2. package/.github/actions/calculate-version/action.yml +106 -0
  3. package/.github/actions/create-pr/action.yml +122 -0
  4. package/.github/actions/create-release/action.yml +74 -0
  5. package/.github/actions/create-tag/action.yml +94 -0
  6. package/.github/actions/detect-changes/action.yml +62 -0
  7. package/.github/actions/manage-branch/action.yml +113 -0
  8. package/.github/actions/promote-branch/action.yml +352 -0
  9. package/.github/example/workflows/job.analyze.code.yml +26 -0
  10. package/.github/example/workflows/job.analyze.docker.yml +32 -0
  11. package/.github/example/workflows/job.app.api.deploy.yml +127 -0
  12. package/.github/example/workflows/job.app.api.test.yml +102 -0
  13. package/.github/example/workflows/job.app.docs.deploy.yml +40 -0
  14. package/.github/example/workflows/job.app.docs.test.yml +50 -0
  15. package/.github/example/workflows/job.app.web.deploy.yml +96 -0
  16. package/.github/example/workflows/job.app.web.test.yml +49 -0
  17. package/.github/example/workflows/job.changes.yml +82 -0
  18. package/.github/example/workflows/job.create-pr.yml +96 -0
  19. package/.github/example/workflows/job.env-check.yml +43 -0
  20. package/.github/example/workflows/job.fast-forward.yml +103 -0
  21. package/.github/example/workflows/job.lint.yml +64 -0
  22. package/.github/example/workflows/job.pr-name.yml +70 -0
  23. package/.github/example/workflows/job.pr-squash-name.yml +27 -0
  24. package/.github/example/workflows/job.tag.yml +38 -0
  25. package/.github/example/workflows/job.version.yml +99 -0
  26. package/.github/example/workflows/lib.plugin-base.publish.yml +59 -0
  27. package/.github/example/workflows/pipe.yml +178 -0
  28. package/.github/example/workflows/scripts/fastforward.sh +51 -0
  29. package/.github/example/workflows/scripts/pipe-cleaner.sh +70 -0
  30. package/.github/scripts/fastforward.sh +51 -0
  31. package/.github/scripts/pipe-cleaner.sh +70 -0
  32. package/.github/workflows/pipeline.yml +220 -0
  33. package/.github/workflows/pr-title-check.yml +70 -0
  34. package/.github/workflows/publish.yml +39 -0
  35. package/.pipecraftrc.json +78 -0
  36. package/.release-it.cjs +71 -0
  37. package/PIPELINE_TESTING_PLAN.md +499 -0
  38. package/README.md +1101 -0
  39. package/TRUNK_FLOW_PLAN.md +401 -0
  40. package/assets/logo_banner.png +0 -0
  41. package/assets/logo_banner.webp +0 -0
  42. package/dist/cli/index.d.ts +3 -0
  43. package/dist/cli/index.d.ts.map +1 -0
  44. package/dist/cli/index.js +364 -0
  45. package/dist/cli/index.js.map +1 -0
  46. package/dist/generators/init.tpl.d.ts +3 -0
  47. package/dist/generators/init.tpl.d.ts.map +1 -0
  48. package/dist/generators/init.tpl.js +117 -0
  49. package/dist/generators/init.tpl.js.map +1 -0
  50. package/dist/generators/workflows.tpl.d.ts +7 -0
  51. package/dist/generators/workflows.tpl.d.ts.map +1 -0
  52. package/dist/generators/workflows.tpl.js +73 -0
  53. package/dist/generators/workflows.tpl.js.map +1 -0
  54. package/dist/templates/actions/calculate-version.yml.tpl.d.ts +3 -0
  55. package/dist/templates/actions/calculate-version.yml.tpl.d.ts.map +1 -0
  56. package/dist/templates/actions/calculate-version.yml.tpl.js +122 -0
  57. package/dist/templates/actions/calculate-version.yml.tpl.js.map +1 -0
  58. package/dist/templates/actions/create-pr.yml.tpl.d.ts +3 -0
  59. package/dist/templates/actions/create-pr.yml.tpl.d.ts.map +1 -0
  60. package/dist/templates/actions/create-pr.yml.tpl.js +145 -0
  61. package/dist/templates/actions/create-pr.yml.tpl.js.map +1 -0
  62. package/dist/templates/actions/create-tag.yml.tpl.d.ts +3 -0
  63. package/dist/templates/actions/create-tag.yml.tpl.d.ts.map +1 -0
  64. package/dist/templates/actions/create-tag.yml.tpl.js +111 -0
  65. package/dist/templates/actions/create-tag.yml.tpl.js.map +1 -0
  66. package/dist/templates/actions/detect-changes.yml.tpl.d.ts +3 -0
  67. package/dist/templates/actions/detect-changes.yml.tpl.d.ts.map +1 -0
  68. package/dist/templates/actions/detect-changes.yml.tpl.js +79 -0
  69. package/dist/templates/actions/detect-changes.yml.tpl.js.map +1 -0
  70. package/dist/templates/actions/manage-branch.yml.tpl.d.ts +3 -0
  71. package/dist/templates/actions/manage-branch.yml.tpl.d.ts.map +1 -0
  72. package/dist/templates/actions/manage-branch.yml.tpl.js +130 -0
  73. package/dist/templates/actions/manage-branch.yml.tpl.js.map +1 -0
  74. package/dist/templates/actions/promote-branch.yml.tpl.d.ts +3 -0
  75. package/dist/templates/actions/promote-branch.yml.tpl.d.ts.map +1 -0
  76. package/dist/templates/actions/promote-branch.yml.tpl.js +362 -0
  77. package/dist/templates/actions/promote-branch.yml.tpl.js.map +1 -0
  78. package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts +16 -0
  79. package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts.map +1 -0
  80. package/dist/templates/workflows/pipeline-path-based.yml.tpl.js +652 -0
  81. package/dist/templates/workflows/pipeline-path-based.yml.tpl.js.map +1 -0
  82. package/dist/types/index.d.ts +63 -0
  83. package/dist/types/index.d.ts.map +1 -0
  84. package/dist/types/index.js +2 -0
  85. package/dist/types/index.js.map +1 -0
  86. package/dist/utils/ast-path-operations.d.ts +287 -0
  87. package/dist/utils/ast-path-operations.d.ts.map +1 -0
  88. package/dist/utils/ast-path-operations.js +419 -0
  89. package/dist/utils/ast-path-operations.js.map +1 -0
  90. package/dist/utils/config.d.ts +3 -0
  91. package/dist/utils/config.d.ts.map +1 -0
  92. package/dist/utils/config.js +49 -0
  93. package/dist/utils/config.js.map +1 -0
  94. package/dist/utils/github-setup.d.ts +74 -0
  95. package/dist/utils/github-setup.d.ts.map +1 -0
  96. package/dist/utils/github-setup.js +418 -0
  97. package/dist/utils/github-setup.js.map +1 -0
  98. package/dist/utils/idempotency.d.ts +47 -0
  99. package/dist/utils/idempotency.d.ts.map +1 -0
  100. package/dist/utils/idempotency.js +176 -0
  101. package/dist/utils/idempotency.js.map +1 -0
  102. package/dist/utils/preflight.d.ts +55 -0
  103. package/dist/utils/preflight.d.ts.map +1 -0
  104. package/dist/utils/preflight.js +261 -0
  105. package/dist/utils/preflight.js.map +1 -0
  106. package/dist/utils/versioning.d.ts +41 -0
  107. package/dist/utils/versioning.d.ts.map +1 -0
  108. package/dist/utils/versioning.js +219 -0
  109. package/dist/utils/versioning.js.map +1 -0
  110. package/docs/USER_JOURNEY_ERRORS.md +352 -0
  111. package/eslint.config.js +77 -0
  112. package/examples/basic-config.json +41 -0
  113. package/examples/monorepo-config.json +49 -0
  114. package/examples/usage.md +289 -0
  115. package/package.json +58 -0
  116. package/scripts/debug-workflows.sh +339 -0
  117. package/scripts/parse-pipeline.js +29 -0
  118. package/scripts/test-job-order.cjs +140 -0
  119. package/scripts/validate-pipeline.cjs +379 -0
  120. package/scripts/verify-job-order.sh +30 -0
  121. package/src/cli/index.ts +414 -0
  122. package/src/generators/init.tpl.ts +126 -0
  123. package/src/generators/workflows.tpl.ts +80 -0
  124. package/src/templates/actions/calculate-version.yml.tpl.ts +124 -0
  125. package/src/templates/actions/create-pr.yml.tpl.ts +147 -0
  126. package/src/templates/actions/create-tag.yml.tpl.ts +112 -0
  127. package/src/templates/actions/detect-changes.yml.tpl.ts +87 -0
  128. package/src/templates/actions/manage-branch.yml.tpl.ts +132 -0
  129. package/src/templates/actions/promote-branch.yml.tpl.ts +364 -0
  130. package/src/templates/workflows/pipeline-path-based.yml.tpl.ts +706 -0
  131. package/src/types/index.ts +64 -0
  132. package/src/utils/README-ast-path-operations.md +390 -0
  133. package/src/utils/ast-path-operations.ts +581 -0
  134. package/src/utils/config.ts +64 -0
  135. package/src/utils/github-setup.ts +558 -0
  136. package/src/utils/idempotency.ts +215 -0
  137. package/src/utils/preflight.ts +306 -0
  138. package/src/utils/versioning.ts +244 -0
  139. package/tests/README.md +229 -0
  140. package/tests/TEST_STRUCTURE.md +256 -0
  141. package/tests/act/run-act-tests.sh +345 -0
  142. package/tests/debugging/debug-utils.ts +538 -0
  143. package/tests/debugging/debug-workflow.test.ts +339 -0
  144. package/tests/debugging/debug-workflows.sh +339 -0
  145. package/tests/debugging/iterative-debug.ts +652 -0
  146. package/tests/debugging/run-debug-tests.sh +431 -0
  147. package/tests/fixtures/basic-config.json +51 -0
  148. package/tests/fixtures/invalid-config.json +9 -0
  149. package/tests/fixtures/pipeline-generated.yml +235 -0
  150. package/tests/fixtures/pipeline-preserve-comments.yml +245 -0
  151. package/tests/fixtures/pipeline-user-modified.yml +245 -0
  152. package/tests/fixtures/test-config.json +58 -0
  153. package/tests/github-live/README.md +250 -0
  154. package/tests/github-local/JOB_WORKFLOW_TESTS.md +296 -0
  155. package/tests/github-local/README.md +240 -0
  156. package/tests/github-local/run-all-tests.sh +422 -0
  157. package/tests/github-local/test-job-workflows.sh +631 -0
  158. package/tests/github-local/test-pipeline-workflow.sh +440 -0
  159. package/tests/integration/generators.test.ts +578 -0
  160. package/tests/integration/path-based-template.test.ts +510 -0
  161. package/tests/integration/simple-path-based.test.ts +415 -0
  162. package/tests/setup.ts +56 -0
  163. package/tests/unit/ast-path-operations-extended.test.ts +302 -0
  164. package/tests/unit/cli.test.ts +541 -0
  165. package/tests/unit/config-extended.test.ts +412 -0
  166. package/tests/unit/config.test.ts +152 -0
  167. package/tests/unit/github-setup.test.ts +189 -0
  168. package/tests/unit/idempotency-isolated.test.ts +297 -0
  169. package/tests/unit/job-order.test.ts +157 -0
  170. package/tests/unit/pipeline-path-based.test.ts +511 -0
  171. package/tests/unit/validate-pipeline.test.ts +384 -0
  172. package/tests/unit/versioning-extended.test.ts +279 -0
  173. package/tests/unit/versioning.test.ts +241 -0
  174. package/tsconfig.debug.json +17 -0
  175. package/tsconfig.json +28 -0
  176. package/vitest.config.ts +51 -0
@@ -0,0 +1,511 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest'
2
+ import { readFileSync, existsSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { parseDocument } from 'yaml'
5
+ import { createPathBasedPipeline } from '../../src/templates/workflows/pipeline-path-based.yml.tpl'
6
+
7
+ /**
8
+ * Extract job names in order from a YAML file
9
+ */
10
+ function getJobOrder(filePath: string): string[] {
11
+ if (!existsSync(filePath)) {
12
+ console.error(`File not found: ${filePath}`)
13
+ return []
14
+ }
15
+
16
+ try {
17
+ const content = readFileSync(filePath, 'utf8')
18
+ const doc = parseDocument(content)
19
+
20
+ if (!doc.contents || !(doc.contents as any).get('jobs')) {
21
+ return []
22
+ }
23
+
24
+ const jobsNode = (doc.contents as any).get('jobs')
25
+ if (!jobsNode.items) {
26
+ return []
27
+ }
28
+
29
+ return jobsNode.items.map(item => item.key.value)
30
+ } catch (error) {
31
+ console.error(`Error parsing ${filePath}:`, error)
32
+ return []
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Check if workflow_dispatch inputs exist in YAML content
38
+ */
39
+ function hasWorkflowDispatchInputs(content: string): boolean {
40
+ return content.includes('workflow_dispatch:') &&
41
+ content.includes('inputs:') &&
42
+ content.includes('version:') &&
43
+ content.includes('baseRef:')
44
+ }
45
+
46
+ /**
47
+ * Check if workflow_call inputs exist in YAML content
48
+ */
49
+ function hasWorkflowCallInputs(content: string): boolean {
50
+ return content.includes('workflow_call:') &&
51
+ content.includes('inputs:') &&
52
+ content.includes('version:') &&
53
+ content.includes('baseRef:')
54
+ }
55
+
56
+ describe('Pipeline Path-Based Template', () => {
57
+ const testFixtures = {
58
+ original: join(process.cwd(), 'tests/fixtures/pipeline-user-modified.yml'),
59
+ config: join(process.cwd(), 'tests/fixtures/test-config.json')
60
+ }
61
+
62
+ describe('Job Order Preservation', () => {
63
+ it('should preserve job order when Pipecraft jobs exist', () => {
64
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
65
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
66
+
67
+ const ctx = {
68
+ existingPipelineContent: originalContent,
69
+ ...config
70
+ }
71
+
72
+ const result = createPathBasedPipeline(ctx)
73
+ const generatedDoc = parseDocument(result.yamlContent)
74
+ const generatedJobsNode = (generatedDoc.contents as any).get('jobs')
75
+ const generatedOrder = generatedJobsNode.items.map(item => item.key.value)
76
+
77
+ const originalOrder = getJobOrder(testFixtures.original)
78
+
79
+ // Should have same number of jobs
80
+ expect(generatedOrder.length).toBe(originalOrder.length)
81
+
82
+ // Should have same job order
83
+ expect(generatedOrder).toEqual(originalOrder)
84
+ })
85
+
86
+ it('should maintain Pipecraft jobs in their original positions', () => {
87
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
88
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
89
+
90
+ const ctx = {
91
+ existingPipelineContent: originalContent,
92
+ ...config
93
+ }
94
+
95
+ const result = createPathBasedPipeline(ctx)
96
+ const generatedDoc = parseDocument(result.yamlContent)
97
+ const generatedJobsNode = (generatedDoc.contents as any).get('jobs')
98
+ const generatedOrder = generatedJobsNode.items.map(item => item.key.value)
99
+
100
+ const originalOrder = getJobOrder(testFixtures.original)
101
+
102
+ // Find positions of Pipecraft jobs
103
+ const pipecraftJobs = ['changes', 'version', 'tag', 'createpr', 'branch']
104
+
105
+ pipecraftJobs.forEach(job => {
106
+ const originalPos = originalOrder.indexOf(job)
107
+ const generatedPos = generatedOrder.indexOf(job)
108
+
109
+ if (originalPos !== -1) {
110
+ expect(generatedPos).toBe(originalPos)
111
+ }
112
+ })
113
+ })
114
+
115
+ it('should preserve user jobs in their original positions', () => {
116
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
117
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
118
+
119
+ const ctx = {
120
+ existingPipelineContent: originalContent,
121
+ ...config
122
+ }
123
+
124
+ const result = createPathBasedPipeline(ctx)
125
+ const generatedDoc = parseDocument(result.yamlContent)
126
+ const generatedJobsNode = (generatedDoc.contents as any).get('jobs')
127
+ const generatedOrder = generatedJobsNode.items.map(item => item.key.value)
128
+
129
+ const originalOrder = getJobOrder(testFixtures.original)
130
+
131
+ // Find user jobs (non-Pipecraft jobs)
132
+ const pipecraftJobs = new Set(['changes', 'version', 'tag', 'createpr', 'branch'])
133
+ const userJobs = originalOrder.filter(job => !pipecraftJobs.has(job))
134
+
135
+ userJobs.forEach(userJob => {
136
+ const originalPos = originalOrder.indexOf(userJob)
137
+ const generatedPos = generatedOrder.indexOf(userJob)
138
+ expect(generatedPos).toBe(originalPos)
139
+ })
140
+ })
141
+ })
142
+
143
+ describe('Workflow Inputs', () => {
144
+ it('should add workflow_dispatch inputs', () => {
145
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
146
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
147
+
148
+ const ctx = {
149
+ existingPipelineContent: originalContent,
150
+ ...config
151
+ }
152
+
153
+ const result = createPathBasedPipeline(ctx)
154
+
155
+ expect(hasWorkflowDispatchInputs(result.yamlContent)).toBe(true)
156
+ })
157
+
158
+ it('should add workflow_call inputs', () => {
159
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
160
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
161
+
162
+ const ctx = {
163
+ existingPipelineContent: originalContent,
164
+ ...config
165
+ }
166
+
167
+ const result = createPathBasedPipeline(ctx)
168
+
169
+ expect(hasWorkflowCallInputs(result.yamlContent)).toBe(true)
170
+ })
171
+
172
+ it('should add workflow inputs without affecting job order', () => {
173
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
174
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
175
+
176
+ const ctx = {
177
+ existingPipelineContent: originalContent,
178
+ ...config
179
+ }
180
+
181
+ const result = createPathBasedPipeline(ctx)
182
+
183
+ // Should have workflow inputs
184
+ expect(hasWorkflowDispatchInputs(result.yamlContent)).toBe(true)
185
+ expect(hasWorkflowCallInputs(result.yamlContent)).toBe(true)
186
+
187
+ // But job order should still be preserved
188
+ const generatedDoc = parseDocument(result.yamlContent)
189
+ const generatedJobsNode = (generatedDoc.contents as any).get('jobs')
190
+ const generatedOrder = generatedJobsNode.items.map(item => item.key.value)
191
+
192
+ const originalOrder = getJobOrder(testFixtures.original)
193
+ expect(generatedOrder).toEqual(originalOrder)
194
+ })
195
+ })
196
+
197
+ describe('Branch Configuration', () => {
198
+ it('should merge template branches with user branches', () => {
199
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
200
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
201
+
202
+ const ctx = {
203
+ existingPipelineContent: originalContent,
204
+ ...config
205
+ }
206
+
207
+ const result = createPathBasedPipeline(ctx)
208
+ const generatedDoc = parseDocument(result.yamlContent)
209
+ const pullRequestNode = (generatedDoc.contents as any).get('on').get('pull_request')
210
+ const branches = pullRequestNode.get('branches')
211
+
212
+ // Should include template branches
213
+ expect(branches.items.map(item => item.value)).toContain('alpha')
214
+ expect(branches.items.map(item => item.value)).toContain('beta')
215
+ expect(branches.items.map(item => item.value)).toContain('gamma')
216
+ expect(branches.items.map(item => item.value)).toContain('delta')
217
+ expect(branches.items.map(item => item.value)).toContain('epsilon')
218
+
219
+ // Should preserve user branches
220
+ expect(branches.items.map(item => item.value)).toContain('develop')
221
+ expect(branches.items.map(item => item.value)).toContain('feature')
222
+ })
223
+ })
224
+
225
+ describe('Pipecraft Job Content', () => {
226
+ it('should update Pipecraft job content with latest template', () => {
227
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
228
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
229
+
230
+ const ctx = {
231
+ existingPipelineContent: originalContent,
232
+ ...config
233
+ }
234
+
235
+ const result = createPathBasedPipeline(ctx)
236
+
237
+ // Check that Pipecraft jobs have the expected structure
238
+ expect(result.yamlContent).toContain('runs-on: ubuntu-latest')
239
+ expect(result.yamlContent).toContain('./.github/actions/detect-changes')
240
+ expect(result.yamlContent).toContain('./.github/actions/calculate-version')
241
+ expect(result.yamlContent).toContain('./.github/actions/create-tag')
242
+ expect(result.yamlContent).toContain('./.github/actions/create-pr')
243
+ expect(result.yamlContent).toContain('./.github/actions/manage-branch')
244
+ })
245
+
246
+ it('should use correct branch flow in job conditions', () => {
247
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
248
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
249
+
250
+ const ctx = {
251
+ existingPipelineContent: originalContent,
252
+ ...config
253
+ }
254
+
255
+ const result = createPathBasedPipeline(ctx)
256
+
257
+ // Should use the correct initial branch from config
258
+ expect(result.yamlContent).toContain("github.ref_name == 'alpha'")
259
+ expect(result.yamlContent).toContain("github.ref_name != 'epsilon'")
260
+ })
261
+ })
262
+
263
+ describe('Comment Preservation', () => {
264
+ it('should preserve user comments', () => {
265
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
266
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
267
+
268
+ const ctx = {
269
+ existingPipelineContent: originalContent,
270
+ ...config
271
+ }
272
+
273
+ const result = createPathBasedPipeline(ctx)
274
+
275
+ // Should preserve user comments
276
+ expect(result.yamlContent).toContain('# USER COMMENT 1')
277
+ expect(result.yamlContent).toContain('# USER COMMENT 9')
278
+ expect(result.yamlContent).toContain('# USER COMMENT 10')
279
+ })
280
+ })
281
+
282
+ describe('New Pipeline Generation', () => {
283
+ it('should create new pipeline when no existing content', () => {
284
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
285
+
286
+ const ctx = {
287
+ ...config
288
+ }
289
+
290
+ const result = createPathBasedPipeline(ctx)
291
+
292
+ expect(result.mergeStatus).toBe('overwritten')
293
+ expect(result.yamlContent).toContain('name: "Pipeline"')
294
+ expect(result.yamlContent).toContain('on:')
295
+ expect(result.yamlContent).toContain('jobs:')
296
+ })
297
+
298
+ it('should build jobs in correct order for new pipeline', () => {
299
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
300
+
301
+ const ctx = {
302
+ ...config
303
+ }
304
+
305
+ const result = createPathBasedPipeline(ctx)
306
+ const generatedDoc = parseDocument(result.yamlContent)
307
+ const generatedJobsNode = (generatedDoc.contents as any).get('jobs')
308
+ const generatedOrder = generatedJobsNode.items.map(item => item.key.value)
309
+
310
+ // Should have Pipecraft jobs in correct order
311
+ const expectedOrder = ['changes', 'version', 'tag', 'createpr', 'branch']
312
+ const pipecraftJobs = generatedOrder.filter(job =>
313
+ ['changes', 'version', 'tag', 'createpr', 'branch'].includes(job)
314
+ )
315
+
316
+ expect(pipecraftJobs).toEqual(expectedOrder)
317
+ })
318
+ })
319
+
320
+ describe('Edge Cases', () => {
321
+ it('should handle pipeline with no existing Pipecraft jobs', () => {
322
+ // Create a pipeline with only user jobs
323
+ const userOnlyPipeline = `name: "User Pipeline"
324
+ on:
325
+ pull_request:
326
+ branches: [main]
327
+ jobs:
328
+ user-job-1:
329
+ runs-on: ubuntu-latest
330
+ steps:
331
+ - run: echo "User job"
332
+ user-job-2:
333
+ runs-on: ubuntu-latest
334
+ steps:
335
+ - run: echo "Another user job"`
336
+
337
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
338
+
339
+ const ctx = {
340
+ existingPipelineContent: userOnlyPipeline,
341
+ ...config
342
+ }
343
+
344
+ const result = createPathBasedPipeline(ctx)
345
+ const generatedDoc = parseDocument(result.yamlContent)
346
+ const generatedJobsNode = (generatedDoc.contents as any).get('jobs')
347
+ const generatedOrder = generatedJobsNode.items.map(item => item.key.value)
348
+
349
+ // Should preserve user jobs and add Pipecraft jobs
350
+ expect(generatedOrder).toContain('user-job-1')
351
+ expect(generatedOrder).toContain('user-job-2')
352
+ expect(generatedOrder).toContain('changes')
353
+ expect(generatedOrder).toContain('version')
354
+ expect(generatedOrder).toContain('tag')
355
+ expect(generatedOrder).toContain('createpr')
356
+ expect(generatedOrder).toContain('branch')
357
+
358
+ // When no existing Pipecraft jobs, Pipecraft jobs are added first, then user jobs
359
+ // This is the expected behavior based on the template logic
360
+ const userJobPositions = [
361
+ generatedOrder.indexOf('user-job-1'),
362
+ generatedOrder.indexOf('user-job-2')
363
+ ]
364
+ const pipecraftJobPositions = [
365
+ generatedOrder.indexOf('changes'),
366
+ generatedOrder.indexOf('version')
367
+ ]
368
+
369
+ // Pipecraft jobs should come first, then user jobs (when no existing Pipecraft jobs)
370
+ expect(Math.min(...pipecraftJobPositions)).toBeLessThan(Math.max(...userJobPositions))
371
+ })
372
+
373
+ it('should handle empty pipeline gracefully', () => {
374
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
375
+
376
+ const ctx = {
377
+ ...config
378
+ }
379
+
380
+ const result = createPathBasedPipeline(ctx)
381
+
382
+ expect(result.yamlContent).toBeDefined()
383
+ expect(result.yamlContent.length).toBeGreaterThan(0)
384
+ expect(result.mergeStatus).toBe('overwritten')
385
+ })
386
+
387
+ it('should handle malformed YAML gracefully', () => {
388
+ const malformedPipeline = `name: "Malformed"
389
+ on:
390
+ pull_request:
391
+ jobs:
392
+ invalid-job:
393
+ runs-on: ubuntu-latest
394
+ steps:
395
+ - name: Test
396
+ run: echo "test"
397
+ # Missing closing quote
398
+ another-job:
399
+ runs-on: ubuntu-latest
400
+ steps:
401
+ - name: Test
402
+ run: echo "test"`
403
+
404
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
405
+
406
+ const ctx = {
407
+ existingPipelineContent: malformedPipeline,
408
+ ...config
409
+ }
410
+
411
+ // Should not throw an error
412
+ expect(() => createPathBasedPipeline(ctx)).not.toThrow()
413
+ })
414
+ })
415
+
416
+ describe('Configuration Handling', () => {
417
+ it('should use custom branch flow from config', () => {
418
+ const customConfig = {
419
+ branchFlow: ['custom1', 'custom2', 'custom3'],
420
+ initialBranch: 'custom1',
421
+ finalBranch: 'custom3'
422
+ }
423
+
424
+ const ctx = {
425
+ ...customConfig
426
+ }
427
+
428
+ const result = createPathBasedPipeline(ctx)
429
+
430
+ // Should use custom branch names
431
+ expect(result.yamlContent).toContain("github.ref_name == 'custom1'")
432
+ expect(result.yamlContent).toContain("github.ref_name != 'custom3'")
433
+ })
434
+
435
+ it('should fall back to default branch flow when not provided', () => {
436
+ const ctx = {}
437
+
438
+ const result = createPathBasedPipeline(ctx)
439
+
440
+ // Should use default branch flow
441
+ expect(result.yamlContent).toContain("github.ref_name == 'develop'")
442
+ expect(result.yamlContent).toContain("github.ref_name != 'main'")
443
+ })
444
+
445
+ it('should handle missing config gracefully', () => {
446
+ const ctx = {
447
+ existingPipelineContent: readFileSync(testFixtures.original, 'utf8')
448
+ }
449
+
450
+ const result = createPathBasedPipeline(ctx)
451
+
452
+ expect(result.yamlContent).toBeDefined()
453
+ expect(result.mergeStatus).toBe('merged')
454
+ })
455
+ })
456
+
457
+ describe('Job Content Validation', () => {
458
+ it('should have correct job dependencies', () => {
459
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
460
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
461
+
462
+ const ctx = {
463
+ existingPipelineContent: originalContent,
464
+ ...config
465
+ }
466
+
467
+ const result = createPathBasedPipeline(ctx)
468
+
469
+ // Check job dependencies
470
+ expect(result.yamlContent).toContain('needs: changes') // version job
471
+ expect(result.yamlContent).toContain('needs: version') // tag job
472
+ expect(result.yamlContent).toContain('needs: [ changes, version ]') // createpr job
473
+ expect(result.yamlContent).toContain('needs: createpr') // branch job
474
+ })
475
+
476
+ it('should have correct job conditions', () => {
477
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
478
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
479
+
480
+ const ctx = {
481
+ existingPipelineContent: originalContent,
482
+ ...config
483
+ }
484
+
485
+ const result = createPathBasedPipeline(ctx)
486
+
487
+ // Check job conditions
488
+ expect(result.yamlContent).toContain("if: github.ref_name == 'alpha'") // version and tag
489
+ expect(result.yamlContent).toContain("if: github.ref_name != 'epsilon'") // createpr
490
+ })
491
+
492
+ it('should use correct action paths', () => {
493
+ const originalContent = readFileSync(testFixtures.original, 'utf8')
494
+ const config = JSON.parse(readFileSync(testFixtures.config, 'utf8'))
495
+
496
+ const ctx = {
497
+ existingPipelineContent: originalContent,
498
+ ...config
499
+ }
500
+
501
+ const result = createPathBasedPipeline(ctx)
502
+
503
+ // Check action paths
504
+ expect(result.yamlContent).toContain('./.github/actions/detect-changes')
505
+ expect(result.yamlContent).toContain('./.github/actions/calculate-version')
506
+ expect(result.yamlContent).toContain('./.github/actions/create-tag')
507
+ expect(result.yamlContent).toContain('./.github/actions/create-pr')
508
+ expect(result.yamlContent).toContain('./.github/actions/manage-branch')
509
+ })
510
+ })
511
+ })