-
Notifications
You must be signed in to change notification settings - Fork 1.2k
336 lines (314 loc) · 14.1 KB
/
Copy pathci-pipeline.yml
File metadata and controls
336 lines (314 loc) · 14.1 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
name: CI Pipeline
on:
workflow_dispatch:
push:
branches-ignore:
- coverity_scan
- run-fuzzer**
- debug-fuzzer-**
pull_request:
schedule:
- cron: '0 20 * * *'
#
# Cancel any in-flight or queued run of the 'CI Pipeline' workflow on the
# same branch.
#
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
#
# An ad hoc continuous integration (CI) run needs the runners that a
# scheduled round (see `ci-scheduled.yml`) may be occupying. Every
# pipeline run therefore cancels any scheduled round in flight before
# the pipeline run's own legs queue. A pull request from a fork holds a read-only token
# whatever permissions the job requests. A fork pull request
# therefore cancels nothing, and the fork pull request's legs wait
# until the scheduled round finishes.
#
scheduled_ci_cancel:
name: Cancel scheduled CI rounds
if: github.repository == 'FreeRADIUS/freeradius-server'
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Cancel any scheduled round in flight
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
for status in queued in_progress; do
for id in $(gh run list --repo "$REPO" --workflow ci-scheduled.yml \
--status "$status" --json databaseId --jq '.[].databaseId' 2>/dev/null); do
echo "Cancelling scheduled run $id, adhoc CI takes priority"
gh run cancel "$id" --repo "$REPO" || true
done
done
ci_prev_run:
name: "CI already run?"
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
should_skip: ${{ steps.fork_schedule.outcome == 'success' || steps.skip_check.outputs.should_skip == 'true' }}
steps:
#
# Only the self-hosted runners have the certificate authority (CA)
# file for the registry mirror. A fork's scheduled run builds every
# image on GitHub's runners, so the image build fails before the run
# tests anything. `fork_schedule` therefore runs only for a
# scheduled run outside FreeRADIUS/freeradius-server, the job
# reports `should_skip` whenever `fork_schedule` ran, and every leg
# reads `should_skip` and skips.
#
- id: fork_schedule
if: ${{ github.event_name == 'schedule' && github.repository != 'FreeRADIUS/freeradius-server' }}
run: echo "Scheduled run outside the main repository, every leg skips"
#
# `concurrent_skipping` widens the duplicate check to runs still in
# flight. `same_content_newer` skips the current run when an older
# run of the identical tree is in flight. The fast-forward to
# `master` lands while the branch pipeline that tested the commit is
# still in flight. Only the check against runs still in flight
# can match.
# Developer branches and pull requests keep 'never'. The
# concurrency group for a developer branch or pull request cancels
# the older run, and skipping against a cancelled run would merge
# an untested tree.
#
- id: skip_check
if: ${{ steps.fork_schedule.outcome == 'skipped' }}
uses: fkirc/skip-duplicate-actions@v5.3.2
with:
concurrent_skipping: ${{ github.event_name == 'push' && !startsWith(github.ref_name, 'developer/') && 'same_content_newer' || 'never' }}
#
# `common_vars` determines which registry holds the images and which
# image tag the current ref should use. `common_vars` also determines
# whether the current ref changes any file that the images include.
# See `.github/workflows/common_vars.yml`.
#
common_vars:
name: Common vars
uses: ./.github/workflows/common_vars.yml
#
# Does the registry hold the images that the current ref needs? The
# images can be absent for multiple reasons: a failed refresh, pruned
# images, a rebuilt registry.
#
# The job runs on a self-hosted runner, because GitHub runners cannot
# resolve the registry. If one image is present, we assume that all
# the images are present. If we cannot get a definitive answer, we
# skip the build.
#
images_present:
name: "Docker images present?"
needs: [common_vars]
if: ${{ github.event_name == 'push'
&& github.repository_owner == 'FreeRADIUS'
&& needs.common_vars.outputs.images_changed == 'false' }}
runs-on: ${{ needs.common_vars.outputs.selfhosted == '1' && 'self-hosted' || 'ubuntu-latest' }}
outputs:
missing: ${{ steps.probe.outputs.missing }}
steps:
- name: Install regctl
uses: regclient/actions/regctl-installer@main
- name: Look for the sentinel image
id: probe
env:
DOCKER_SENTINEL_IMAGE: ${{ needs.common_vars.outputs.image_prefix }}self-hosted:${{ needs.common_vars.outputs.image_tag }}
DOCKER_REGISTRY: ${{ needs.common_vars.outputs.docker_registry }}
DOCKER_USERNAME: ${{ secrets.DOCKER_REPO_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_REPO_PASSWORD }}
run: |
if [ -n "$DOCKER_USERNAME" ]; then
printf '%s' "$DOCKER_PASSWORD" \
| regctl registry login "$DOCKER_REGISTRY" -u "$DOCKER_USERNAME" --pass-stdin \
|| echo "Continuing without login, regctl could not sign in to $DOCKER_REGISTRY"
fi
if regctl manifest head "$DOCKER_SENTINEL_IMAGE" >/dev/null 2>&1; then
echo "$DOCKER_SENTINEL_IMAGE present"
echo "missing=false" >> "$GITHUB_OUTPUT"
elif regctl repo ls "$DOCKER_REGISTRY" >/dev/null 2>&1; then
# The registry answered, so the registry is reachable and the
# image really is absent.
echo "$DOCKER_SENTINEL_IMAGE absent, the images need rebuilding"
echo "missing=true" >> "$GITHUB_OUTPUT"
else
echo "Assuming the images are present, $DOCKER_REGISTRY did not answer"
echo "missing=false" >> "$GITHUB_OUTPUT"
fi
#
# The refresh job skips on the vast majority of commits. When the
# refresh job runs, every leg waits for the refresh job, so the
# docker images have all necessary packages before the rest of CI
# runs.
#
# Only a push rebuilds, because only a push publishes, and a pull
# request has no tag to publish to. A branch publishes under a tag
# named after the branch (a push to `feature/dhcp-fix` publishes
# `self-hosted-ci-ubuntu24:feature-dhcp-fix`, while `master` keeps reading
# `self-hosted-ci-ubuntu24:latest`), so work in progress never
# overwrites the images that the default branch reads. A fork has no
# images at all until the fork builds images, so a fork always
# refreshes. The fork refreshes into the fork's own registry, where
# the fork can overwrite nothing but the fork's own tags.
#
#
# A skipped job in `needs` skips every dependent before GitHub reads
# the dependent's own condition, unless the condition contains
# `!cancelled()`. `images_present` skips whenever the images changed,
# and an image change is exactly when the refresh has to run. The
# condition therefore checks each prerequisite's result itself.
#
docker_ci_images_refresh:
name: Docker CI images refresh
needs: [common_vars, images_present, ci_prev_run]
if: ${{ !cancelled()
&& needs.common_vars.result == 'success'
&& needs.ci_prev_run.result == 'success'
&& needs.images_present.result != 'failure'
&& needs.ci_prev_run.outputs.should_skip != 'true'
&& ((github.event_name == 'push'
&& (needs.common_vars.outputs.images_changed == 'true'
|| needs.images_present.outputs.missing == 'true'))
|| github.repository_owner != 'FreeRADIUS') }}
uses: ./.github/workflows/docker-ci-images-refresh.yml
secrets: inherit
#
# Only the multi-server tests, `docker-crossbuild` and `docker-service`
# read the `freeradius4-{crossbuild,profiling-deps,service}` images,
# and the 'freeradius4' images take the longest to build. The
# 'freeradius4' refresh therefore runs as a separate job, and only the
# legs that read the 'freeradius4' images wait for the refresh. The
# build legs start as soon as `docker_ci_images_refresh` publishes the
# images that the build legs read.
#
docker_freeradius_images_refresh:
name: Docker FreeRADIUS images refresh
needs: [docker_ci_images_refresh]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result == 'success' }}
uses: ./.github/workflows/docker-freeradius-images-refresh.yml
secrets: inherit
ci:
needs: [docker_ci_images_refresh, ci_prev_run]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.ci_prev_run.outputs.should_skip != 'true' }}
name: CI
uses: ./.github/workflows/ci.yml
secrets: inherit
ci-deb:
needs: [docker_ci_images_refresh, ci_prev_run]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.ci_prev_run.outputs.should_skip != 'true' }}
name: CI DEB
uses: ./.github/workflows/ci-deb.yml
secrets: inherit
ci-rpm:
needs: [docker_ci_images_refresh, ci_prev_run]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.ci_prev_run.outputs.should_skip != 'true' }}
name: CI RPM
uses: ./.github/workflows/ci-rpm.yml
secrets: inherit
ci-sanitizers:
needs: [docker_ci_images_refresh, ci_prev_run]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.ci_prev_run.outputs.should_skip != 'true' }}
name: CI-Sanitizers
uses: ./.github/workflows/ci-sanitizers.yml
secrets: inherit
#
# `ci-freebsd` and `ci-macos` run on every commit but are absent
# from the `needs` list of the `merge` job, because both legs are too unreliable. Both legs run
# on virtual machines and read no docker images, so neither leg waits
# for a refresh.
#
ci-freebsd:
needs: [ci_prev_run]
if: ${{ !cancelled()
&& needs.ci_prev_run.outputs.should_skip != 'true' }}
name: CI FreeBSD
uses: ./.github/workflows/ci-freebsd.yml
secrets: inherit
ci-macos:
needs: [ci_prev_run]
if: ${{ !cancelled()
&& needs.ci_prev_run.outputs.should_skip != 'true' }}
name: CI macOS
uses: ./.github/workflows/ci-macos.yml
secrets: inherit
#
# `docker_freeradius_images_refresh` skips whenever `docker_ci_images_refresh` skips or fails,
# so the legs that read the 'freeradius4' images check both results.
# The legs must not read stale images after a skipped
# `docker_freeradius_images_refresh` that follows a failed `docker_ci_images_refresh`.
#
ci-multi-server:
needs: [docker_ci_images_refresh, docker_freeradius_images_refresh, ci_prev_run]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.docker_freeradius_images_refresh.result != 'failure'
&& needs.ci_prev_run.outputs.should_skip != 'true' }}
name: Multi-Server CI Tests
uses: ./.github/workflows/ci-multi-server-tests.yml
secrets: inherit
docker-crossbuild:
name: Docker crossbuild images
needs: [docker_ci_images_refresh, docker_freeradius_images_refresh, ci]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.docker_freeradius_images_refresh.result != 'failure'
&& needs.ci.result == 'success' && github.repository_owner == 'FreeRADIUS' }}
uses: ./.github/workflows/docker-crossbuild.yml
secrets: inherit
docker-service:
name: Docker service images
needs: [docker_ci_images_refresh, docker_freeradius_images_refresh, ci]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.docker_freeradius_images_refresh.result != 'failure'
&& needs.ci.result == 'success' && github.repository_owner == 'FreeRADIUS' }}
uses: ./.github/workflows/docker-service.yml
secrets: inherit
#
# `docs` only runs when the commit changes a file that the
# documentation build reads. `docs` is absent from the `needs` list of the `merge` job,
# because a failed documentation build should not hold up a merge.
#
docs:
name: Documentation
needs: [common_vars, docker_ci_images_refresh, ci_prev_run]
if: ${{ !cancelled() && needs.docker_ci_images_refresh.result != 'failure'
&& needs.ci_prev_run.outputs.should_skip != 'true'
&& needs.common_vars.outputs.docs_changed == 'true' }}
uses: ./.github/workflows/documentation.yml
secrets: inherit
#
# `merge` runs when every gating leg passed. The `needs` list holds only
# the gating legs. The other legs are too unreliable to block a
# merge on.
#
# By default GitHub skips a job unless every job in the job's `needs`
# succeeds. The docker refresh skips on commits that do not modify
# packaging. A skipped refresh would skip the merge step, unless the
# merge step carries an explicit condition.
#
# A skipped pipeline still merges. The duplicate check only reports
# a skip once an earlier run of the 'CI Pipeline' workflow finished
# successfully on the same tree. The merged content has therefore
# already passed. Requiring the legs to report `success` in the merge
# condition would strand a developer who amends a commit message and
# force pushes. The reworded commit carries the tree that just
# passed, so every leg skips.
#
merge:
name: Merge
needs: [ci_prev_run, ci, ci-deb, ci-rpm, ci-sanitizers]
if: ${{ !cancelled()
&& (needs.ci_prev_run.outputs.should_skip == 'true'
|| (needs.ci.result == 'success'
&& needs.ci-deb.result == 'success'
&& needs.ci-rpm.result == 'success'
&& needs.ci-sanitizers.result == 'success')) }}
uses: ./.github/workflows/merge-upstream.yml
secrets:
GH_APP_PRIVATE_KEY_CI_MERGE: ${{ secrets.GH_APP_PRIVATE_KEY_CI_MERGE }}