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.
- package/.claude/settings.local.json +35 -0
- package/.github/actions/calculate-version/action.yml +106 -0
- package/.github/actions/create-pr/action.yml +122 -0
- package/.github/actions/create-release/action.yml +74 -0
- package/.github/actions/create-tag/action.yml +94 -0
- package/.github/actions/detect-changes/action.yml +62 -0
- package/.github/actions/manage-branch/action.yml +113 -0
- package/.github/actions/promote-branch/action.yml +352 -0
- package/.github/example/workflows/job.analyze.code.yml +26 -0
- package/.github/example/workflows/job.analyze.docker.yml +32 -0
- package/.github/example/workflows/job.app.api.deploy.yml +127 -0
- package/.github/example/workflows/job.app.api.test.yml +102 -0
- package/.github/example/workflows/job.app.docs.deploy.yml +40 -0
- package/.github/example/workflows/job.app.docs.test.yml +50 -0
- package/.github/example/workflows/job.app.web.deploy.yml +96 -0
- package/.github/example/workflows/job.app.web.test.yml +49 -0
- package/.github/example/workflows/job.changes.yml +82 -0
- package/.github/example/workflows/job.create-pr.yml +96 -0
- package/.github/example/workflows/job.env-check.yml +43 -0
- package/.github/example/workflows/job.fast-forward.yml +103 -0
- package/.github/example/workflows/job.lint.yml +64 -0
- package/.github/example/workflows/job.pr-name.yml +70 -0
- package/.github/example/workflows/job.pr-squash-name.yml +27 -0
- package/.github/example/workflows/job.tag.yml +38 -0
- package/.github/example/workflows/job.version.yml +99 -0
- package/.github/example/workflows/lib.plugin-base.publish.yml +59 -0
- package/.github/example/workflows/pipe.yml +178 -0
- package/.github/example/workflows/scripts/fastforward.sh +51 -0
- package/.github/example/workflows/scripts/pipe-cleaner.sh +70 -0
- package/.github/scripts/fastforward.sh +51 -0
- package/.github/scripts/pipe-cleaner.sh +70 -0
- package/.github/workflows/pipeline.yml +220 -0
- package/.github/workflows/pr-title-check.yml +70 -0
- package/.github/workflows/publish.yml +39 -0
- package/.pipecraftrc.json +78 -0
- package/.release-it.cjs +71 -0
- package/PIPELINE_TESTING_PLAN.md +499 -0
- package/README.md +1101 -0
- package/TRUNK_FLOW_PLAN.md +401 -0
- package/assets/logo_banner.png +0 -0
- package/assets/logo_banner.webp +0 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +364 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/generators/init.tpl.d.ts +3 -0
- package/dist/generators/init.tpl.d.ts.map +1 -0
- package/dist/generators/init.tpl.js +117 -0
- package/dist/generators/init.tpl.js.map +1 -0
- package/dist/generators/workflows.tpl.d.ts +7 -0
- package/dist/generators/workflows.tpl.d.ts.map +1 -0
- package/dist/generators/workflows.tpl.js +73 -0
- package/dist/generators/workflows.tpl.js.map +1 -0
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/calculate-version.yml.tpl.js +122 -0
- package/dist/templates/actions/calculate-version.yml.tpl.js.map +1 -0
- package/dist/templates/actions/create-pr.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/create-pr.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/create-pr.yml.tpl.js +145 -0
- package/dist/templates/actions/create-pr.yml.tpl.js.map +1 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/create-tag.yml.tpl.js +111 -0
- package/dist/templates/actions/create-tag.yml.tpl.js.map +1 -0
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/detect-changes.yml.tpl.js +79 -0
- package/dist/templates/actions/detect-changes.yml.tpl.js.map +1 -0
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/manage-branch.yml.tpl.js +130 -0
- package/dist/templates/actions/manage-branch.yml.tpl.js.map +1 -0
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/promote-branch.yml.tpl.js +362 -0
- package/dist/templates/actions/promote-branch.yml.tpl.js.map +1 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts +16 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts.map +1 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js +652 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js.map +1 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ast-path-operations.d.ts +287 -0
- package/dist/utils/ast-path-operations.d.ts.map +1 -0
- package/dist/utils/ast-path-operations.js +419 -0
- package/dist/utils/ast-path-operations.js.map +1 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +49 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/github-setup.d.ts +74 -0
- package/dist/utils/github-setup.d.ts.map +1 -0
- package/dist/utils/github-setup.js +418 -0
- package/dist/utils/github-setup.js.map +1 -0
- package/dist/utils/idempotency.d.ts +47 -0
- package/dist/utils/idempotency.d.ts.map +1 -0
- package/dist/utils/idempotency.js +176 -0
- package/dist/utils/idempotency.js.map +1 -0
- package/dist/utils/preflight.d.ts +55 -0
- package/dist/utils/preflight.d.ts.map +1 -0
- package/dist/utils/preflight.js +261 -0
- package/dist/utils/preflight.js.map +1 -0
- package/dist/utils/versioning.d.ts +41 -0
- package/dist/utils/versioning.d.ts.map +1 -0
- package/dist/utils/versioning.js +219 -0
- package/dist/utils/versioning.js.map +1 -0
- package/docs/USER_JOURNEY_ERRORS.md +352 -0
- package/eslint.config.js +77 -0
- package/examples/basic-config.json +41 -0
- package/examples/monorepo-config.json +49 -0
- package/examples/usage.md +289 -0
- package/package.json +58 -0
- package/scripts/debug-workflows.sh +339 -0
- package/scripts/parse-pipeline.js +29 -0
- package/scripts/test-job-order.cjs +140 -0
- package/scripts/validate-pipeline.cjs +379 -0
- package/scripts/verify-job-order.sh +30 -0
- package/src/cli/index.ts +414 -0
- package/src/generators/init.tpl.ts +126 -0
- package/src/generators/workflows.tpl.ts +80 -0
- package/src/templates/actions/calculate-version.yml.tpl.ts +124 -0
- package/src/templates/actions/create-pr.yml.tpl.ts +147 -0
- package/src/templates/actions/create-tag.yml.tpl.ts +112 -0
- package/src/templates/actions/detect-changes.yml.tpl.ts +87 -0
- package/src/templates/actions/manage-branch.yml.tpl.ts +132 -0
- package/src/templates/actions/promote-branch.yml.tpl.ts +364 -0
- package/src/templates/workflows/pipeline-path-based.yml.tpl.ts +706 -0
- package/src/types/index.ts +64 -0
- package/src/utils/README-ast-path-operations.md +390 -0
- package/src/utils/ast-path-operations.ts +581 -0
- package/src/utils/config.ts +64 -0
- package/src/utils/github-setup.ts +558 -0
- package/src/utils/idempotency.ts +215 -0
- package/src/utils/preflight.ts +306 -0
- package/src/utils/versioning.ts +244 -0
- package/tests/README.md +229 -0
- package/tests/TEST_STRUCTURE.md +256 -0
- package/tests/act/run-act-tests.sh +345 -0
- package/tests/debugging/debug-utils.ts +538 -0
- package/tests/debugging/debug-workflow.test.ts +339 -0
- package/tests/debugging/debug-workflows.sh +339 -0
- package/tests/debugging/iterative-debug.ts +652 -0
- package/tests/debugging/run-debug-tests.sh +431 -0
- package/tests/fixtures/basic-config.json +51 -0
- package/tests/fixtures/invalid-config.json +9 -0
- package/tests/fixtures/pipeline-generated.yml +235 -0
- package/tests/fixtures/pipeline-preserve-comments.yml +245 -0
- package/tests/fixtures/pipeline-user-modified.yml +245 -0
- package/tests/fixtures/test-config.json +58 -0
- package/tests/github-live/README.md +250 -0
- package/tests/github-local/JOB_WORKFLOW_TESTS.md +296 -0
- package/tests/github-local/README.md +240 -0
- package/tests/github-local/run-all-tests.sh +422 -0
- package/tests/github-local/test-job-workflows.sh +631 -0
- package/tests/github-local/test-pipeline-workflow.sh +440 -0
- package/tests/integration/generators.test.ts +578 -0
- package/tests/integration/path-based-template.test.ts +510 -0
- package/tests/integration/simple-path-based.test.ts +415 -0
- package/tests/setup.ts +56 -0
- package/tests/unit/ast-path-operations-extended.test.ts +302 -0
- package/tests/unit/cli.test.ts +541 -0
- package/tests/unit/config-extended.test.ts +412 -0
- package/tests/unit/config.test.ts +152 -0
- package/tests/unit/github-setup.test.ts +189 -0
- package/tests/unit/idempotency-isolated.test.ts +297 -0
- package/tests/unit/job-order.test.ts +157 -0
- package/tests/unit/pipeline-path-based.test.ts +511 -0
- package/tests/unit/validate-pipeline.test.ts +384 -0
- package/tests/unit/versioning-extended.test.ts +279 -0
- package/tests/unit/versioning.test.ts +241 -0
- package/tsconfig.debug.json +17 -0
- package/tsconfig.json +28 -0
- 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
|
+
}
|