Repository navigation
198 lines (182 loc) · 9.73 KB
/
Copy pathdependency-lock-sync.yml
File metadata and controls
198 lines (182 loc) · 9.73 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
name: Dependency Lock Sync
# Refreshes OmadaWeb.PS/DependencyLock.psd1 and Build/Dependencies.psd1 after a version changed in
# Build/Dependencies.
#
# The module verifies every binary it downloads against the SHA-256 pinned in the lock file, so a
# version bump is only part of an update: the hash has to be recomputed from the newly published
# package, and the SBOM has to report the version that is actually loaded. PR Validation fails while
# any of the three disagree, so this is the step that makes such a pull request mergeable.
#
# It runs weekly, a few hours after Dependabot's Monday run, over every open Dependabot pull request
# against Build/Dependencies - so a bump arrives already synced rather than waiting for someone to
# notice a red pull request. It can also be started by hand from the Actions tab for a single branch,
# which is what to use after a rebase. The same thing locally is:
#
# ./Build/Update-DependencyLock.ps1 -Refresh
#
# Two things worth knowing before reading the jobs below.
#
# Trust. The sweep pushes to a branch it did not write, so it must not *execute* anything from that
# branch. It checks out two trees - the default branch and the pull request branch - and runs the
# script from the default branch against the other one as data, via -RepositoryRoot. Nothing from the
# pull request branch runs with the write-scoped token. This is also why the workflow is triggered by
# schedule and workflow_dispatch rather than by the pull request itself: both run in the context of
# the default branch, so the token is a normal one and no pull_request_target is involved.
#
# Consequences on the pull request. Pushing to a Dependabot branch stops Dependabot from rebasing it
# automatically; comment `@dependabot recreate` to get a fresh one. The refreshed pins are committed
# through the REST contents API rather than with git push: a commit created that way is authored by
# github-actions[bot] and signed by GitHub, which is what lets .github/workflows/dependabot-validation.yml
# tell this workflow's commit from one pushed by anybody else. A push made with the GITHUB_TOKEN starts
# no workflow run, so after committing this workflow dispatches dependabot-validation.yml for the branch
# itself. That workflow starts PR Validation when the bump passes its reputation policy, and leaves it to
# a maintainer's `/validate` when it does not. Merging stays with a maintainer either way, and the
# -Check gate still blocks an unsynced bump from merging.
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch carrying the version bump (e.g. dependabot/nuget/Build/Dependencies/Selenium.WebDriver-4.48.0)'
required: true
type: string
schedule:
# Monday 06:00 UTC. Dependabot is scheduled for Monday in .github/dependabot.yml and opens its
# pull requests in the small hours, so by now there is something to sync.
- cron: '0 6 * * 1'
permissions:
contents: read
concurrency:
# A manual run for one branch is independent of the weekly sweep, so it gets its own group. Two
# runs are never cancelled in favour of each other: the loser would leave a branch half-synced.
group: dependency-lock-sync-${{ github.event.inputs.branch || 'sweep' }}
cancel-in-progress: false
jobs:
discover:
name: Find branches to sync
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
branches: ${{ steps.list.outputs.branches }}
count: ${{ steps.list.outputs.count }}
steps:
- name: List the branches carrying a bump
id: list
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
// workflow_dispatch names one branch explicitly and skips discovery entirely.
if (context.eventName === 'workflow_dispatch') {
const branch = context.payload.inputs.branch;
core.setOutput('branches', JSON.stringify([branch]));
core.setOutput('count', '1');
return;
}
const pulls = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
// Only Dependabot's own branches, and only those touching the manifests this lock
// tracks. Anything else either has no pins to refresh or is not ours to push to.
const branches = pulls
.filter(pull => pull.user.login === 'dependabot[bot]')
.filter(pull => pull.head.repo?.full_name === `${context.repo.owner}/${context.repo.repo}`)
.filter(pull => pull.head.ref.startsWith('dependabot/nuget/Build/Dependencies'))
.map(pull => pull.head.ref);
core.info(`Branches to sync: ${branches.length ? branches.join(', ') : '(none)'}`);
core.setOutput('branches', JSON.stringify(branches));
core.setOutput('count', String(branches.length));
sync:
name: Refresh pinned hashes
needs: discover
if: needs.discover.outputs.count != '0'
runs-on: windows-latest
permissions:
# Commit the refreshed pins through the contents API.
contents: write
# Dispatch dependabot-validation.yml for the branch after committing.
actions: write
strategy:
# One failing branch must not stop the others from being synced.
fail-fast: false
matrix:
branch: ${{ fromJson(needs.discover.outputs.branches) }}
steps:
- name: Checkout the default branch
# The trusted tree. Only the script from here is ever executed.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: trusted
persist-credentials: false
- name: Checkout the branch carrying the bump
# The untrusted tree. Read as data, written to, pushed - never executed.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ matrix.branch }}
path: bump
- name: Refresh the dependency lock and the SBOM
shell: pwsh
# The Dependabot configuration is read from the trusted tree rather than from the branch,
# because that is the copy Dependabot itself obeys: it only ever reads dependabot.yml from the
# default branch. Taking it from the branch would fail the closure ignore-policy check on any
# bump opened before those rules landed - refusing to sync a branch over a rule that does not
# apply to it. PR Validation still checks the branch's own copy, where it belongs.
run: >-
./trusted/Build/Update-DependencyLock.ps1 -Refresh
-RepositoryRoot (Resolve-Path ./bump).Path
-DependabotConfigPath (Resolve-Path ./trusted/.github/dependabot.yml).Path
- name: Commit the refreshed pins
id: commit
shell: pwsh
working-directory: bump
# The branch name arrives through the environment rather than being interpolated into the
# script, so a branch whose name contains shell metacharacters cannot become code here.
env:
BUMP_BRANCH: ${{ matrix.branch }}
GITHUB_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
run: |
$Paths = @('OmadaWeb.PS/DependencyLock.psd1', 'Build/Dependencies.psd1')
$Changed = @($Paths | Where-Object { -not [string]::IsNullOrWhiteSpace((git status --porcelain -- $_)) })
if ($Changed.Count -eq 0) {
Write-Host "Pins already match the manifest; nothing to commit."
"committed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
exit 0
}
$Headers = @{
Authorization = "Bearer $env:GITHUB_TOKEN"
Accept = 'application/vnd.github+json'
'X-GitHub-Api-Version' = '2022-11-28'
}
foreach ($Path in $Changed) {
# The repository stores these files with LF line endings; Update-DependencyLock.ps1 writes
# CRLF. git would normalise that on commit, the contents API commits bytes as given.
$Text = [System.IO.File]::ReadAllText((Resolve-Path $Path)) -replace "`r`n", "`n"
$Uri = 'https://api.github.com/repos/{0}/contents/{1}' -f $env:REPOSITORY, $Path
$Current = Invoke-RestMethod -Uri ('{0}?ref={1}' -f $Uri, [uri]::EscapeDataString($env:BUMP_BRANCH)) -Headers $Headers
$Body = @{
message = "deps: refresh pinned SHA-256 hashes and SBOM versions for the bumped package ($Path)"
content = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Text))
sha = $Current.sha
branch = $env:BUMP_BRANCH
} | ConvertTo-Json
$Result = Invoke-RestMethod -Method Put -Uri $Uri -Headers $Headers -Body $Body -ContentType 'application/json'
Write-Host ("Committed {0} as {1}." -f $Path, $Result.commit.sha)
}
"committed=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
- name: Evaluate the bump against the validation policy
if: steps.commit.outputs.committed == 'true'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
BUMP_BRANCH: ${{ matrix.branch }}
# ref_name rather than event.repository.default_branch: a scheduled run's payload carries no
# repository object, and both triggers of this workflow run on the default branch.
DEFAULT_BRANCH: ${{ github.ref_name }}
run: |
gh workflow run dependabot-validation.yml --ref $env:DEFAULT_BRANCH -f "branch=$env:BUMP_BRANCH" -f lock_synced=true
exit $LASTEXITCODE