-
Notifications
You must be signed in to change notification settings - Fork 1k
283 lines (239 loc) 路 10.5 KB
/
Copy pathupdate-nix-hashes.yml
File metadata and controls
283 lines (239 loc) 路 10.5 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
name: Update Nix hashes
on:
pull_request:
types:
- opened
- synchronize
- reopened
paths:
- mix.exs
- mix.lock
- "!.github/**" # Important: Exclude PRs related to .github from auto-run
- "!.github/workflows/**" # Important: Exclude PRs related to .github/workflows from auto-run
- "!.github/actions/**" # Important: Exclude PRs related to .github/actions from auto-run
# For pull requests that the gate holds back, so the hashes can still be
# verified deliberately, by someone who can trigger a workflow run.
workflow_dispatch:
permissions:
contents: read
concurrency:
group: update-nix-hashes-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
PATCH_NAME: nix-hashes.patch
jobs:
check_paths:
uses: ./.github/workflows/check_paths.yml
# Runs for every pull request that touches the dependencies, so a stale hash
# becomes visible wherever it appears, not just where it is repaired
# automatically. Refreshing the hashes means building those dependencies and
# therefore running their code, which is why this job receives no secrets or
# repository write credentials:
# it only reports what would have to change and publishes it as a patch.
# Pushing that change back is left to the commit job below.
compute:
name: Compute updated hashes
needs: check_paths
# Pull requests that touch .github are not run automatically anywhere in
# this repository. Those are what the manual trigger is for.
if: needs.check_paths.outputs.githubfolder == 'false' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: read
outputs:
changed: ${{ steps.diff.outputs.changed }}
steps:
- name: Checkout the pull request commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Install Nix
uses: nixbuild/nix-quick-install-action@9f63be77f412a248c9d9a65a4c82cf066cdf8f0c # v35
- name: Nix binary cache
uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2
with:
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', 'mix.lock') }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}-
# collect garbage until Nix store size (in bytes) is at most this number
# before trying to save a new cache
# 1G = 1073741824, 2G = 2147483648
gc-max-store-size-linux: 2147483648
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}-
# created more than this number of seconds ago
# relative to the start of the `Post Restore and save Nix store` phase
purge-created: 0
# except any version with the key that is the same as the `primary-key`
purge-primary-key: never
# The helper invalidates every pinned hash before building, so a stale
# fixed-output path restored from the cache cannot hide a change.
- name: Refresh and verify the Nix hashes
run: nix run .#update-nix-hashes
- name: Collect the hash changes
id: diff
shell: bash
run: |
git diff --output="$PATCH_NAME" -- nix
if [ -s "$PATCH_NAME" ]; then
cat "$PATCH_NAME"
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "no hash needed an update"
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
# Uploaded for every pull request, not only the ones repaired below: on a
# human pull request it is what the author has to apply.
- name: Upload the patch
if: steps.diff.outputs.changed == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nix-hashes-patch
path: ${{ env.PATCH_NAME }}
if-no-files-found: error
retention-days: 7
# Deliberately separate from the commit job: this one fails whenever the
# hashes are stale, on every pull request including Dependabot's, so the
# state is never reported as good while it is not. The commit job depends on
# `compute` alone, so it still runs and repairs them, and its push produces a
# new run in which there is nothing left to change and this job is green.
verify:
name: Verify the pinned hashes
needs: compute
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Report stale hashes
shell: bash
env:
CHANGED: ${{ needs.compute.outputs.changed }}
run: |
set -euo pipefail
if [ "$CHANGED" = "true" ]; then
echo "::error::the pinned Nix hashes are out of date. Run 'nix run .#update-nix-hashes' and commit the result, or download the nix-hashes-patch artifact of this run." >&2
exit 1
fi
echo "the pinned Nix hashes are up to date"
commit:
name: Commit updated hashes
needs: compute
# Repairing automatically means pushing into the pull request's branch, so
# it stays limited to Dependabot's own pull requests from branches in this
# repository, where nobody else can steer that branch. The actor check also
# keeps the updater from running on the commit it pushes itself: that push
# uses a token of its own, so the resulting synchronize event no longer has
# Dependabot as its actor.
if: >-
needs.compute.outputs.changed == 'true' &&
github.actor == 'dependabot[bot]' &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
timeout-minutes: 10
# The push uses its own token, so the workflow token stays read-only.
permissions:
contents: read
steps:
- name: Checkout the Dependabot commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Download the patch
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nix-hashes-patch
# The patch comes out of a job that ran third-party build code, so it is
# untrusted input. Rather than parsing its syntax, it is applied to the
# index without a token and the staged result is validated: that is what
# a commit would contain, and unlike a patch it cannot be misread. A
# binary hunk, or ---/+++ paths disagreeing with the diff --git line,
# change nothing about the check below.
- name: Apply the patch to the index
shell: bash
run: |
set -euo pipefail
if [ ! -s "$PATCH_NAME" ]; then
echo "::error::the patch is empty" >&2
exit 1
fi
git apply --cached -- "$PATCH_NAME"
- name: Validate the staged result
shell: bash
run: |
set -euo pipefail
staged=0
# Each record is ":<src mode> <dst mode> <src sha> <dst sha> <status>"
# followed by the path, both NUL terminated.
while IFS= read -r -d '' entry && IFS= read -r -d '' path; do
read -r src_mode dst_mode _ _ status <<<"${entry#:}"
if [ "$status" != "M" ]; then
echo "::error::$path is staged as '$status', but only modifications are allowed" >&2
exit 1
fi
if [ "$src_mode" != "100644" ] || [ "$dst_mode" != "100644" ]; then
echo "::error::$path changes its mode from $src_mode to $dst_mode" >&2
exit 1
fi
case "$path" in
nix/*.nix) ;;
*)
echo "::error::$path is not a .nix file below nix/" >&2
exit 1
;;
esac
before="$(mktemp)"
after="$(mktemp)"
# Masking only well formed SRI literals folds the format check into
# the comparison: a malformed replacement is left in place and the
# two versions stop matching.
git show "HEAD:$path" | sed -E 's|sha256-[A-Za-z0-9+/]{43}=|<hash>|g' > "$before"
git show ":$path" | sed -E 's|sha256-[A-Za-z0-9+/]{43}=|<hash>|g' > "$after"
if ! cmp -s "$before" "$after"; then
echo "::error::$path changes more than sha256 literals" >&2
diff -u "$before" "$after" >&2 || true
exit 1
fi
echo "$path: only sha256 literals changed"
staged=$((staged + 1))
done < <(git diff --cached --raw -z HEAD)
if [ "$staged" -eq 0 ]; then
echo "::error::the patch staged no change" >&2
exit 1
fi
- name: Commit the staged hashes
shell: bash
run: |
set -euo pipefail
git \
-c user.name="teslamate-actions" \
-c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -m "build(deps): update Nix hashes"
# The only step that sees a write token, in a workspace that never ran
# repository code.
- name: Push to the Dependabot branch
shell: bash
env:
PUSH_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
: "${PUSH_TOKEN:?is empty. Runs triggered by Dependabot cannot read Actions secrets, so this has to be stored as a Dependabot secret}"
# No force: if Dependabot rebased the branch in the meantime, the
# patch was computed against a commit that is no longer there and the
# push has to fail rather than overwrite it.
# The single quotes are load-bearing: git expands ${PUSH_TOKEN} from
# the environment only inside the helper subprocess, so the token
# never appears on a command line or on disk.
git \
-c credential.helper='!f() { echo "username=x-access-token"; echo "password=${PUSH_TOKEN}"; }; f' \
push \
"https://github.com/${GITHUB_REPOSITORY}.git" \
"HEAD:refs/heads/${HEAD_REF}"