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,558 @@
1
+ import { execSync } from 'child_process'
2
+ import { prompt } from '@featherscloud/pinion'
3
+ import { loadConfig } from './config.js'
4
+ import { PipecraftConfig } from '../types/index.js'
5
+
6
+ interface WorkflowPermissions {
7
+ default_workflow_permissions: 'read' | 'write'
8
+ can_approve_pull_request_reviews: boolean
9
+ }
10
+
11
+ interface RepositoryInfo {
12
+ owner: string
13
+ repo: string
14
+ remote: string
15
+ }
16
+
17
+ interface BranchProtectionRules {
18
+ required_status_checks: null | {
19
+ strict: boolean
20
+ contexts: string[]
21
+ }
22
+ enforce_admins: boolean
23
+ required_pull_request_reviews: null | {
24
+ dismiss_stale_reviews: boolean
25
+ require_code_owner_reviews: boolean
26
+ required_approving_review_count: number
27
+ }
28
+ restrictions: null
29
+ allow_force_pushes: boolean
30
+ allow_deletions: boolean
31
+ required_linear_history: boolean
32
+ required_conversation_resolution: boolean
33
+ }
34
+
35
+ /**
36
+ * Get repository information from git remote
37
+ */
38
+ export function getRepositoryInfo(): RepositoryInfo {
39
+ try {
40
+ // Get the remote URL
41
+ const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim()
42
+
43
+ // Parse GitHub URL
44
+ // Supports: https://github.com/owner/repo.git, git@github.com:owner/repo.git
45
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)(\.git)?$/)
46
+
47
+ if (!match) {
48
+ throw new Error('Could not parse GitHub repository URL from git remote')
49
+ }
50
+
51
+ return {
52
+ owner: match[1],
53
+ repo: match[2],
54
+ remote: remoteUrl
55
+ }
56
+ } catch (error: any) {
57
+ throw new Error(`Failed to get repository info: ${error.message}`)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get GitHub token from environment or gh CLI
63
+ */
64
+ export function getGitHubToken(): string {
65
+ // Check environment variables
66
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN
67
+
68
+ if (token) {
69
+ return token
70
+ }
71
+
72
+ // Try to get from gh CLI
73
+ try {
74
+ const ghToken = execSync('gh auth token', { encoding: 'utf8', stdio: 'pipe' }).trim()
75
+ if (ghToken) {
76
+ return ghToken
77
+ }
78
+ } catch (error) {
79
+ // gh CLI not available or not authenticated
80
+ }
81
+
82
+ throw new Error(
83
+ 'GitHub token not found. Please:\n' +
84
+ ' 1. Set GITHUB_TOKEN or GH_TOKEN environment variable, or\n' +
85
+ ' 2. Authenticate with gh CLI: gh auth login'
86
+ )
87
+ }
88
+
89
+ /**
90
+ * Get current workflow permissions
91
+ */
92
+ export async function getWorkflowPermissions(
93
+ owner: string,
94
+ repo: string,
95
+ token: string
96
+ ): Promise<WorkflowPermissions> {
97
+ const response = await fetch(
98
+ `https://api.github.com/repos/${owner}/${repo}/actions/permissions/workflow`,
99
+ {
100
+ headers: {
101
+ 'Authorization': `Bearer ${token}`,
102
+ 'Accept': 'application/vnd.github+json',
103
+ 'X-GitHub-Api-Version': '2022-11-28'
104
+ }
105
+ }
106
+ )
107
+
108
+ if (!response.ok) {
109
+ const error = await response.text()
110
+ throw new Error(`Failed to get workflow permissions: ${response.status} ${error}`)
111
+ }
112
+
113
+ return response.json()
114
+ }
115
+
116
+ /**
117
+ * Update workflow permissions
118
+ */
119
+ export async function updateWorkflowPermissions(
120
+ owner: string,
121
+ repo: string,
122
+ token: string,
123
+ permissions: Partial<WorkflowPermissions>
124
+ ): Promise<void> {
125
+ const response = await fetch(
126
+ `https://api.github.com/repos/${owner}/${repo}/actions/permissions/workflow`,
127
+ {
128
+ method: 'PUT',
129
+ headers: {
130
+ 'Authorization': `Bearer ${token}`,
131
+ 'Accept': 'application/vnd.github+json',
132
+ 'X-GitHub-Api-Version': '2022-11-28',
133
+ 'Content-Type': 'application/json'
134
+ },
135
+ body: JSON.stringify(permissions)
136
+ }
137
+ )
138
+
139
+ if (!response.ok) {
140
+ const error = await response.text()
141
+ throw new Error(`Failed to update workflow permissions: ${response.status} ${error}`)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Determine required permission changes without prompting
147
+ * Returns: changes object if changes needed, null if already correct
148
+ */
149
+ export function getRequiredPermissionChanges(
150
+ currentPermissions: WorkflowPermissions
151
+ ): Partial<WorkflowPermissions> | null {
152
+ // Check if permissions are already correct
153
+ if (
154
+ currentPermissions.default_workflow_permissions === 'write' &&
155
+ currentPermissions.can_approve_pull_request_reviews === true
156
+ ) {
157
+ return null
158
+ }
159
+
160
+ const changes: Partial<WorkflowPermissions> = {}
161
+
162
+ if (currentPermissions.default_workflow_permissions !== 'write') {
163
+ changes.default_workflow_permissions = 'write'
164
+ }
165
+
166
+ if (!currentPermissions.can_approve_pull_request_reviews) {
167
+ changes.can_approve_pull_request_reviews = true
168
+ }
169
+
170
+ return Object.keys(changes).length > 0 ? changes : null
171
+ }
172
+
173
+ /**
174
+ * Display current permissions and prompt for changes
175
+ * Returns: changes object if user accepted changes, 'declined' if user declined, null if already correct
176
+ */
177
+ export async function promptPermissionChanges(
178
+ currentPermissions: WorkflowPermissions
179
+ ): Promise<Partial<WorkflowPermissions> | null | 'declined'> {
180
+ console.log('\nšŸ“‹ Current GitHub Actions Workflow Permissions:')
181
+ console.log(` Default permissions: ${currentPermissions.default_workflow_permissions}`)
182
+ console.log(` Can create/approve PRs: ${currentPermissions.can_approve_pull_request_reviews ? 'Yes' : 'No'}`)
183
+
184
+ // Check if permissions are already correct
185
+ if (
186
+ currentPermissions.default_workflow_permissions === 'write' &&
187
+ currentPermissions.can_approve_pull_request_reviews === true
188
+ ) {
189
+ console.log('\nāœ… Permissions are already configured correctly for PipeCraft!')
190
+ return null
191
+ }
192
+
193
+ console.log('\nāš ļø PipeCraft requires the following permissions:')
194
+ console.log(' • Default permissions: write (for creating tags and pushing)')
195
+ console.log(' • Can create/approve PRs: Yes (for automated PR creation)')
196
+
197
+ const changes: Partial<WorkflowPermissions> = {}
198
+ let userDeclinedAnyChanges = false
199
+
200
+ // Ask about default permissions
201
+ if (currentPermissions.default_workflow_permissions !== 'write') {
202
+ const response: any = await prompt({
203
+ type: 'confirm',
204
+ name: 'updatePermissions',
205
+ message: 'Change default workflow permissions from "read" to "write"?',
206
+ default: true
207
+ } as any)
208
+
209
+ if (response.updatePermissions) {
210
+ changes.default_workflow_permissions = 'write'
211
+ } else {
212
+ console.log('āš ļø Warning: PipeCraft may not work correctly without write permissions')
213
+ userDeclinedAnyChanges = true
214
+ }
215
+ }
216
+
217
+ // Ask about PR creation
218
+ if (!currentPermissions.can_approve_pull_request_reviews) {
219
+ const response: any = await prompt({
220
+ type: 'confirm',
221
+ name: 'allowPRs',
222
+ message: 'Allow GitHub Actions to create and approve pull requests?',
223
+ default: true
224
+ } as any)
225
+
226
+ if (response.allowPRs) {
227
+ changes.can_approve_pull_request_reviews = true
228
+ } else {
229
+ console.log('āš ļø Warning: PipeCraft cannot create PRs without this permission')
230
+ userDeclinedAnyChanges = true
231
+ }
232
+ }
233
+
234
+ // If user declined all changes, return 'declined'
235
+ if (userDeclinedAnyChanges && Object.keys(changes).length === 0) {
236
+ return 'declined'
237
+ }
238
+
239
+ return Object.keys(changes).length > 0 ? changes : null
240
+ }
241
+
242
+ /**
243
+ * Get branch protection rules
244
+ */
245
+ export async function getBranchProtection(
246
+ owner: string,
247
+ repo: string,
248
+ branch: string,
249
+ token: string
250
+ ): Promise<BranchProtectionRules | null> {
251
+ const response = await fetch(
252
+ `https://api.github.com/repos/${owner}/${repo}/branches/${branch}/protection`,
253
+ {
254
+ headers: {
255
+ 'Authorization': `Bearer ${token}`,
256
+ 'Accept': 'application/vnd.github+json',
257
+ 'X-GitHub-Api-Version': '2022-11-28'
258
+ }
259
+ }
260
+ )
261
+
262
+ if (response.status === 404) {
263
+ // Branch protection not configured
264
+ return null
265
+ }
266
+
267
+ if (!response.ok) {
268
+ const error = await response.text()
269
+ throw new Error(`Failed to get branch protection for ${branch}: ${response.status} ${error}`)
270
+ }
271
+
272
+ return response.json()
273
+ }
274
+
275
+ /**
276
+ * Update branch protection rules to enable auto-merge
277
+ */
278
+ export async function updateBranchProtection(
279
+ owner: string,
280
+ repo: string,
281
+ branch: string,
282
+ token: string
283
+ ): Promise<void> {
284
+ // Minimal branch protection to enable auto-merge
285
+ // GitHub requires at least ONE of the main protection types
286
+ const protection: BranchProtectionRules = {
287
+ required_status_checks: {
288
+ strict: false,
289
+ contexts: [] // No specific checks required, but enables the feature
290
+ },
291
+ enforce_admins: false,
292
+ required_pull_request_reviews: null, // No required reviews for auto-merge branches
293
+ restrictions: null,
294
+ allow_force_pushes: false,
295
+ allow_deletions: false,
296
+ required_linear_history: true,
297
+ required_conversation_resolution: false
298
+ }
299
+
300
+ const response = await fetch(
301
+ `https://api.github.com/repos/${owner}/${repo}/branches/${branch}/protection`,
302
+ {
303
+ method: 'PUT',
304
+ headers: {
305
+ 'Authorization': `Bearer ${token}`,
306
+ 'Accept': 'application/vnd.github+json',
307
+ 'X-GitHub-Api-Version': '2022-11-28',
308
+ 'Content-Type': 'application/json'
309
+ },
310
+ body: JSON.stringify(protection)
311
+ }
312
+ )
313
+
314
+ if (!response.ok) {
315
+ const error = await response.text()
316
+ throw new Error(`Failed to update branch protection for ${branch}: ${response.status} ${error}`)
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Enable auto-merge feature for the repository
322
+ */
323
+ export async function enableAutoMerge(
324
+ owner: string,
325
+ repo: string,
326
+ token: string
327
+ ): Promise<boolean> {
328
+ // First check if auto-merge is already enabled
329
+ const checkResponse = await fetch(
330
+ `https://api.github.com/repos/${owner}/${repo}`,
331
+ {
332
+ headers: {
333
+ 'Authorization': `Bearer ${token}`,
334
+ 'Accept': 'application/vnd.github+json',
335
+ 'X-GitHub-Api-Version': '2022-11-28'
336
+ }
337
+ }
338
+ )
339
+
340
+ if (checkResponse.ok) {
341
+ const repoData = await checkResponse.json()
342
+ if (repoData.allow_auto_merge === true) {
343
+ return false // Already enabled
344
+ }
345
+ }
346
+
347
+ // Enable auto-merge
348
+ const response = await fetch(
349
+ `https://api.github.com/repos/${owner}/${repo}`,
350
+ {
351
+ method: 'PATCH',
352
+ headers: {
353
+ 'Authorization': `Bearer ${token}`,
354
+ 'Accept': 'application/vnd.github+json',
355
+ 'X-GitHub-Api-Version': '2022-11-28',
356
+ 'Content-Type': 'application/json'
357
+ },
358
+ body: JSON.stringify({
359
+ allow_auto_merge: true
360
+ })
361
+ }
362
+ )
363
+
364
+ if (!response.ok) {
365
+ const error = await response.text()
366
+ throw new Error(`Failed to enable auto-merge: ${response.status} ${error}`)
367
+ }
368
+
369
+ return true // Was enabled
370
+ }
371
+
372
+ /**
373
+ * Configure branch protection for branches that need auto-merge
374
+ */
375
+ export async function configureBranchProtection(
376
+ repoInfo: RepositoryInfo,
377
+ token: string,
378
+ autoApply: boolean
379
+ ): Promise<void> {
380
+ console.log('\nšŸ” Checking auto-merge configuration...')
381
+
382
+ // Load config to get autoMerge settings
383
+ let config: PipecraftConfig
384
+ try {
385
+ config = loadConfig('.pipecraftrc.json') as PipecraftConfig
386
+ } catch (error) {
387
+ console.log('āš ļø Could not load .pipecraftrc.json - skipping auto-merge setup')
388
+ return
389
+ }
390
+
391
+ if (!config.autoMerge || !config.branchFlow) {
392
+ console.log('ā„¹ļø No autoMerge configuration found - skipping auto-merge setup')
393
+ return
394
+ }
395
+
396
+ // Enable repository-level auto-merge feature
397
+ try {
398
+ const wasEnabled = await enableAutoMerge(repoInfo.owner, repoInfo.repo, token)
399
+ if (wasEnabled) {
400
+ console.log('āœ… Enabled auto-merge for repository')
401
+ } else {
402
+ console.log('āœ… Auto-merge already enabled for repository')
403
+ }
404
+ } catch (error: any) {
405
+ console.error(`āš ļø Could not enable auto-merge: ${error.message}`)
406
+ console.log(' You can enable it manually at:')
407
+ console.log(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings`)
408
+ return
409
+ }
410
+
411
+ // Determine which branches need auto-merge
412
+ const autoMergeConfig = config.autoMerge
413
+ const branchesNeedingProtection: string[] = []
414
+
415
+ if (typeof autoMergeConfig === 'boolean') {
416
+ // If true for all, protect all intermediate branches
417
+ if (autoMergeConfig && config.branchFlow.length > 1) {
418
+ branchesNeedingProtection.push(...config.branchFlow.slice(1, -1))
419
+ }
420
+ } else if (typeof autoMergeConfig === 'object') {
421
+ // Check which branches have autoMerge enabled
422
+ for (const [branch, enabled] of Object.entries(autoMergeConfig)) {
423
+ if (enabled === true) {
424
+ branchesNeedingProtection.push(branch)
425
+ }
426
+ }
427
+ }
428
+
429
+ if (branchesNeedingProtection.length === 0) {
430
+ console.log('ā„¹ļø No branches configured for auto-merge - skipping branch protection setup')
431
+ return
432
+ }
433
+
434
+ console.log(`šŸ“‹ Branches with auto-merge enabled: ${branchesNeedingProtection.join(', ')}`)
435
+
436
+ // Check each branch
437
+ for (const branch of branchesNeedingProtection) {
438
+ try {
439
+ const protection = await getBranchProtection(repoInfo.owner, repoInfo.repo, branch, token)
440
+
441
+ if (protection === null) {
442
+ // Branch protection not configured
443
+ if (autoApply) {
444
+ console.log(`šŸ”§ Configuring branch protection for ${branch}...`)
445
+ await updateBranchProtection(repoInfo.owner, repoInfo.repo, branch, token)
446
+ console.log(`āœ… Branch protection enabled for ${branch}`)
447
+ } else {
448
+ const response: any = await prompt({
449
+ type: 'confirm',
450
+ name: 'enableProtection',
451
+ message: `Enable branch protection for '${branch}' to support auto-merge?`,
452
+ default: true
453
+ } as any)
454
+
455
+ if (response.enableProtection) {
456
+ console.log(`šŸ”§ Configuring branch protection for ${branch}...`)
457
+ await updateBranchProtection(repoInfo.owner, repoInfo.repo, branch, token)
458
+ console.log(`āœ… Branch protection enabled for ${branch}`)
459
+ } else {
460
+ console.log(`āš ļø Skipped ${branch} - auto-merge will not work without branch protection`)
461
+ }
462
+ }
463
+ } else {
464
+ console.log(`āœ… Branch protection already configured for ${branch}`)
465
+ }
466
+ } catch (error: any) {
467
+ console.error(`āš ļø Could not configure protection for ${branch}: ${error.message}`)
468
+ }
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Main setup function
474
+ */
475
+ export async function setupGitHubPermissions(autoApply: boolean = false): Promise<void> {
476
+ console.log('šŸ” Checking GitHub repository configuration...\n')
477
+
478
+ // Get repository info
479
+ const repoInfo = getRepositoryInfo()
480
+ console.log(`šŸ“¦ Repository: ${repoInfo.owner}/${repoInfo.repo}`)
481
+
482
+ // Get GitHub token
483
+ let token: string
484
+ try {
485
+ token = getGitHubToken()
486
+ console.log('āœ… GitHub token found')
487
+ } catch (error: any) {
488
+ console.error(`\nāŒ ${error.message}`)
489
+ process.exit(1)
490
+ }
491
+
492
+ // Get current permissions
493
+ console.log('šŸ” Fetching current workflow permissions...')
494
+ const currentPermissions = await getWorkflowPermissions(
495
+ repoInfo.owner,
496
+ repoInfo.repo,
497
+ token
498
+ )
499
+
500
+ let changes: Partial<WorkflowPermissions> | null | 'declined'
501
+ let permissionsAlreadyCorrect = false
502
+
503
+ if (autoApply) {
504
+ // Auto-apply mode: determine changes without prompting
505
+ changes = getRequiredPermissionChanges(currentPermissions)
506
+
507
+ if (changes === null) {
508
+ console.log('\nāœ… Workflow permissions are already configured correctly!')
509
+ permissionsAlreadyCorrect = true
510
+ } else {
511
+ // Show what will be applied
512
+ console.log('\nšŸ“‹ Current GitHub Actions Workflow Permissions:')
513
+ console.log(` Default permissions: ${currentPermissions.default_workflow_permissions}`)
514
+ console.log(` Can create/approve PRs: ${currentPermissions.can_approve_pull_request_reviews ? 'Yes' : 'No'}`)
515
+ console.log('\nšŸ”§ Applying required changes:')
516
+ if (changes.default_workflow_permissions) {
517
+ console.log(` • Setting default permissions to: ${changes.default_workflow_permissions}`)
518
+ }
519
+ if (changes.can_approve_pull_request_reviews !== undefined) {
520
+ console.log(` • Allowing PR creation/approval: ${changes.can_approve_pull_request_reviews}`)
521
+ }
522
+ }
523
+ } else {
524
+ // Interactive mode: prompt for changes
525
+ changes = await promptPermissionChanges(currentPermissions)
526
+
527
+ if (changes === null) {
528
+ console.log('\nāœ… Workflow permissions are already configured correctly!')
529
+ permissionsAlreadyCorrect = true
530
+ } else if (changes === 'declined') {
531
+ console.log('\nāš ļø Setup incomplete - permissions were not updated')
532
+ console.log('šŸ’” You can run this command again anytime or update permissions manually at:')
533
+ console.log(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`)
534
+ // Still continue to check branch protection
535
+ permissionsAlreadyCorrect = true
536
+ }
537
+ }
538
+
539
+ // Apply permission changes if needed
540
+ if (!permissionsAlreadyCorrect && changes && changes !== 'declined') {
541
+ console.log('\nšŸ”„ Updating repository settings...')
542
+ await updateWorkflowPermissions(
543
+ repoInfo.owner,
544
+ repoInfo.repo,
545
+ token,
546
+ changes
547
+ )
548
+
549
+ console.log('āœ… GitHub Actions permissions updated successfully!')
550
+ }
551
+
552
+ // Configure branch protection for auto-merge
553
+ await configureBranchProtection(repoInfo, token, autoApply)
554
+
555
+ console.log('\n✨ Setup complete!')
556
+ console.log('\nšŸ’” You can verify the changes at:')
557
+ console.log(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`)
558
+ }