-
Notifications
You must be signed in to change notification settings - Fork 0
302 lines (278 loc) · 12.9 KB
/
Copy pathautoupdate.yml
File metadata and controls
302 lines (278 loc) · 12.9 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
name: Autoupdate Scoop manifests
# Refresh wheels.json (stable) and wheels-be.json (bleeding-edge) when the
# upstream wheels-dev/wheels release workflow tags a new version.
#
# Flow (PR + auto-merge model, mirrors wheels-dev/homebrew-wheels):
# 1. scoop checkver -Update rewrites the manifest in place (no git ops).
# 2. If anything changed, we create a feature branch, commit, push, and
# open a PR via `gh pr create`.
# 3. A synchronous wait-and-merge loop polls the PR's mergeStateStatus.
# Once CLEAN, we `gh pr merge --squash --delete-branch` and exit.
# 4. The concurrency lock is held through the merge so the next queued
# run forks off a clean, already-bumped base.
#
# Three triggers:
# - repository_dispatch (wheels-released): fired by wheels-dev/wheels
# release.yml on every published release. End-to-end ~2-3 min from
# upstream tag → PR merge here. Payload includes 'channel' so we
# only refresh the affected manifest.
# - schedule: daily cron at 08:30 UTC. Catches missed dispatches
# (token expiry, network blip) without waiting for the next release.
# - workflow_dispatch: manual one-off for debugging.
#
# Concurrency: serialize across all triggers. Back-to-back snapshots can
# fire dispatches seconds apart; without serialization they fork off the
# same base and conflict. cancel-in-progress=false because each dispatch
# must actually bump the manifest — skipping intermediate snapshots in a
# burst still leaves the bucket at the wrong version.
#
# Auth: AUTO_MERGE_PAT is a fine-grained PAT with Contents:write +
# Pull-requests:write on this repo. Used for the checkout (so the push
# event triggers any downstream workflows), the push, the PR creation,
# and the merge. Falls back to GITHUB_TOKEN if the secret isn't set —
# push still works but auto-merge may be gated by the default token's
# downstream-event restriction.
#
# Security note: every value from github.event.client_payload.* flows
# through env: bindings before reaching any run script — never inlined
# via ${{ }} interpolation in a shell command. See the security guide:
# https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/
on:
repository_dispatch:
types: [wheels-released]
schedule:
- cron: '30 8 * * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
concurrency:
group: scoop-autoupdate
cancel-in-progress: false
jobs:
autoupdate:
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout bucket
uses: actions/checkout@v4
with:
token: ${{ secrets.AUTO_MERGE_PAT || secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Install Scoop
shell: pwsh
run: |
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force
Invoke-RestMethod -Uri https://get.scoop.sh -OutFile install.ps1
.\install.ps1 -RunAsAdmin
$scoopShim = "$env:USERPROFILE\scoop\shims"
echo "$scoopShim" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "SCOOP_HOME=$(scoop prefix scoop)" | Out-File -FilePath $env:GITHUB_ENV -Append
# `hub` is no longer required — we use checkver.ps1 directly (no
# git ops in scoop) plus `gh` (preinstalled on the runner) for
# the PR + merge steps. Previous direct-push flow needed it
# because auto-pr.ps1 -Push wrapped git via hub; we don't call
# auto-pr.ps1 anymore.
- name: Decide which manifests to refresh
id: route
shell: pwsh
env:
DISPATCH_CHANNEL: ${{ github.event.client_payload.channel }}
run: |
# Channel from repository_dispatch routes to a single manifest.
# cron and workflow_dispatch refresh both.
$event = "${env:GITHUB_EVENT_NAME}"
$channel = "${env:DISPATCH_CHANNEL}"
$app = '*'
if ($event -eq 'repository_dispatch') {
switch ($channel) {
'stable' { $app = 'wheels' }
'bleeding-edge' { $app = 'wheels-be' }
'release-candidate' {
# RCs aren't published to Scoop today. Skip cleanly so the
# dispatch loop in wheels release.yml can stay channel-agnostic.
Write-Host "RC dispatch — Scoop has no RC channel, skipping."
"skip=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
exit 0
}
default {
# Unknown channel (or empty). Refresh both as a safe fallback.
Write-Host "Unknown channel '$channel' — refreshing both manifests."
$app = '*'
}
}
}
Write-Host "Event=$event Channel=$channel App=$app"
"app=$app" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"skip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: Run Scoop checkver (update manifests in-place, no git)
if: steps.route.outputs.skip != 'true'
shell: pwsh
env:
APP: ${{ steps.route.outputs.app }}
run: |
$ErrorActionPreference = 'Stop'
$checkver = Join-Path $env:SCOOP_HOME 'bin\checkver.ps1'
if (-not (Test-Path $checkver)) {
throw "Cannot find checkver.ps1 at $checkver"
}
# -Update writes the manifest in place if the upstream version
# changed; -SkipUpdated suppresses the "no update needed" lines.
# No git operations — the PR creation runs as a separate step
# below so we have full control over the branch / commit / PR
# metadata.
& $checkver -App $env:APP -Dir './bucket' -Update -SkipUpdated
- name: Detect manifest changes and compute PR metadata
id: changes
if: steps.route.outputs.skip != 'true'
shell: pwsh
env:
DISPATCH_VERSION: ${{ github.event.client_payload.version }}
DISPATCH_CHANNEL: ${{ github.event.client_payload.channel }}
run: |
$ErrorActionPreference = 'Stop'
$changed = (git status --porcelain ./bucket) -split "`n" | Where-Object { $_ -ne '' }
if (-not $changed -or $changed.Count -eq 0) {
Write-Host "No manifest changes detected; skipping PR."
"has_changes=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
exit 0
}
Write-Host "Changes detected:"
$changed | ForEach-Object { Write-Host " $_" }
$event = "${env:GITHUB_EVENT_NAME}"
$version = "${env:DISPATCH_VERSION}"
$channel = "${env:DISPATCH_CHANNEL}"
$runNumber = "${env:GITHUB_RUN_NUMBER}"
# Validate the version string before letting it shape a branch
# name or commit message — it originates from a repository_dispatch
# payload (untrusted). Allow only SemVer-ish characters.
if ($version -and $version -notmatch '^[A-Za-z0-9_.+\-]{1,64}$') {
Write-Warning "Rejecting suspicious version string: '$version'"
$version = ''
}
# Branch names can't contain '+' (URL-encoded as %2B and breaks
# `gh pr create --head`). SemVer build metadata uses '+' so we
# normalize to '-' for the branch only — the manifest keeps the
# canonical version string.
$safeVer = $version -replace '\+', '-'
if ($event -eq 'repository_dispatch' -and $version) {
if ($channel -eq 'stable') {
$title = "wheels: update to $version"
$branch = "auto-update/wheels-$safeVer"
} elseif ($channel -eq 'bleeding-edge') {
$title = "wheels-be: update to $version"
$branch = "auto-update/wheels-be-$safeVer"
} else {
$title = "chore: refresh manifests ($channel $version)"
$branch = "auto-update/run-$runNumber"
}
} else {
$title = "chore: scheduled manifest refresh (run $runNumber)"
$branch = "auto-update/cron-$runNumber"
}
"has_changes=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"branch=$branch" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"title=$title" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Host "Branch: $branch"
Write-Host "Title: $title"
- name: Create branch, commit, push, open PR
id: pr
if: steps.changes.outputs.has_changes == 'true'
shell: pwsh
env:
GH_TOKEN: ${{ secrets.AUTO_MERGE_PAT || secrets.GITHUB_TOKEN }}
BRANCH: ${{ steps.changes.outputs.branch }}
TITLE: ${{ steps.changes.outputs.title }}
run: |
$ErrorActionPreference = 'Stop'
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git checkout -b $env:BRANCH
git add ./bucket
git commit -m $env:TITLE
git push origin $env:BRANCH
$runUrl = "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID"
$eventName = "$env:GITHUB_EVENT_NAME"
$body = @(
"Auto-updated by ``scoop checkver`` from the upstream wheels-dev/wheels release.",
"",
"This PR was opened by the autoupdate.yml workflow on ``$eventName``. It will auto-merge once any required CI checks pass.",
"",
"- Triggered by: ``$eventName``",
"- Workflow run: $runUrl"
) -join "`n"
gh pr create `
--base main `
--head $env:BRANCH `
--title $env:TITLE `
--body $body | Out-Null
# Look the PR up by branch — more reliable than parsing the URL
# gh pr create prints to stdout.
$prNumber = (gh pr list --head $env:BRANCH --base main --json number --jq '.[0].number')
if (-not $prNumber) {
throw "Could not resolve PR number for branch $env:BRANCH"
}
Write-Host "Opened PR #$prNumber"
"pr_number=$prNumber" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
# Synchronous wait-and-merge — mirrors wheels-dev/homebrew-wheels'
# auto-update.yml pattern. The reason we don't use `gh pr merge --auto`:
# --auto releases the workflow's concurrency lock before the merge
# actually commits, allowing the next queued auto-update run to fork
# off a stale base. Holding the slot through the merge means each
# subsequent run sees an already-bumped main and produces a clean PR
# with no conflicts.
#
# Polls every 15s, caps total wait at 10 min. CLEAN → squash-merge.
# BLOCKED/UNSTABLE → required check still running, keep waiting.
# DIRTY → unexpected (concurrency group should prevent it); fail
# loudly. BEHIND → upstream moved; let GitHub auto-update via --auto.
- name: Wait for checks and merge
if: steps.changes.outputs.has_changes == 'true'
shell: pwsh
env:
GH_TOKEN: ${{ secrets.AUTO_MERGE_PAT || secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: |
$ErrorActionPreference = 'Stop'
$MAX_WAIT = 600
$INTERVAL = 15
$elapsed = 0
$state = ''
while ($elapsed -lt $MAX_WAIT) {
$state = (gh pr view $env:PR_NUMBER --json mergeStateStatus --jq '.mergeStateStatus').Trim()
switch ($state) {
'CLEAN' {
Write-Host "PR #$env:PR_NUMBER is mergeable. Squash-merging..."
gh pr merge $env:PR_NUMBER --squash --delete-branch
Write-Host "PR #$env:PR_NUMBER merged."
exit 0
}
{ $_ -in 'BLOCKED','UNSTABLE' } {
Write-Host "PR #$env:PR_NUMBER state=$state; waiting (${elapsed}s elapsed)..."
Start-Sleep -Seconds $INTERVAL
$elapsed += $INTERVAL
}
'DIRTY' {
Write-Error "PR #$env:PR_NUMBER is in DIRTY state — concurrency lock failed to prevent a conflict."
exit 1
}
'BEHIND' {
Write-Host "PR #$env:PR_NUMBER is BEHIND base — switching to --auto so GitHub rebases and merges."
gh pr merge $env:PR_NUMBER --squash --delete-branch --auto
exit 0
}
default {
Write-Host "Unknown mergeStateStatus '$state'; waiting..."
Start-Sleep -Seconds $INTERVAL
$elapsed += $INTERVAL
}
}
}
Write-Error "PR #$env:PR_NUMBER did not become mergeable within $MAX_WAIT seconds (last state: $state)"
exit 1
- name: Report
if: always()
shell: pwsh
run: |
git log --oneline -5
git status