Repository navigation
615 lines (600 loc) · 31.7 KB
/
Copy pathexamples.yml
File metadata and controls
615 lines (600 loc) · 31.7 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
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
name: Examples
on:
# Once per change: on the PR, and on main after the merge.
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
# A new push to a PR supersedes the previous run; main runs are never cancelled.
concurrency:
group: examples-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Works out which example projects the change touches. The workflow itself
# always runs, so the required `examples-ok` summary job always reports: a
# docs-only PR skips every build job and still goes green.
#
# Each output is a JSON array of project directories (or files, for compose
# and scripts). Anything under examples/_infra/, this workflow, or
# scripts/ci_changed_projects.py selects everything, as do workflow_dispatch
# and a base that cannot be diffed. Run the same script locally to see a
# selection: scripts/ci_changed_projects.py --base origin/main
changes:
runs-on: ubuntu-latest
outputs:
jvm: ${{ steps.select.outputs.jvm }}
dotnet: ${{ steps.select.outputs.dotnet }}
go: ${{ steps.select.outputs.go }}
python: ${{ steps.select.outputs.python }}
cpp: ${{ steps.select.outputs.cpp }}
compose: ${{ steps.select.outputs.compose }}
scripts: ${{ steps.select.outputs.scripts }}
k8s: ${{ steps.select.outputs.k8s }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: select
env:
EVENT: ${{ github.event_name }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE: ${{ github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
base="${PR_BASE:-$PUSH_BEFORE}"
python3 scripts/ci_changed_projects.py \
--event "$EVENT" --base "$base" --head "$HEAD_SHA" --github-output
jvm:
name: Maven — ${{ matrix.dir }}
needs: changes
if: needs.changes.outputs.jvm != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.changes.outputs.jvm) }}
steps:
- uses: actions/checkout@v7
# Spring Boot and Quarkus poms both target Java 25.
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "25"
# Cache policy (see the cache note above the cpp job): legs only restore. The one shared
# key is written by the `warm-maven` job on main.
- name: Restore the Maven repository
uses: actions/cache/restore@v6.1.0
with:
path: ~/.m2/repository
key: maven-${{ runner.os }}-${{ hashFiles('examples/**/pom.xml') }}
restore-keys: |
maven-${{ runner.os }}-
- name: Build
env:
DIR: ${{ matrix.dir }}
MAVEN_ARGS: -Daether.transport.http.retryHandler.count=8 -Daether.transport.http.retryHandler.interval=5000 -Daether.transport.http.retryHandler.serviceUnavailable=429,503
run: |
test -f "$DIR/pom.xml" || { echo "::error::no pom.xml in $DIR"; exit 1; }
mvn -B -ntp -q -f "$DIR/pom.xml" package -DskipTests
dotnet:
name: .NET — ${{ matrix.dir }}
needs: changes
if: needs.changes.outputs.dotnet != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.changes.outputs.dotnet) }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- name: Build
working-directory: ${{ matrix.dir }}
run: |
ls ./*.csproj >/dev/null || { echo "::error::no .csproj in ${{ matrix.dir }}"; exit 1; }
dotnet build -c Release
go:
name: Go — ${{ matrix.dir }}
needs: changes
if: needs.changes.outputs.go != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.changes.outputs.go) }}
steps:
- uses: actions/checkout@v7
- name: Require go.mod
run: test -f "${{ matrix.dir }}/go.mod" || { echo "::error::no go.mod in ${{ matrix.dir }}"; exit 1; }
- uses: actions/setup-go@v7
with:
go-version-file: ${{ matrix.dir }}/go.mod
cache: false
# Cache policy (see the cache note above the cpp job): legs only restore. The one shared
# key is written by the `warm-go` job on main.
- name: Restore the Go module cache
uses: actions/cache/restore@v6.1.0
with:
path: ~/go/pkg/mod
key: go-${{ runner.os }}-${{ hashFiles('examples/**/go.sum') }}
restore-keys: |
go-${{ runner.os }}-
- name: Vet and build
working-directory: ${{ matrix.dir }}
run: go vet ./... && go build -o /dev/null ./... && go test ./...
python:
name: Python — ${{ matrix.dir }}
needs: changes
if: needs.changes.outputs.python != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.changes.outputs.python) }}
steps:
- uses: actions/checkout@v7
# uv pinned to the version the Containerfiles install. Legs do not use the setup-uv cache: the
# cache policy below (restore everywhere, write only on main) has no entry for it, and every
# dependency set here is small.
- uses: astral-sh/setup-uv@v10.3.0
with:
version: "0.13.0"
python-version: "3.14"
enable-cache: false
- name: Install and byte-compile
working-directory: ${{ matrix.dir }}
env:
UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/venv
run: |
test -f pyproject.toml || { echo "::error::no pyproject.toml in ${{ matrix.dir }}"; exit 1; }
test -f uv.lock || { echo "::error::no uv.lock in ${{ matrix.dir }}"; exit 1; }
uv sync --locked --no-dev
uv run --no-sync python -m compileall -q .
# Cache policy. The repository gets 10 GB of Actions cache, shared by every branch. The rule:
# every run RESTORES, only a push to main WRITES, and every ecosystem keeps a small, fixed
# number of entries, whatever the number of projects.
# - Maven and Go: ONE shared entry each, keyed on the hash of every pom.xml / go.sum under
# examples/. The build legs only restore it (actions/cache/restore). The `warm-maven` /
# `warm-go` jobs on main resolves the dependencies of ALL projects once and saves it, then deletes
# the superseded entries. A leg cannot be the writer: it downloads only its own
# dependencies, so whichever leg won the save would freeze a cache holding one project.
# Tradeoff: any pom/go.sum change on main re-warms everything (Maven: 62 builds,
# Go: `go mod download` x31), which costs minutes of main-only CI time and never delays
# a pull request. Go keeps only the module cache; the build cache is not worth its bytes.
# - Conan: one entry per distinct conan.lock (22 of 31), pruned of sources, build trees and
# downloads (about 90% of the bytes; only the built packages are kept) before main saves
# it. The per-lock entries stay small enough to fit; one shared entry would need a warm
# job that compiles all 22 dependency trees (hours), or a single leg's packages would
# stand in for everyone's.
# - BuildKit image layers (type=gha, mode=min): cache-from always, cache-to on main only.
# - Sizing, measured with `gh api repos/.../actions/caches` before this change: unpruned
# Conan entries 210-668 MB (avg 408 MB, 20 entries); pruned is ~10% (25-caching/cpp:
# 1.6 GB -> 163 MB), budgeted here at 20% = 135 MB for the largest. Per-pom Maven entries
# were 56-350 MB and per-go.sum Go entries 22-248 MB (avg 111 MB), so those two are
# what outgrew the quota, not Conan. Steady state on main:
# Conan 22 x <=135 MB (typical 80) <= 3.0 GB
# Maven 1 shared union 0.45 GB (measured: 446 MB)
# Go 1 shared module cache 0.22 GB (measured: 216 MB)
# layers 27 blobs = 200 MB measured 0.25 GB
# Pages bundler cache <= 0.05 GB (estimate)
# typical 22 x 80 MB + 0.45 + 0.22 + 0.25 + 0.05 = ~2.7 GB; bound ~3.9 GB.
# Peak ~7 GB: a Conan key bump or bulk lock change leaves the superseded per-lock
# entries in place until they age out, and layer blobs stay 7 days unused.
# Measured on main 2026-10-15 (`gh cache list --key`): Conan 27 entries = 2.16 GB
# (5 superseded after the S14 lock changes), Maven 446 MB, Go 216 MB; 3.46 GB in all.
# Partly measured: the prune ratio (one project, 25-caching/cpp). Estimated: Pages.
# PR refs hold nothing.
# - Hygiene: .github/workflows/cache-cleanup.yml deletes the caches of a PR ref when the
# PR closes; CLAUDE.md ("CI cache budget") has the commands for inspecting and clearing.
#
# Each C++ service builds in its own Containerfile (Conan 2.33.0 + GCC 15 / C++23 on
# UBI 10). The expensive part is compiling the Conan dependency tree (gRPC, Drogon,
# OpenTelemetry C++, libpq...): 30+ minutes cold for the largest projects on 4 CPUs. Its
# home is a BuildKit cache mount (id cndp-conan2), which neither the runner nor
# type=gha carries to the next job, so the cache is round-tripped by hand:
# 1. actions/cache restores the directory `conan-cache`, keyed
# `<OS>-conan2-ubi10.2-gcc15-conan2.33.0-<hash of the project's conan.lock>`
# (for example Linux-conan2-ubi10.2-gcc15-conan2.33.0-b4900...). A lock change misses
# the exact key and falls back to the newest cache of the same toolchain
# (restore-keys), so only the changed packages recompile. On a PR this is
# actions/cache/restore (nothing is saved); on main it is actions/cache, whose post
# step saves the directory when the exact key missed.
# 2. buildkit-cache-dance injects that directory into the cache mount before the build.
# Its post step extracts the mount afterwards, before the cache post step saves it;
# extraction is skipped everywhere except a push to main with an exact-key miss.
# 3. Just before that extraction, on main with a miss, a throwaway build prunes the mount
# and fails the job if no built package is left, so an empty cache is never saved.
# On a main miss the builder stage is forced to run (no-cache-filters): the type=gha layer
# cache can otherwise satisfy the stage from cache, BuildKit then skips the
# `conan install` RUN, the cache mount stays empty and an empty Conan cache would be saved
# under the exact key until the next conan.lock change.
# Bump the `conan2-ubi10.2-gcc15-conan2.33.0` part of CONAN_KEY below when the
# Containerfile base image, GCC or Conan version changes.
# The build context is the service directory, or its parent cpp/ directory
# for the multi-service examples; scripts/ci_changed_projects.py cpp-context
# decides, mirroring each example's compose.yaml.
cpp:
name: C++ — ${{ matrix.dir }}
needs: changes
if: needs.changes.outputs.cpp != '[]'
runs-on: ubuntu-latest
timeout-minutes: 120
env:
# Toolchain part of the Conan cache key; the runner OS is added per step.
CONAN_KEY: conan2-ubi10.2-gcc15-conan2.33.0
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.changes.outputs.cpp) }}
steps:
- uses: actions/checkout@v7
- name: Locate Containerfile and build context
id: ctx
env:
DIR: ${{ matrix.dir }}
run: |
test -f "$DIR/conanfile.py" || { echo "::error::no conanfile.py in $DIR"; exit 1; }
test -f "$DIR/Containerfile" || { echo "::error::no Containerfile in $DIR"; exit 1; }
echo "context=$(python3 scripts/ci_changed_projects.py cpp-context "$DIR")" >> "$GITHUB_OUTPUT"
echo "scope=cpp-${DIR//\//-}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v4
id: buildx
- name: Restore the Conan cache (pull request)
id: conan-restore
if: github.ref != 'refs/heads/main'
uses: actions/cache/restore@v6.1.0
with:
path: conan-cache
key: ${{ runner.os }}-${{ env.CONAN_KEY }}-${{ hashFiles(format('{0}/conan.lock', matrix.dir)) }}
restore-keys: |
${{ runner.os }}-${{ env.CONAN_KEY }}-
- name: Restore the Conan cache (main; saved after the build on a miss)
id: conan-main
if: github.ref == 'refs/heads/main'
uses: actions/cache@v6.1.0
with:
path: conan-cache
key: ${{ runner.os }}-${{ env.CONAN_KEY }}-${{ hashFiles(format('{0}/conan.lock', matrix.dir)) }}
restore-keys: |
${{ runner.os }}-${{ env.CONAN_KEY }}-
- name: Inject the Conan cache into the BuildKit cache mount
uses: reproducible-containers/buildkit-cache-dance@v3.4.0
with:
builder: ${{ steps.buildx.outputs.name }}
cache-map: |
{
"conan-cache": { "target": "/root/.conan2", "id": "cndp-conan2" }
}
skip-extraction: ${{ github.ref != 'refs/heads/main' || steps.conan-main.outputs.cache-hit == 'true' }}
# Exact tag (newest published on ghcr; the action's default is :latest).
utility-image: ghcr.io/containerd/busybox:1.36@sha256:7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c
- uses: docker/build-push-action@v7
with:
context: ${{ steps.ctx.outputs.context }}
file: ${{ matrix.dir }}/Containerfile
push: false
cache-from: type=gha,scope=${{ steps.ctx.outputs.scope }}
cache-to: ${{ github.ref == 'refs/heads/main' && format('type=gha,mode=min,scope={0}', steps.ctx.outputs.scope) || '' }}
# Every Containerfile has a `builder` stage (two also run conan in `schemagen`).
no-cache-filters: ${{ github.ref == 'refs/heads/main' && steps.conan-main.outputs.cache-hit != 'true' && 'builder,schemagen' || '' }}
- name: Prune the Conan cache mount before it is extracted and saved
if: github.ref == 'refs/heads/main' && steps.conan-main.outputs.cache-hit != 'true'
env:
BUILDER: ${{ steps.buildx.outputs.name }}
run: |
# Conan keeps build trees (p/b/<id>/b), recipe sources (p/<id>/s) and download
# archives (p/*/d) next to the built packages (p/b/<id>/p): about 90% of the bytes.
# A rebuild fetches them again, so only the packages are worth saving. Measured on
# 25-caching/cpp: 1.6 GB -> 163 MB, and the next conan install is all cache hits.
# The RUN fails (and so does the job) when no built package (p/b/<id>/p) is left, so
# an empty cache is never extracted and saved. On 25-caching/cpp 13 are left.
# The STAMP build arg keeps BuildKit from reusing a cached RUN; --no-cache would
# not do, because it gives the RUN an empty cache mount instead of the real one.
empty="$(mktemp -d)"
docker buildx build --builder "$BUILDER" --progress=plain \
--build-arg "STAMP=$(date +%s%N)" -f - "$empty" <<'EOF'
FROM registry.access.redhat.com/ubi10/ubi-minimal:10.2-1791444377
ARG STAMP
RUN --mount=type=cache,id=cndp-conan2,target=/c,sharing=locked \
echo "$STAMP" >/dev/null && du -sh /c \
&& find /c/p -mindepth 2 -maxdepth 3 -type d \( -name b -o -name s -o -name d \) -prune -exec rm -rf {} + \
&& du -sh /c \
&& n="$(find /c/p/b -mindepth 2 -maxdepth 2 -type d -name p 2>/dev/null | wc -l)" \
&& echo "built packages left: $n" \
&& [ "$n" -gt 0 ]
EOF
# Writes the shared Maven and Go caches (see the cache note above the cpp job). Main only,
# one job per ecosystem, so a slow or flaky warm-up never touches a pull request. A failed
# warm-up fails the run and saves nothing (the save is the last step); the restore of the
# previous entry plus the exact-key check make a successful run a no-op until a pom.xml or
# go.sum changes. Not part of `examples-ok`: PRs never run it.
warm-maven:
name: Warm the Maven cache
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 90
concurrency:
group: warm-maven-${{ github.ref }}
cancel-in-progress: false
permissions:
actions: write
contents: read
env:
MAVEN_ARGS: -Daether.transport.http.retryHandler.count=8 -Daether.transport.http.retryHandler.interval=5000 -Daether.transport.http.retryHandler.serviceUnavailable=429,503
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
# Exact-key probe: on a hit nothing below runs.
- id: exact
uses: actions/cache/restore@v6.1.0
with:
path: ~/.m2/repository
key: maven-${{ runner.os }}-${{ hashFiles('examples/**/pom.xml') }}
lookup-only: true
# Miss: start from the newest older entry so the warm-up only adds what changed.
- id: m2
if: steps.exact.outputs.cache-hit != 'true'
uses: actions/cache/restore@v6.1.0
with:
path: ~/.m2/repository
key: maven-${{ runner.os }}-${{ hashFiles('examples/**/pom.xml') }}
restore-keys: |
maven-${{ runner.os }}-
- if: steps.exact.outputs.cache-hit != 'true'
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "25"
- name: Resolve the dependencies of every Maven project
if: steps.exact.outputs.cache-hit != 'true'
run: |
failed=0
while IFS= read -r dir; do
echo "::group::$dir"
mvn -B -ntp -q -f "$dir/pom.xml" package -DskipTests || { echo "::error::mvn failed in $dir"; failed=1; }
echo "::endgroup::"
done < <(python3 scripts/ci_changed_projects.py --all | jq -r '.jvm[]')
exit "$failed"
- name: Save the Maven repository
if: steps.exact.outputs.cache-hit != 'true'
uses: actions/cache/save@v6.1.0
with:
path: ~/.m2/repository
key: ${{ steps.m2.outputs.cache-primary-key }}
# actions/cache/save only warns when the save fails, so confirm the new entry exists
# before removing the old ones. `gh cache list --key` matches by key prefix.
- name: Delete the superseded Maven entries
if: steps.exact.outputs.cache-hit != 'true'
env:
KEEP: ${{ steps.m2.outputs.cache-primary-key }}
run: |
gh cache list --key "$KEEP" --limit 1000 --json key --jq '.[].key' | grep -qx "$KEEP" \
|| { echo "::warning::$KEEP not saved; keeping old entries"; exit 0; }
gh cache list --key "maven-$RUNNER_OS-" --limit 1000 --json id,key \
--jq ".[] | select(.key != \"$KEEP\") | .id" | xargs -r -n1 gh cache delete || true
warm-go:
name: Warm the Go cache
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: warm-go-${{ github.ref }}
cancel-in-progress: false
permissions:
actions: write
contents: read
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
# Exact-key probe: on a hit nothing below runs.
- id: exact
uses: actions/cache/restore@v6.1.0
with:
path: ~/go/pkg/mod
key: go-${{ runner.os }}-${{ hashFiles('examples/**/go.sum') }}
lookup-only: true
- id: gomod
if: steps.exact.outputs.cache-hit != 'true'
uses: actions/cache/restore@v6.1.0
with:
path: ~/go/pkg/mod
key: go-${{ runner.os }}-${{ hashFiles('examples/**/go.sum') }}
restore-keys: |
go-${{ runner.os }}-
# Same toolchain as the build legs (every go.mod says go 1.27.2).
- if: steps.exact.outputs.cache-hit != 'true'
uses: actions/setup-go@v7
with:
go-version-file: examples/01-cloud-native-principles/go/go.mod
cache: false
- name: Download the modules of every Go project
if: steps.exact.outputs.cache-hit != 'true'
run: |
failed=0
while IFS= read -r dir; do
(cd "$dir" && go mod download) || { echo "::error::go mod download failed in $dir"; failed=1; }
done < <(python3 scripts/ci_changed_projects.py --all | jq -r '.go[]')
exit "$failed"
- name: Save the Go module cache
if: steps.exact.outputs.cache-hit != 'true'
uses: actions/cache/save@v6.1.0
with:
path: ~/go/pkg/mod
key: ${{ steps.gomod.outputs.cache-primary-key }}
- name: Delete the superseded Go entries
if: steps.exact.outputs.cache-hit != 'true'
env:
KEEP: ${{ steps.gomod.outputs.cache-primary-key }}
run: |
gh cache list --key "$KEEP" --limit 1000 --json key --jq '.[].key' | grep -qx "$KEEP" \
|| { echo "::warning::$KEEP not saved; keeping old entries"; exit 0; }
gh cache list --key "go-$RUNNER_OS-" --limit 1000 --json id,key \
--jq ".[] | select(.key != \"$KEEP\") | .id" | xargs -r -n1 gh cache delete || true
# Compose files and verify.sh scripts are cheap to check, so each kind is one
# job that loops over its selection and reports every failure at the end.
compose:
name: Compose config
needs: changes
if: needs.changes.outputs.compose != '[]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The runner's bundled compose is older and rejects overriding a service
# brought in with include: ("conflicts with imported resource"). Pin the
# newest stable compose plugin (v5.6.0) and verify its checksum instead.
- name: Install docker compose v5.6.0
env:
COMPOSE_VERSION: v5.6.0
run: |
base="https://github.com/docker/compose/releases/download/$COMPOSE_VERSION"
mkdir -p "$HOME/.docker/cli-plugins" && cd "$HOME/.docker/cli-plugins"
curl -fsSLO "$base/docker-compose-linux-x86_64"
curl -fsSL "$base/docker-compose-linux-x86_64.sha256" | sha256sum -c -
mv docker-compose-linux-x86_64 docker-compose && chmod +x docker-compose
docker compose version
- name: docker compose config -q
env:
FILES: ${{ needs.changes.outputs.compose }}
run: |
failed=0
while IFS= read -r f; do
if [ ! -f "$f" ]; then echo "::error file=$f::compose file not found"; failed=1; continue; fi
# include: and env_file paths resolve relative to the file.
if ! (cd "$(dirname "$f")" && docker compose -f "$(basename "$f")" config -q); then
echo "::error file=$f::docker compose config failed"; failed=1
fi
done < <(jq -r '.[]' <<<"$FILES")
exit "$failed"
scripts:
name: Shell scripts
needs: changes
if: needs.changes.outputs.scripts != '[]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: bash -n and shellcheck
env:
FILES: ${{ needs.changes.outputs.scripts }}
run: |
failed=0
while IFS= read -r f; do
if [ ! -f "$f" ]; then echo "::error file=$f::script not found"; failed=1; continue; fi
bash -n "$f" || { echo "::error file=$f::bash -n failed"; failed=1; }
shellcheck --severity=error "$f" || { echo "::error file=$f::shellcheck failed"; failed=1; }
done < <(jq -r '.[]' <<<"$FILES")
exit "$failed"
# Kubernetes and OpenShift manifests: render the infrastructure and every language overlay, validate
# against the Kubernetes schemas (and the Strimzi, OLM and OpenShift ones), and assert the conventions
# the minikube (k8s/) and OpenShift Local (openshift/) runs rely on. No cluster is needed.
# The selector picks examples/<NN>/k8s and examples/<NN>/openshift; a k8s/ change picks both.
k8s:
name: Kubernetes and OpenShift manifests
needs: changes
if: needs.changes.outputs.k8s != '[]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The runner's kubectl is not pinned; install the version the minikube cluster runs.
- name: Install kubectl v1.36.5 and kubeconform v0.8.0
env:
KUBECTL_SHA256: 33bc88a24c3b09cf55bfd59e6ca977881b61fc3f25ab65bc80c65b430248648e
run: |
mkdir -p "$HOME/.local/bin" && cd "$RUNNER_TEMP"
curl -fsSLO https://dl.k8s.io/release/v1.36.5/bin/linux/amd64/kubectl
echo "$KUBECTL_SHA256 kubectl" | sha256sum -c -
install -m 0755 kubectl "$HOME/.local/bin/kubectl"
base=https://github.com/yannh/kubeconform/releases/download/v0.8.0
curl -fsSLO "$base/kubeconform-linux-amd64.tar.gz"
curl -fsSL "$base/CHECKSUMS" | grep ' kubeconform-linux-amd64.tar.gz$' | sha256sum -c -
tar -xzf kubeconform-linux-amd64.tar.gz kubeconform
install -m 0755 kubeconform "$HOME/.local/bin/kubeconform"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Render, validate and assert
env:
DIRS: ${{ needs.changes.outputs.k8s }}
# OpenShift 4.22 runs Kubernetes 1.35. To confirm against `oc version` in the live run.
OCP_KUBE_VERSION: 1.35.6
# datreeio/CRDs-catalog pinned to a commit; it carries the Strimzi and OLM schemas and, under
# openshift/v4.15-strict, the Route and ImageStream ones (a different file name layout).
CRDS: https://raw.githubusercontent.com/datreeio/CRDs-catalog/63669a570e231d4f1f8396d229a1de512bcf0a34/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
OCP_CRDS: https://raw.githubusercontent.com/datreeio/CRDs-catalog/63669a570e231d4f1f8396d229a1de512bcf0a34/openshift/v4.15-strict/{{.ResourceKind}}_{{.Group}}_{{.ResourceAPIVersion}}.json
run: |
failed=0
while IFS= read -r dir; do
[ -d "$dir" ] || { echo "::error::$dir not found"; failed=1; continue; }
kubeconform -v
flavor="$(basename "$dir")"
if [ "$flavor" = openshift ]; then
kver="$OCP_KUBE_VERSION"
targets=("$dir/operators" "$dir/infra")
# Exactly the five languages that run on OpenShift Local (no C++), none of them extra.
langs="$(for o in "$dir"/overlays/*/; do basename "$o"; done | sort | tr '\n' ' ')"
[ "$langs" = "dotnet go python quarkus spring-boot " ] || { echo "::error::$dir/overlays must be dotnet go python quarkus spring-boot, found: $langs"; failed=1; }
# Credentials and cluster access: no token or password handling the plan forbids, in code (comments excluded).
if grep -rhvE '^\s*#' "$dir"/*.sh | grep -nE 'use-context|oc adm policy|port-forward|skopeo login|--dest-creds|--dest-registry-token|--tls-verify=false|--insecure|curl[^|]* -k |crc delete'; then
echo "::error::$dir scripts use a forbidden command"; failed=1
fi
else
kver=1.36.5
kubeconform -strict -summary -kubernetes-version "$kver" -schema-location default -schema-location "$CRDS" "$dir/namespace.yaml" || failed=1
targets=("$dir/infra")
fi
for o in "$dir"/overlays/*/; do targets+=("${o%/}"); done
for t in "${targets[@]}"; do
echo "::group::$t"
# infra reads the shared schema from ../../db/init and openshift/infra the k8s files, outside its directory.
kubectl kustomize --load-restrictor LoadRestrictionsNone "$t" > "$RUNNER_TEMP/render.yaml" || { echo "::error::render failed: $t"; failed=1; echo "::endgroup::"; continue; }
kubeconform -strict -summary -kubernetes-version "$kver" -schema-location default -schema-location "$CRDS" -schema-location "$OCP_CRDS" "$RUNNER_TEMP/render.yaml" || { echo "::error::kubeconform failed: $t"; failed=1; }
if grep -nE 'image: .*:latest|:latest$' "$RUNNER_TEMP/render.yaml"; then echo "::error::$t uses :latest"; failed=1; fi
if [ "$flavor" = openshift ]; then
if grep -nE 'nodePort|runAsUser|fsGroup|anyuid' "$RUNNER_TEMP/render.yaml"; then echo "::error::$t has a nodePort, a fixed UID or group, or anyuid"; failed=1; fi
case "$t" in
*/operators)
grep -q 'installPlanApproval: Manual' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t Subscription must use installPlanApproval: Manual"; failed=1; }
grep -qE 'startingCSV: amqstreams\.v[0-9.]+-[0-9]+' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t Subscription must pin startingCSV"; failed=1; }
grep -q 'targetNamespaces' "$RUNNER_TEMP/render.yaml" && { echo "::error::$t OperatorGroup must be AllNamespaces"; failed=1; } ;;
*/overlays/*)
lang="$(basename "$t")"
reg=image-registry.openshift-image-registry.svc:5000/cndp-05
[ "$(grep -cE "image: ${reg}/${lang}-[a-z-]+:v1$" "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must use 3 images ${reg}/${lang}-<svc>:v1"; failed=1; }
[ "$(grep -c 'imagePullPolicy: Always' "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must set imagePullPolicy: Always on 3 containers"; failed=1; }
if ! { grep -q 'kind: Route' "$RUNNER_TEMP/render.yaml" && grep -q 'termination: edge' "$RUNNER_TEMP/render.yaml" && grep -q 'insecureEdgeTerminationPolicy: Redirect' "$RUNNER_TEMP/render.yaml"; }; then echo "::error::$t lacks the edge Route with Redirect"; failed=1; fi ;;
*/infra)
if ! { grep -q 'kind: Route' "$RUNNER_TEMP/render.yaml" && grep -q 'insecureEdgeTerminationPolicy: Redirect' "$RUNNER_TEMP/render.yaml"; }; then echo "::error::$t lacks the Grafana Route"; failed=1; fi
grep -q 'version: 4.2.0' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t Kafka must run version 4.2.0 (the AMQ Streams bundle)"; failed=1; }
grep -vE 'image: (docker.io|quay.io|registry.redhat.io)/' <(grep -E '^\s+image: ' "$RUNNER_TEMP/render.yaml") && { echo "::error::$t has an image that is not fully qualified"; failed=1; } ;;
esac
else
case "$t" in
*/overlays/*)
lang="$(basename "$t")"
[ "$(grep -cE "image: localhost/cndp-05/${lang}-[a-z-]+:v1$" "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must use 3 images localhost/cndp-05/${lang}-<svc>:v1"; failed=1; }
[ "$(grep -c 'imagePullPolicy: Never' "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must set imagePullPolicy: Never on 3 containers"; failed=1; }
grep -q 'nodePort: 30080' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t lacks nodePort 30080"; failed=1; } ;;
*/infra)
grep -q 'nodePort: 30300' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t lacks nodePort 30300"; failed=1; }
grep -vE 'image: (docker.io|quay.io)/' <(grep -E '^\s+image: ' "$RUNNER_TEMP/render.yaml") && { echo "::error::$t has an image that is not fully qualified"; failed=1; } ;;
esac
fi
echo "::endgroup::"
done
done < <(jq -r '.[]' <<<"$DIRS")
exit "$failed"
# The one check branch protection requires: passes when every job above
# succeeded or was skipped by `changes`, fails otherwise.
examples-ok:
name: examples-ok
if: always()
needs: [changes, jvm, dotnet, go, python, cpp, compose, scripts, k8s]
runs-on: ubuntu-latest
steps:
- env:
NEEDS: ${{ toJSON(needs) }}
run: jq -e 'to_entries | all(.value.result == "success" or .value.result == "skipped")' <<<"$NEEDS"