-
Notifications
You must be signed in to change notification settings - Fork 17
457 lines (413 loc) · 18.6 KB
/
rebase-shears.yml
File metadata and controls
457 lines (413 loc) · 18.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
name: Merging-Rebase Automation
run-name: Rebase ${{ (inputs.branch == '' || inputs.branch == 'all') && 'all shears/* branches' || format('the shears/{} branch', inputs.branch) }}
on:
schedule:
- cron: '0 */6 * * *'
workflow_dispatch:
inputs:
branch:
description: 'Shears branch to update (seen, next, main, maint, or all)'
required: true
type: choice
options:
- all
- seen
- next
- main
- maint
push:
description: 'Push the result after successful rebase'
required: false
type: boolean
default: false
env:
BRANCH: ${{ inputs.branch || 'all' }}
PUSH: ${{ github.event_name == 'schedule' && 'true' || inputs.push || 'true' }}
COPILOT_MODEL: claude-opus-4.6
jobs:
rebase:
if: github.event.repository.owner.login == 'git-for-windows'
runs-on: ubuntu-latest
permissions:
checks: write
steps:
- name: Check if rebase is needed
id: precheck
if: github.event_name == 'schedule'
uses: actions/github-script@v8
with:
script: |
// To avoid expensive clones when nothing changed, each run records
// the ref tips it saw (GfW main + upstream branches) in its check
// run output via the Checks API. On schedule triggers, we compare
// current refs against the previous run's recorded state and skip
// the rebase entirely if nothing moved.
const runs = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: ${{ toJSON(github.event.workflow) }},
per_page: 2
})
const previous = runs.data.workflow_runs.find(r => r.id !== context.runId)
let summary = ''
if (previous) {
const checkRuns = await github.rest.checks.listForSuite({
owner: context.repo.owner,
repo: context.repo.repo,
check_suite_id: previous.check_suite_id
})
summary = checkRuns.data.check_runs[0]?.output?.summary || ''
}
const prevRefs = Object.fromEntries(
summary.split('\n').filter(Boolean).map(l => l.split(' '))
)
const branches = ['main', 'seen', 'next', 'master', 'maint']
const expected = branches.map(b => b === 'main' ? 'origin/main' : `upstream/${b}`)
if (!expected.every(k => prevRefs[k])) return
core.exportVariable('REF_STATE', summary)
for (const branch of branches) {
const owner = branch === 'main' ? '${{ github.repository_owner }}' : 'git'
const key = branch === 'main' ? 'origin/main' : `upstream/${branch}`
const r = await github.rest.git.getRef({ owner, repo: 'git', ref: `heads/${branch}` })
if (r.data.object.sha !== prevRefs[key]) return
}
const originRun = prevRefs['run'] || previous.html_url
core.notice(`Nothing changed since ${originRun}, skipping`)
core.setOutput('skip', 'true')
core.exportVariable('PUSH', 'false')
- name: Checkout automation repo
if: steps.precheck.outputs.skip != 'true'
uses: actions/checkout@v6
with:
path: automation
- name: Set up Copilot CLI
if: steps.precheck.outputs.skip != 'true'
run: npm install -g @github/copilot
- name: Clone git-for-windows/git
if: steps.precheck.outputs.skip != 'true'
uses: actions/checkout@v6
with:
repository: ${{ github.repository_owner }}/git
fetch-depth: 0
path: git
- name: Configure git
if: steps.precheck.outputs.skip != 'true'
working-directory: git
run: |
git config user.name "Git for Windows Build Agent"
git config user.email "ci@git-for-windows.build"
- name: Add upstream remote
id: fetch
if: steps.precheck.outputs.skip != 'true'
working-directory: git
run: |
git remote add upstream https://github.com/git/git.git || true
git fetch upstream --no-tags
echo "main_sha=$(git rev-parse origin/main)" >>"$GITHUB_OUTPUT"
cat >>"$GITHUB_ENV" <<-EOF
REF_STATE<<REFS
$(git for-each-ref --format='%(refname:strip=2) %(objectname)' refs/remotes/origin/main refs/remotes/upstream/seen refs/remotes/upstream/next refs/remotes/upstream/master refs/remotes/upstream/maint)
REFS
EOF
- name: Record ref state
if: env.REF_STATE
uses: actions/github-script@v8
with:
script: |
let refState = process.env.REF_STATE
if (!refState.includes('\nrun '))
refState += `\nrun ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
const jobs = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId
})
const job = jobs.data.jobs.find(j => j.name === '${{ github.job }}')
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: job.id,
output: {
title: 'Ref state',
summary: refState
}
})
- name: Install build dependencies
if: steps.precheck.outputs.skip != 'true'
run: |
sudo apt-get update -q
sudo apt-get install -y -q libcurl4-openssl-dev libexpat-dev gettext zlib1g-dev
- name: Run rebase (single branch)
id: rebase-single
if: env.BRANCH != 'all' && steps.precheck.outputs.skip != 'true'
working-directory: git
# Copilot CLI authenticates via GH_TOKEN, which needs a fine-grained PAT
# with the "Copilot Requests" permission (Account permissions section).
# Create one at https://github.com/settings/personal-access-tokens/new
env:
GH_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
case "$BRANCH" in
main) UPSTREAM="upstream/master" ;;
*) UPSTREAM="upstream/$BRANCH" ;;
esac
echo "::group::Fetching shears branches"
git fetch origin shears/seen shears/next shears/main shears/maint
echo "::endgroup::"
if test 0 = "$(git rev-list --count "origin/shears/$BRANCH..$UPSTREAM")"; then
echo "::notice::Nothing to do for shears/$BRANCH: $UPSTREAM has no new commits"
exit 0
fi
if "$GITHUB_WORKSPACE/automation/rebase-branch.sh" "shears/$BRANCH" "$UPSTREAM" "$GITHUB_WORKSPACE/automation"; then
echo "to_push=shears/$BRANCH" >>"$GITHUB_OUTPUT"
else
echo "failed_worktrees=$PWD/rebase-worktree-$BRANCH" >>"$GITHUB_OUTPUT"
exit 1
fi
- name: Run rebase (all branches)
id: rebase-all
if: env.BRANCH == 'all' && steps.precheck.outputs.skip != 'true'
working-directory: git
env:
GH_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
echo "::group::Fetching shears branches"
git fetch origin shears/seen shears/next shears/main shears/maint
echo "::endgroup::"
to_push=""
failed_worktrees=""
for BRANCH in seen next main maint; do
case "$BRANCH" in
main) UPSTREAM="upstream/master" ;;
*) UPSTREAM="upstream/$BRANCH" ;;
esac
if test 0 = "$(git rev-list --count "origin/shears/$BRANCH..$UPSTREAM")"; then
echo "::notice::Nothing to do for shears/$BRANCH: $UPSTREAM has no new commits"
if test -n "$GITHUB_STEP_SUMMARY"; then
cat >>"$GITHUB_STEP_SUMMARY" <<-UPTODATE_EOF
## Rebase Summary: $BRANCH
Already up to date with \`$UPSTREAM\`; nothing to rebase.
UPTODATE_EOF
fi
continue
fi
if "$GITHUB_WORKSPACE/automation/rebase-branch.sh" "shears/$BRANCH" "$UPSTREAM" "$GITHUB_WORKSPACE/automation"; then
to_push="${to_push:+$to_push }shears/$BRANCH"
else
echo "::error::Rebase failed for shears/$BRANCH"
failed_worktrees="${failed_worktrees:+$failed_worktrees }$PWD/rebase-worktree-$BRANCH"
fi
done
echo "to_push=$to_push" >>"$GITHUB_OUTPUT"
echo "failed_worktrees=$failed_worktrees" >>"$GITHUB_OUTPUT"
# Fail the step if any rebase failed
test -z "$failed_worktrees"
- name: Create bundles and archives
if: always() && steps.precheck.outputs.skip != 'true'
working-directory: git
run: |
set -x
mkdir -p upload
# Bundles for successful branches
for branch in ${{ steps.rebase-single.outputs.to_push }} ${{ steps.rebase-all.outputs.to_push }}; do
name=${branch##*/}
git bundle create "upload/$name.bundle" "$branch" ^origin/main
done
# Bundles and archives for failed branches
for worktree in ${{ steps.rebase-single.outputs.failed_worktrees }} ${{ steps.rebase-all.outputs.failed_worktrees }}; do
test -d "$worktree" || continue
name=${worktree##*rebase-worktree-}
rebase_head_arg=
git -C "$worktree" rev-parse --verify REBASE_HEAD >/dev/null 2>&1 &&
rebase_head_arg=REBASE_HEAD
git -C "$worktree" for-each-ref --format='%(refname)' |
grep -v '^refs/tags/' |
sed 's,^refs/remotes/origin/,^refs/remotes/origin/,' |
git -C "$worktree" bundle create "../upload/$name.bundle" --stdin HEAD $rebase_head_arg
tar -czf "upload/$name.tar.gz" -C "$worktree" .
done
- name: Upload artifacts
if: always() && steps.precheck.outputs.skip != 'true'
uses: actions/upload-artifact@v7
with:
name: rebase-result
path: git/upload/
if-no-files-found: warn
- name: Obtain installation access token
if: always() && env.PUSH == 'true' && steps.precheck.outputs.skip != 'true'
uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: git,shears-builds
- name: Push results
if: always() && env.PUSH == 'true' && steps.app-token.outputs.token && (steps.rebase-single.outputs.to_push || steps.rebase-all.outputs.to_push)
working-directory: git
run: |
AUTH="$(echo -n 'x-access-token:${{ steps.app-token.outputs.token }}' | base64)"
GIT_CONFIG_PARAMETERS="'http.https://github.com/.extraHeader=' 'http.https://github.com/.extraHeader=Authorization: basic $AUTH'" \
git push --force origin ${{ steps.rebase-single.outputs.to_push }} ${{ steps.rebase-all.outputs.to_push }}
- name: Create check runs
if: always() && env.PUSH == 'true' && steps.app-token.outputs.token
working-directory: git
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
head_sha="${{ steps.fetch.outputs.main_sha }}"
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
for branch in ${{ steps.rebase-single.outputs.to_push }} ${{ steps.rebase-all.outputs.to_push }}; do
name=${branch##*/}
report="rebase-worktree-$name/conflict-report.md"
test -f "$report" || continue
gh api "repos/${{ github.repository_owner }}/git/check-runs" \
-f "name=shears-$name" \
-f "head_sha=$head_sha" \
-f "status=completed" \
-f "conclusion=success" \
-f "details_url=$run_url" \
-f "output[title]=shears/$name rebased successfully" \
-F "output[summary]=@$report"
done
for worktree in ${{ steps.rebase-single.outputs.failed_worktrees }} ${{ steps.rebase-all.outputs.failed_worktrees }}; do
test -d "$worktree" || continue
name=${worktree##*rebase-worktree-}
report="$worktree/conflict-report.md"
test -f "$report" || report=/dev/null
gh api "repos/${{ github.repository_owner }}/git/check-runs" \
-f "name=shears-$name" \
-f "head_sha=$head_sha" \
-f "status=completed" \
-f "conclusion=failure" \
-f "details_url=$run_url" \
-f "output[title]=shears/$name rebase failed" \
-F "output[summary]=@$report"
done
- name: Mirror results to PRs
if: always() && env.PUSH == 'true' && steps.app-token.outputs.token
working-directory: git
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
builds_repo="${{ github.repository_owner }}/shears-builds"
run_id="$GITHUB_RUN_ID"
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$run_id"
AUTH="$(echo -n 'x-access-token:${{ steps.app-token.outputs.token }}' | base64)"
errors=0
push_to_builds () {
GIT_CONFIG_PARAMETERS="'http.https://github.com/.extraHeader=' 'http.https://github.com/.extraHeader=Authorization: basic $AUTH'" \
git push "https://github.com/$builds_repo" "$@"
}
# Successful rebases: create regular PRs
for branch in ${{ steps.rebase-single.outputs.to_push }} ${{ steps.rebase-all.outputs.to_push }}; do
name=${branch##*/}
worktree="rebase-worktree-$name"
test -d "$worktree" || continue
marker=$(git -C "$worktree" rev-parse "HEAD^{/Start.the.merging-rebase}") || {
echo "::error::Cannot find marker for shears/$name"
errors=1
continue
}
tip=$(git -C "$worktree" rev-parse HEAD)
report="$worktree/conflict-report.md"
test -f "$report" || continue
push_to_builds \
"$marker:refs/heads/base/shears/$name-$run_id" \
"$tip:refs/heads/shears/$name-$run_id" || {
echo "::error::Failed to push shears/$name to $builds_repo"
errors=1
continue
}
title="Rebase shears/$name (#$run_id)"
stats="$worktree/conflict-stats.txt"
if test -f "$stats"; then
eval "$(cat "$stats")"
total=$((skipped + resolved))
if test "$total" -gt 0; then
title="Rebase shears/$name: $total conflict(s) ($skipped skipped, $resolved resolved) (#$run_id)"
fi
fi
{
echo "[Workflow run]($run_url)"
echo
cat "$report"
} >"$worktree/pr-body.md"
pr_url=$(gh api "repos/$builds_repo/pulls" \
-f "title=$title" \
-f "head=shears/$name-$run_id" \
-f "base=base/shears/$name-$run_id" \
-F "body=@$worktree/pr-body.md" \
--jq '.html_url') || {
echo "::error::Failed to create PR for shears/$name"
errors=1
continue
}
echo "::notice::Created PR '$title': $pr_url"
done
# Failed rebases: create draft PRs, upload artifacts
for worktree in ${{ steps.rebase-single.outputs.failed_worktrees }} ${{ steps.rebase-all.outputs.failed_worktrees }}; do
test -d "$worktree" || continue
name=${worktree##*rebase-worktree-}
report="$worktree/conflict-report.md"
tip=$("$GITHUB_WORKSPACE/automation/stash-with-conflicts.sh" "$worktree") || {
echo "::warning::Failed to create state commit for shears/$name, using HEAD"
tip=$(git -C "$worktree" rev-parse HEAD)
}
marker=$(git -C "$worktree" rev-parse "HEAD^{/Start.the.merging-rebase}") || {
echo "::warning::Marker not found for shears/$name, trying merge-base"
marker=$(git -C "$worktree" merge-base HEAD REBASE_HEAD) || {
echo "::error::Cannot determine base for shears/$name, skipping"
errors=1
continue
}
}
push_to_builds \
"$marker:refs/heads/base/shears/$name-$run_id" \
"$tip:refs/heads/shears/$name-$run_id" || {
echo "::error::Failed to push shears/$name to $builds_repo"
errors=1
continue
}
title="FAILED: Rebase shears/$name (#$run_id)"
{
echo "[Workflow run]($run_url) failed to rebase shears/$name."
echo
test -f "$report" && cat "$report"
} >"$worktree/pr-body.md"
pr_url=$(gh api "repos/$builds_repo/pulls" \
-f "title=$title" \
-f "head=shears/$name-$run_id" \
-f "base=base/shears/$name-$run_id" \
-F "body=@$worktree/pr-body.md" \
-F "draft=true" \
--jq '.html_url') || {
echo "::error::Failed to create draft PR for shears/$name"
errors=1
continue
}
echo "::notice::Created draft PR '$title': $pr_url"
# Upload bundle and worktree archive as release assets
assets=""
test -f "upload/$name.bundle" &&
assets="$assets upload/$name.bundle#Git bundle"
test -f "upload/$name.tar.gz" &&
assets="$assets upload/$name.tar.gz#Worktree archive"
if test -n "$assets"; then
tag="shears-$name-$run_id"
gh release create "$tag" \
--repo "$builds_repo" \
--target "$tip" \
--title "$title" \
--notes "See [draft PR]($pr_url)" \
--prerelease \
$assets || {
echo "::warning::Failed to upload artifacts for shears/$name"
}
gh api "repos/$builds_repo/issues/${pr_url##*/}/comments" \
-f "body=Artifacts: [release assets](https://github.com/$builds_repo/releases/tag/$tag)" || {
echo "::warning::Failed to comment artifact link for shears/$name"
}
fi
done
exit $errors