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,581 @@
1
+ import { parseDocument, stringify, YAMLMap, YAMLSeq, Scalar, Node } from 'yaml'
2
+
3
+ /**
4
+ * # AST Path Operations for YAML Manipulation
5
+ *
6
+ * This module provides a powerful and precise way to manipulate YAML documents using
7
+ * path-based operations. It allows you to target specific locations in the YAML
8
+ * structure and apply different types of operations (set, merge, overwrite, preserve).
9
+ *
10
+ * ## Key Features
11
+ *
12
+ * - **Path-based targeting**: Use dot notation to target specific YAML paths
13
+ * - **Multiple operation types**: Set, merge, overwrite, or preserve values
14
+ * - **Flexible value types**: Support objects, arrays, strings, YAML nodes, and parsed documents
15
+ * - **Type safety**: Full TypeScript support with proper Node types
16
+ * - **Context injection**: Dynamic values can be injected at build time
17
+ *
18
+ * ## Usage Examples
19
+ *
20
+ * ### Basic Path Operations
21
+ * ```typescript
22
+ * import { applyPathOperations, createValueFromString } from './ast-path-operations'
23
+ *
24
+ * const operations = [
25
+ * {
26
+ * path: 'on.workflow_call.inputs.version',
27
+ * operation: 'set',
28
+ * value: {
29
+ * description: 'The version to deploy',
30
+ * required: false,
31
+ * type: 'string'
32
+ * }
33
+ * }
34
+ * ]
35
+ *
36
+ * applyPathOperations(doc, operations)
37
+ * ```
38
+ *
39
+ * ### Complex Job Definitions
40
+ * ```typescript
41
+ * const jobOperation = {
42
+ * path: 'jobs.changes',
43
+ * operation: 'overwrite',
44
+ * value: createValueFromString(`
45
+ * runs-on: ubuntu-latest
46
+ * steps:
47
+ * - uses: ./.github/actions/detect-changes
48
+ * with:
49
+ * baseRef: ${{ inputs.baseRef || 'main' }}
50
+ * `)
51
+ * }
52
+ * ```
53
+ *
54
+ * ### Array Merging
55
+ * ```typescript
56
+ * const branchOperation = {
57
+ * path: 'on.pull_request.branches',
58
+ * operation: 'merge',
59
+ * value: ['develop', 'staging', 'main']
60
+ * }
61
+ * ```
62
+ *
63
+ * ## Operation Types
64
+ *
65
+ * - **`set`**: Set a value at the specified path (creates if doesn't exist)
66
+ * - **`merge`**: Merge with existing value (for objects/arrays)
67
+ * - **`overwrite`**: Replace existing value completely
68
+ * - **`preserve`**: Keep existing value, ignore template value
69
+ *
70
+ * ## Value Types Supported
71
+ *
72
+ * - **Objects**: `{ key: 'value' }` - Simple key-value pairs
73
+ * - **Arrays**: `['item1', 'item2']` - Simple arrays
74
+ * - **YAML Strings**: Multi-line YAML with proper formatting
75
+ * - **Parsed Documents**: Pre-parsed YAML nodes
76
+ * - **Primitives**: strings, numbers, booleans
77
+ *
78
+ * @fileoverview Path-based AST operations for precise YAML manipulation
79
+ * @author Pipecraft Team
80
+ * @version 1.0.0
81
+ */
82
+
83
+ /**
84
+ * Available operation types for path-based AST manipulation
85
+ *
86
+ * @typedef {('set' | 'merge' | 'overwrite' | 'preserve')} PathOperation
87
+ *
88
+ * - `set`: Set a value at the specified path (creates if doesn't exist)
89
+ * - `merge`: Merge with existing value (for objects/arrays)
90
+ * - `overwrite`: Replace existing value completely
91
+ * - `preserve`: Keep existing value, ignore template value
92
+ */
93
+ export type PathOperation = 'set' | 'merge' | 'overwrite' | 'preserve'
94
+
95
+ /**
96
+ * Supported value types for path operations
97
+ *
98
+ * @typedef {(Node | object | string | number | boolean | any[])} PathValue
99
+ *
100
+ * Supports YAML nodes, JavaScript objects, primitives, and arrays
101
+ */
102
+ export type PathValue = Node | object | string | number | boolean | any[]
103
+
104
+ /**
105
+ * Configuration for a single path operation
106
+ *
107
+ * @interface PathOperationConfig
108
+ * @property {string} path - Dot-notation path to target (e.g., 'jobs.changes.steps')
109
+ * @property {PathOperation} operation - Type of operation to perform
110
+ * @property {PathValue} value - Value to set/merge/overwrite
111
+ * @property {boolean} [required=true] - Whether the path must exist
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const config: PathOperationConfig = {
116
+ * path: 'on.workflow_call.inputs.version',
117
+ * operation: 'set',
118
+ * value: {
119
+ * description: 'The version to deploy',
120
+ * required: false,
121
+ * type: 'string'
122
+ * },
123
+ * required: true
124
+ * }
125
+ * ```
126
+ */
127
+ export interface PathOperationConfig {
128
+ path: string
129
+ operation: PathOperation
130
+ value: PathValue
131
+ required?: boolean
132
+ commentBefore?: string
133
+ comment?: string
134
+ spaceBefore?: boolean
135
+ spaceBeforeComment?: boolean
136
+ tag?: string
137
+ }
138
+
139
+ /**
140
+ * Set a value at a specific path in the YAML AST
141
+ *
142
+ * Creates intermediate nodes as needed and sets the final value at the specified path.
143
+ * This is the core function for setting values in the YAML structure.
144
+ *
145
+ * @param {YAMLMap} doc - The YAML document to modify
146
+ * @param {string} path - Dot-notation path (e.g., 'jobs.changes.steps')
147
+ * @param {PathValue} value - Value to set at the path
148
+ *
149
+ * @throws {Error} When path navigation fails or parent is not a map
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * const doc = parseDocument('name: Pipeline')
154
+ * setPathValue(doc.contents, 'jobs.changes.runs-on', 'ubuntu-latest')
155
+ * // Results in: jobs: { changes: { 'runs-on': 'ubuntu-latest' } }
156
+ * ```
157
+ */
158
+ export function setPathValue(doc: YAMLMap, path: string, value: PathValue, document?: any, commentBefore?: string, spaceBeforeComment?: boolean): void {
159
+ const pathParts = path.split('.')
160
+ let current: Node = doc
161
+
162
+ // Navigate to the parent of the target
163
+ for (let i = 0; i < pathParts.length - 1; i++) {
164
+ const part = pathParts[i]
165
+
166
+ if (current instanceof YAMLMap) {
167
+ let next: any = current.get(part)
168
+ if (!next) {
169
+ // Create missing intermediate nodes
170
+ next = new YAMLMap()
171
+ current.set(part, next)
172
+ }
173
+ current = next
174
+ } else {
175
+ throw new Error(`Cannot navigate to ${part} - parent is not a map`)
176
+ }
177
+ }
178
+
179
+ // Set the final value
180
+ const finalKey = pathParts[pathParts.length - 1]
181
+ if (current instanceof YAMLMap) {
182
+ let node: any
183
+
184
+ // Check if value is already a node (from createValueFromString, etc.)
185
+ if (value && typeof value === 'object' && ('items' in value || 'type' in value)) {
186
+ // Value is already a node, use it directly
187
+ node = value
188
+ } else {
189
+ // Value is not a node, create one
190
+ node = document && document.createNode ? document.createNode(value) : createNode(value)
191
+ }
192
+
193
+ // When commentBefore is provided, or when setting a job key (which may need comments added later),
194
+ // we need to create a Scalar key instead of a string key.
195
+ // This is because YAML's set() method creates a plain string key by default,
196
+ // but comments can only be attached to Scalar objects.
197
+ const isJobKey = pathParts.length === 2 && pathParts[0] === 'jobs'
198
+
199
+ if (commentBefore || isJobKey) {
200
+ // Create a Scalar key (with or without comment)
201
+ const scalarKey = new Scalar(finalKey)
202
+
203
+ if (commentBefore) {
204
+ ;(scalarKey as any).commentBefore = commentBefore
205
+ }
206
+
207
+ // Add blank line before the comment if requested
208
+ if (spaceBeforeComment) {
209
+ ;(scalarKey as any).spaceBefore = true
210
+ }
211
+
212
+ // Add the pair to the map with a Scalar key
213
+ current.add({ key: scalarKey, value: node })
214
+ } else {
215
+ // Normal case: use the simple set() method which creates a string key
216
+ current.set(finalKey, node)
217
+ }
218
+ } else {
219
+ throw new Error(`Cannot set ${finalKey} - parent is not a map`)
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Get a value at a specific path in the YAML AST
225
+ *
226
+ * Navigates to the specified path and returns the node if found, null otherwise.
227
+ * This is useful for checking if a path exists before applying operations.
228
+ *
229
+ * @param {YAMLMap} doc - The YAML document to read from
230
+ * @param {string} path - Dot-notation path (e.g., 'jobs.changes.steps')
231
+ * @returns {Node | null} The node at the path, or null if not found
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * const doc = parseDocument('jobs: { changes: { runs-on: ubuntu-latest } }')
236
+ * const value = getPathValue(doc.contents, 'jobs.changes.runs-on')
237
+ * console.log(value) // Scalar('ubuntu-latest')
238
+ * ```
239
+ */
240
+ export function getPathValue(doc: YAMLMap, path: string): Node | null {
241
+ const pathParts = path.split('.')
242
+ let current: Node = doc
243
+
244
+ for (const part of pathParts) {
245
+ if (current instanceof YAMLMap) {
246
+ current = current.get(part) as Node
247
+ if (!current) return null
248
+ } else {
249
+ return null
250
+ }
251
+ }
252
+
253
+ return current
254
+ }
255
+
256
+ /**
257
+ * Ensure a path exists and apply the specified operation
258
+ *
259
+ * This is the main orchestration function that handles all path operations.
260
+ * It checks if the path exists, applies the appropriate operation based on
261
+ * the configuration, and handles required vs optional paths.
262
+ *
263
+ * @param {YAMLMap} doc - The YAML document to modify
264
+ * @param {PathOperationConfig} config - Operation configuration
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * const config: PathOperationConfig = {
269
+ * path: 'jobs.changes.runs-on',
270
+ * operation: 'set',
271
+ * value: 'ubuntu-latest',
272
+ * required: true
273
+ * }
274
+ *
275
+ * ensurePathAndApply(doc, config)
276
+ * ```
277
+ */
278
+ export function ensurePathAndApply(
279
+ doc: YAMLMap,
280
+ config: PathOperationConfig,
281
+ document?: any
282
+ ): void {
283
+ const { path, operation, value, required = true, commentBefore, spaceBeforeComment } = config
284
+
285
+ // Check if path exists
286
+ const existingValue = getPathValue(doc, path)
287
+
288
+ if (!existingValue && !required) {
289
+ // Path doesn't exist and not required - skip
290
+ return
291
+ }
292
+
293
+ // Apply operation based on whether path exists or not
294
+ // For required paths that don't exist, we still respect the operation type
295
+ switch (operation) {
296
+ case 'set':
297
+ setPathValue(doc, path, value, document, commentBefore, spaceBeforeComment)
298
+ break
299
+
300
+ case 'merge':
301
+ // Merge will handle non-existent paths by creating them
302
+ mergePathValue(doc, path, value, document)
303
+ break
304
+
305
+ case 'overwrite':
306
+ setPathValue(doc, path, value, document, commentBefore, spaceBeforeComment)
307
+ break
308
+
309
+ case 'preserve':
310
+ // Only preserve if path exists, otherwise create it
311
+ if (!existingValue) {
312
+ setPathValue(doc, path, value, document, commentBefore, spaceBeforeComment)
313
+ }
314
+ // If exists, do nothing - keep existing value
315
+ break
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Merge a value at a specific path (for objects/arrays)
321
+ *
322
+ * Intelligently merges values based on their type:
323
+ * - Objects: Merges key-value pairs, preserving existing keys
324
+ * - Arrays: Adds new items that don't already exist
325
+ * - Other types: Falls back to overwrite behavior
326
+ *
327
+ * @param {YAMLMap} doc - The YAML document to modify
328
+ * @param {string} path - Dot-notation path to merge at
329
+ * @param {PathValue} value - Value to merge
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * // Merge object properties
334
+ * mergePathValue(doc, 'on.workflow_call.inputs', {
335
+ * version: { description: 'Version to deploy' },
336
+ * environment: { description: 'Environment to deploy to' }
337
+ * })
338
+ *
339
+ * // Merge array items
340
+ * mergePathValue(doc, 'on.pull_request.branches', ['feature-branch'])
341
+ * ```
342
+ */
343
+ function mergePathValue(doc: YAMLMap, path: string, value: PathValue, document?: any): void {
344
+ const existingValue = getPathValue(doc, path)
345
+
346
+ if (!existingValue) {
347
+ setPathValue(doc, path, value, document)
348
+ return
349
+ }
350
+
351
+ // Merge logic based on type
352
+ if (existingValue instanceof YAMLMap && typeof value === 'object') {
353
+ // Merge objects
354
+ const newMap = (document && document.createNode ? document.createNode(value) : createNode(value)) as YAMLMap
355
+ for (const pair of newMap.items) {
356
+ const key = pair.key
357
+ const val = pair.value
358
+ existingValue.set(key, val)
359
+ }
360
+ } else if (existingValue instanceof YAMLSeq && Array.isArray(value)) {
361
+ // Merge arrays - add new items that don't exist
362
+ const newSeq = (document && document.createNode ? document.createNode(value) : createNode(value)) as YAMLSeq
363
+ for (const item of newSeq.items) {
364
+ if (!existingValue.items.some(existing =>
365
+ stringify(existing) === stringify(item)
366
+ )) {
367
+ existingValue.items.push(item)
368
+ }
369
+ }
370
+ } else {
371
+ // Fallback to overwrite
372
+ setPathValue(doc, path, value, document)
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Create a YAML node from a JavaScript value, YAML node, or parsed document
378
+ *
379
+ * This is the core value conversion function that handles all supported value types.
380
+ * It intelligently converts JavaScript values to appropriate YAML nodes.
381
+ *
382
+ * @param {PathValue} value - Value to convert to YAML node
383
+ * @returns {Node} The converted YAML node
384
+ *
385
+ * @example
386
+ * ```typescript
387
+ * // Convert object
388
+ * const node = createNode({ key: 'value' })
389
+ *
390
+ * // Convert array
391
+ * const node = createNode(['item1', 'item2'])
392
+ *
393
+ * // Convert string
394
+ * const node = createNode('simple string')
395
+ *
396
+ * // Convert YAML string
397
+ * const node = createNode(createValueFromString(`
398
+ * runs-on: ubuntu-latest
399
+ * steps:
400
+ * - name: Example step
401
+ * run: echo "Hello"
402
+ * `))
403
+ * ```
404
+ */
405
+ function createNode(value: PathValue): Node {
406
+ // If it's already a YAML node, return it
407
+ if (value && typeof value === 'object' && 'type' in value) {
408
+ return value as Node
409
+ }
410
+
411
+ // If it's a parsed document, extract the contents
412
+ if (value && typeof value === 'object' && 'contents' in value) {
413
+ const contents = (value as any).contents
414
+ // If contents is a single item, return it directly
415
+ if (contents && typeof contents === 'object' && 'type' in contents) {
416
+ return contents
417
+ }
418
+ return contents
419
+ }
420
+
421
+ // Handle primitive types
422
+ if (typeof value === 'string') {
423
+ return new Scalar(value)
424
+ } else if (typeof value === 'number') {
425
+ return new Scalar(value)
426
+ } else if (typeof value === 'boolean') {
427
+ return new Scalar(value)
428
+ } else if (Array.isArray(value)) {
429
+ const seq = new YAMLSeq()
430
+ for (const item of value) {
431
+ seq.items.push(createNode(item))
432
+ }
433
+ return seq
434
+ } else if (typeof value === 'object' && value !== null) {
435
+ const map = new YAMLMap()
436
+ for (const [key, val] of Object.entries(value)) {
437
+ map.set(key, createNode(val))
438
+ }
439
+ return map
440
+ } else {
441
+ return new Scalar(value)
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Apply multiple path operations to a document
447
+ *
448
+ * This is the main entry point for applying multiple operations to a YAML document.
449
+ * It processes all operations in order and applies them to the document.
450
+ *
451
+ * @param {YAMLMap} doc - The YAML document to modify
452
+ * @param {PathOperationConfig[]} operations - Array of operations to apply
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * const operations: PathOperationConfig[] = [
457
+ * {
458
+ * path: 'on.workflow_call.inputs.version',
459
+ * operation: 'set',
460
+ * value: { description: 'Version to deploy', required: false, type: 'string' }
461
+ * },
462
+ * {
463
+ * path: 'jobs.changes',
464
+ * operation: 'overwrite',
465
+ * value: createValueFromString(`
466
+ * runs-on: ubuntu-latest
467
+ * steps:
468
+ * - uses: ./.github/actions/detect-changes
469
+ * `)
470
+ * }
471
+ * ]
472
+ *
473
+ * applyPathOperations(doc, operations)
474
+ * ```
475
+ */
476
+ export function applyPathOperations(
477
+ doc: YAMLMap,
478
+ operations: PathOperationConfig[],
479
+ document?: any
480
+ ): void {
481
+ for (const operation of operations) {
482
+ ensurePathAndApply(doc, operation, document)
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Helper functions for creating values
488
+ *
489
+ * These convenience functions make it easier to create YAML nodes from different
490
+ * value types. They handle the parsing and conversion automatically.
491
+ */
492
+
493
+ /**
494
+ * Create a YAML node from a YAML string
495
+ *
496
+ * Parses a YAML string and returns the root node. This is useful for complex
497
+ * multi-line YAML structures like job definitions.
498
+ *
499
+ * @param {string} yamlString - YAML string to parse
500
+ * @returns {Node} The parsed YAML node
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * const node = createValueFromString(`
505
+ * runs-on: ubuntu-latest
506
+ * steps:
507
+ * - name: Checkout code
508
+ * uses: actions/checkout@v3
509
+ * - name: Run tests
510
+ * run: npm test
511
+ * `)
512
+ * ```
513
+ */
514
+ export function createValueFromString(yamlString: string, context?: any, document?: any): Node {
515
+ // Evaluate JavaScript template literals in the string using the provided context
516
+ // Use a more sophisticated approach that handles nested braces
517
+ let processedString = yamlString
518
+ let match
519
+ const regex = /\$\{([^{}]+)\}/g
520
+
521
+ while ((match = regex.exec(processedString)) !== null) {
522
+ const [fullMatch, expression] = match
523
+ try {
524
+ // Create a function that evaluates the expression with the context
525
+ const func = new Function('ctx', `return ${expression}`)
526
+ const result = func(context || {})
527
+ processedString = processedString.replace(fullMatch, JSON.stringify(result))
528
+ // Reset regex lastIndex to avoid issues with string replacement
529
+ regex.lastIndex = 0
530
+ } catch (error) {
531
+ // If evaluation fails, return the original expression as a string
532
+ processedString = processedString.replace(fullMatch, `"${expression}"`)
533
+ regex.lastIndex = 0
534
+ }
535
+ }
536
+
537
+ // Parse the YAML string and return the root content as a proper Node
538
+ const doc = parseDocument(processedString)
539
+ return doc.contents as Node
540
+ }
541
+
542
+ /**
543
+ * Create a YAML node from a JavaScript object
544
+ *
545
+ * Converts a plain JavaScript object to a YAML map node.
546
+ *
547
+ * @param {object} obj - JavaScript object to convert
548
+ * @returns {Node} The converted YAML map node
549
+ *
550
+ * @example
551
+ * ```typescript
552
+ * const node = createValueFromObject({
553
+ * description: 'The version to deploy',
554
+ * required: false,
555
+ * type: 'string'
556
+ * })
557
+ * ```
558
+ */
559
+ export function createValueFromObject(obj: object, doc?: any): Node {
560
+ if (doc && doc.createNode) {
561
+ return doc.createNode(obj)
562
+ }
563
+ return createNode(obj)
564
+ }
565
+
566
+ /**
567
+ * Create a YAML node from a JavaScript array
568
+ *
569
+ * Converts a JavaScript array to a YAML sequence node.
570
+ *
571
+ * @param {any[]} arr - JavaScript array to convert
572
+ * @returns {Node} The converted YAML sequence node
573
+ *
574
+ * @example
575
+ * ```typescript
576
+ * const node = createValueFromArray(['develop', 'staging', 'main'])
577
+ * ```
578
+ */
579
+ export function createValueFromArray(arr: any[]): Node {
580
+ return createNode(arr)
581
+ }
@@ -0,0 +1,64 @@
1
+ import { cosmiconfigSync } from 'cosmiconfig'
2
+ import { PipecraftConfig, DomainConfig } from '../types/index.js'
3
+
4
+ export const loadConfig = (configPath?: string) => {
5
+ const explorer = cosmiconfigSync('pipecraft')
6
+ const result = configPath ? explorer.load(configPath) : explorer.search()
7
+
8
+ if (!result) {
9
+ throw new Error(`No configuration file found. Expected: ${configPath || '.pipecraftrc.json'}`)
10
+ }
11
+
12
+ return result.config
13
+ }
14
+
15
+ export const validateConfig = (config: any) => {
16
+ const requiredFields = ['ciProvider', 'mergeStrategy', 'requireConventionalCommits', 'initialBranch', 'finalBranch', 'branchFlow', 'domains']
17
+
18
+ for (const field of requiredFields) {
19
+ if (!(field in config)) {
20
+ throw new Error(`Missing required field: ${field}`)
21
+ }
22
+ }
23
+
24
+ if (!['github', 'gitlab'].includes(config.ciProvider)) {
25
+ throw new Error('ciProvider must be either "github" or "gitlab"')
26
+ }
27
+
28
+ if (!['fast-forward', 'merge'].includes(config.mergeStrategy)) {
29
+ throw new Error('mergeStrategy must be either "fast-forward" or "merge"')
30
+ }
31
+
32
+ if (!Array.isArray(config.branchFlow) || config.branchFlow.length < 2) {
33
+ throw new Error('branchFlow must be an array with at least 2 branches')
34
+ }
35
+
36
+ if (typeof config.domains !== 'object') {
37
+ throw new Error('domains must be an object')
38
+ }
39
+
40
+ for (const [domainName, domainConfig] of Object.entries(config.domains) as [string, DomainConfig][]) {
41
+ if (!domainConfig || typeof domainConfig !== 'object') {
42
+ throw new Error(`Domain "${domainName}" must be an object`)
43
+ }
44
+
45
+ if (!domainConfig.paths || !Array.isArray(domainConfig.paths)) {
46
+ throw new Error(`Domain "${domainName}" must have a "paths" array`)
47
+ }
48
+
49
+ if (domainConfig.paths.length === 0) {
50
+ throw new Error(`Domain "${domainName}" must have at least one path pattern`)
51
+ }
52
+
53
+ // Set defaults for optional properties
54
+ if (domainConfig.testable === undefined) {
55
+ domainConfig.testable = true
56
+ }
57
+ if (domainConfig.deployable === undefined) {
58
+ domainConfig.deployable = true
59
+ }
60
+ }
61
+
62
+
63
+ return true
64
+ }