-
Notifications
You must be signed in to change notification settings - Fork 78
630 lines (570 loc) · 25.7 KB
/
ci.yml
File metadata and controls
630 lines (570 loc) · 25.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
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
name: CI
on:
push:
branches: [master]
tags: ['v*']
# Trigger-level paths filtering is intentionally NOT used: combined with the
# required `ci-pass` aggregator under Repository Rulesets, doc-only PRs would
# never spawn a workflow run, `ci-pass` would never report, and merge would
# be blocked indefinitely (admin override is also rejected by Rulesets).
# Instead, the `changes` job below detects what changed and gates heavy jobs
# via `needs: [changes]` + `if: needs.changes.outputs.code == 'true'`. The
# ci-pass aggregator still runs on doc-only PRs (its `if: always()` ensures
# the Rulesets gate is satisfied).
pull_request:
# Weekly NVD rescan: catches new CVEs landing on unchanged dependencies
# between releases. Also keeps the ~2GB NVD cache warm (7-day GitHub cache
# eviction). Monday 06:00 UTC = Sunday evening EST (low-traffic window).
schedule:
- cron: '0 6 * * 1'
workflow_call:
workflow_dispatch:
permissions:
contents: read
# Required by `dorny/paths-filter` on `pull_request` events to read changed
# files via the GitHub API (push events use git diff and don't need it).
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Detects which classes of files changed so heavy jobs can be skipped on
# doc-only PRs. Required because trigger-level `paths-ignore` would deadlock
# the `ci-pass` aggregator under Repository Rulesets — see the `on:` comment.
# Always runs (no `needs`) so that downstream `if:` expressions can read its
# outputs even when no code changed.
changes:
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
code: ${{ steps.outputs.outputs.code }}
e2e: ${{ steps.outputs.outputs.e2e }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Skipped under local act runs: act's default event payload lacks both
# `event.before` (push diff base) and `event.repository.default_branch`,
# both of which dorny/paths-filter needs to auto-detect a base ref. Real
# GitHub Actions populates these on push/pull_request/schedule events.
- id: filter
if: vars.ACT != 'true'
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
with:
# `code` fires on anything that isn't a doc / config-only change.
# `e2e` is a stricter subset — only KinD-relevant changes warrant
# the 10–15 min e2e job.
# CLAUDE.md is re-included so its edits go through the full gate.
filters: |
code:
- '!(**.md|docs/**|specs/**|LICENSE|.gitignore|.claudeignore|.claude/**|benchmarks/**|**.png|**.jpg|**.gif|**.svg)'
- 'CLAUDE.md'
e2e:
- 'k8s/**'
- 'pom.xml'
- '**/pom.xml'
- 'Makefile'
- '.github/workflows/ci.yml'
- 'e2e/**'
- '**/Dockerfile'
- '**/src/**'
# Final outputs:
# - under act, force `code`/`e2e` true so the full pipeline runs
# locally (act users want full exercise, not selective skipping);
# - on tag pushes, force them true too — a `v*` tag at master's HEAD
# yields an empty paths-filter diff, which would skip `cve-check`
# and cascade-skip the release `docker` job (green run, zero images
# published). A tag is a release and MUST run every job.
# Otherwise propagate the dorny/paths-filter result.
- id: outputs
run: |
if [ "${{ vars.ACT }}" = "true" ] || [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
echo "e2e=true" >> "$GITHUB_OUTPUT"
else
echo "code=${{ steps.filter.outputs.code }}" >> "$GITHUB_OUTPUT"
echo "e2e=${{ steps.filter.outputs.e2e }}" >> "$GITHUB_OUTPUT"
fi
static-check:
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install: true
cache: true
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository
key: maven-${{ hashFiles('**/pom.xml') }}
restore-keys: maven-
- name: Static check
run: make static-check
test:
needs: [changes, static-check]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install: true
cache: true
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository
key: maven-${{ hashFiles('**/pom.xml') }}
restore-keys: maven-
- name: Test with coverage
run: make coverage-generate
- name: Verify coverage threshold
run: make coverage-check
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
continue-on-error: true
with:
name: coverage-report
path: '**/target/site/jacoco/'
retention-days: 14
integration-test:
needs: [changes, static-check]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install: true
cache: true
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository
key: maven-${{ hashFiles('**/pom.xml') }}
restore-keys: maven-
- name: Integration test
run: make integration-test
- name: Upload Failsafe reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
continue-on-error: true
with:
name: failsafe-reports
path: '**/target/failsafe-reports/'
retention-days: 14
build:
needs: [changes, static-check]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install: true
cache: true
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository
key: maven-${{ hashFiles('**/pom.xml') }}
restore-keys: maven-
- name: Build
run: make build
# Upload on every push so the image-scan job (gates 1-3 on every push)
# and e2e job can reuse the JARs instead of rebuilding with Maven.
- name: Upload build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
continue-on-error: true
with:
name: service-jars
path: |
employee-service/target/*.jar
department-service/target/*.jar
organization-service/target/*.jar
gateway-service/target/*.jar
retention-days: 1
cve-check:
needs: [changes, static-check]
# Runs on:
# - push to master AND tag pushes (release-time gate — the docker job
# depends on it; master-push kept while OWASP step is continue-on-error
# pending NVD nanosecond-timestamp upstream fix — see CLAUDE.md #1c)
# - weekly schedule (catches new NVD advisories landing
# on unchanged deps between releases)
# - workflow_dispatch (ad-hoc rescan after a new CVE drop)
# Skipped under local act runs because the OWASP NVD download is slow.
# Also skipped on doc-only changes (changes.outputs.code == 'false').
if: |
vars.ACT != 'true' && needs.changes.outputs.code == 'true' && (
(github.event_name == 'push' && (
github.ref == 'refs/heads/master' ||
startsWith(github.ref, 'refs/tags/')
)) ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install: true
cache: true
- name: Cache NVD database
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository/org/owasp/dependency-check-data
key: nvd-db-${{ hashFiles('pom.xml') }}
restore-keys: nvd-db-
- name: Create Maven settings for OSS Index
run: make maven-settings-ossindex
# TEMPORARY: continue-on-error while upstream ships a fix for NVD's
# 2026-04-15 9-digit-nanosecond timestamp change. ODC 12.2.0 and 12.2.1
# fail to parse NVD's `timestamp` field, making `cve-check` red on
# every run. Upstream tracking: dependency-check/DependencyCheck#8424
# and jeremylong/open-vulnerability-clients#106. Remove this flag
# when ODC > 12.2.1 ships with the fix (Renovate bumps the plugin).
# Coverage is not lost — the `image-scan` job runs Trivy with CRITICAL/HIGH
# blocking on every push across the 4-service matrix.
- name: Run OWASP dependency check
continue-on-error: true
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
OSS_INDEX_USER: ${{ secrets.OSS_INDEX_USER }}
OSS_INDEX_TOKEN: ${{ secrets.OSS_INDEX_TOKEN }}
MAVEN_OPTS: '--add-modules jdk.incubator.vector'
run: make cve-check
- name: Upload CVE report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
continue-on-error: true
with:
name: cve-report
path: '**/target/dependency-check-report.html'
retention-days: 14
# Pre-tag Dockerfile validation running gates 1-3 (build + Trivy image scan
# + Spring Boot smoke test) on EVERY push. Catches base-image CVE regressions
# and Dockerfile breakages on the commit that introduced them, not on release
# day. The full tag-gated `docker` job below is a superset (adds multi-arch
# build + push + cosign) and is the matrix-exception to the consolidated
# `/ci-workflow` "single docker job" pattern: running the full 4-service
# multi-arch matrix on every push would be too expensive, so image-scan runs
# single-arch gates 1-3 on every push and docker runs the full pipeline on
# tag pushes only.
image-scan:
needs: [changes, build]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
service: [gateway, employee, organization, department]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Try the upload-artifact → download-artifact fast path first. Under
# `nektos/act` this reliably fails (cross-job artifact storage is not
# supported) so we fall back to rebuilding the JARs with Maven. The
# fallback keeps this job exercised by `make ci-run`.
- name: Download build artifacts
id: download-jars
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
continue-on-error: true
with:
name: service-jars
# Fallback rebuild path: only runs if download-artifact failed (local act runs).
- if: steps.download-jars.outcome == 'failure'
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install: true
cache: true
- name: Cache Maven dependencies
if: steps.download-jars.outcome == 'failure'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository
key: maven-${{ hashFiles('**/pom.xml') }}
restore-keys: maven-
- name: Build JARs
if: steps.download-jars.outcome == 'failure'
run: make build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build image for scan
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: ./${{ matrix.service }}-service
file: ./${{ matrix.service }}-service/Dockerfile
platforms: linux/amd64
load: true
tags: ${{ matrix.service }}:scan
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trivy image scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ matrix.service }}:scan
scanners: 'vuln,secret,misconfig'
severity: 'CRITICAL,HIGH'
exit-code: '1'
ignore-unfixed: true
- name: Smoke test
run: |
set -euo pipefail
SVC='${{ matrix.service }}'
docker run -d --name="${SVC}-smoke" "${SVC}:scan"
echo "Waiting up to 90s for Spring Boot to boot..."
end=$((SECONDS + 90))
while [ $SECONDS -lt $end ]; do
if docker logs "${SVC}-smoke" 2>&1 | grep -qE 'Started [A-Z][a-zA-Z]+Application in|Tomcat started on port|Netty started on port'; then
echo "PASS: ${SVC} container booted Spring Boot successfully"
docker rm -f "${SVC}-smoke" >/dev/null
exit 0
fi
sleep 3
done
echo "FAIL: ${SVC} did not reach Spring Boot start within 90s"
echo "--- container logs ---"
docker logs "${SVC}-smoke"
docker rm -f "${SVC}-smoke" >/dev/null || true
exit 1
# Validates the OCI-manifest-level Dockerfile contract (USER non-root,
# EXPOSE port, WORKDIR, ENTRYPOINT) with container-structure-test.
# Orthogonal to Trivy (CVE/secret/misconfig) and the smoke test (runtime
# boot) — catches a USER reset to root, a wrong port, a changed
# entrypoint. Runs on every push so Dockerfile-contract drift fails the
# commit that introduced it, not just the release.
- name: Container structure test
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$PWD/.container-structure-test.yaml:/test.yaml:ro" \
gcr.io/gcp-runtimes/container-structure-test:v1.16.0 \
test --image "${{ matrix.service }}:scan" --config /test.yaml
- name: Cleanup smoke container
if: always()
run: docker rm -f "${{ matrix.service }}-smoke" 2>/dev/null || true
# End-to-end test job (mandatory per /ci-workflow).
# Runs `make e2e` which cycles: kind-create → kind-setup (MongoDB) →
# kind-deploy (build+load images) → e2e-test script → kind-destroy.
# Skipped under local act runs — nested docker-in-docker (KinD cluster +
# cloud-provider-kind container on the `kind` network) does not work
# reliably inside act's runner containers. The job is exercised on
# GitHub Actions runners on every push/tag/PR.
e2e:
# Gated on a stricter filter than `code` so e2e runs only on changes that
# could affect KinD-deployed services (k8s manifests, code, build files,
# Dockerfiles, the e2e script itself, the CI workflow). Matches skill
# convention for the "what could break the cluster?" surface.
if: vars.ACT != 'true' && needs.changes.outputs.e2e == 'true'
needs: [changes, build, test]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install: true
cache: true
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository
key: maven-${{ hashFiles('**/pom.xml') }}
restore-keys: maven-
- name: Run end-to-end tests
run: make e2e
docker:
if: startsWith(github.ref, 'refs/tags/')
# Gate releases on the full pre-push hardening pipeline:
# build + test + cve-check (already passed) → image scan → smoke test → push (with provenance + SBOM) → cosign sign
needs: [build, test, cve-check]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write # GHCR push
id-token: write # cosign keyless OIDC signing (Sigstore Fulcio)
strategy:
matrix:
service: [gateway, employee, organization, department]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: service-jars
- name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Docker metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository }}/${{ matrix.service }}
# Convention: bare semver on Docker image tags, `v`-prefix on git tags.
# The `{{version}}` template strips the `v` from git tag `vX.Y.Z`
# automatically, matching how Docker Hub official images are tagged
# (mongo:8.0.20, node:24, postgres:17, nginx:1.27, alpine:3.20, etc.).
# Git tag stays `vX.Y.Z` (Go module / GitHub release convention).
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
flavor: |
latest=true
# ----------------------------------------------------------------
# GATE 1: Build single-arch image locally for security checks.
# `load: true` lands the image in the local Docker daemon so the
# next steps can scan/run it. Multi-arch + attestations cannot be
# combined with `load: true` per Docker's docs, so we build twice:
# once locally for gating, then again multi-arch for the push.
# The second build is ~95% cache-hit from this one (type=gha).
# ----------------------------------------------------------------
- name: Build image for scan
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: ./${{ matrix.service }}-service
file: ./${{ matrix.service }}-service/Dockerfile
platforms: linux/amd64
load: true
tags: ${{ matrix.service }}:scan
cache-from: type=gha
cache-to: type=gha,mode=max
# ----------------------------------------------------------------
# GATE 2: Trivy image scan — CRITICAL/HIGH blocks the release.
# Catches CVEs in the base image (eclipse-temurin:25-jre-noble)
# and any OS packages added during the build that filesystem-only
# scans (already done in the static-check job) cannot see.
# `ignore-unfixed: true` skips CVEs with no upstream fix yet —
# blocking on those creates churn without resolution.
# ----------------------------------------------------------------
- name: Trivy image scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ matrix.service }}:scan
scanners: 'vuln,secret,misconfig'
severity: 'CRITICAL,HIGH'
exit-code: '1'
ignore-unfixed: true
# ----------------------------------------------------------------
# GATE 3: Spring Boot boot-marker smoke test.
# Spring Boot services need MongoDB to pass /actuator/health, so
# a health-endpoint smoke test would always fail. Instead, watch
# the container logs for Spring Boot's standard "started" line.
# This proves the JAR is well-formed, JVM starts, Spring context
# boots, and embedded Tomcat begins listening — without needing
# any external dependencies.
# ----------------------------------------------------------------
- name: Smoke test
run: |
set -euo pipefail
SVC='${{ matrix.service }}'
docker run -d --name="${SVC}-smoke" "${SVC}:scan"
echo "Waiting up to 90s for Spring Boot to boot..."
end=$((SECONDS + 90))
while [ $SECONDS -lt $end ]; do
if docker logs "${SVC}-smoke" 2>&1 | grep -qE 'Started [A-Z][a-zA-Z]+Application in|Tomcat started on port|Netty started on port'; then
echo "PASS: ${SVC} container booted Spring Boot successfully"
docker rm -f "${SVC}-smoke" >/dev/null
exit 0
fi
sleep 3
done
echo "FAIL: ${SVC} did not reach Spring Boot start within 90s"
echo "--- container logs ---"
docker logs "${SVC}-smoke"
docker rm -f "${SVC}-smoke" >/dev/null || true
exit 1
# ----------------------------------------------------------------
# All gates passed → publish multi-arch.
# ----------------------------------------------------------------
- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: ./${{ matrix.service }}-service
file: ./${{ matrix.service }}-service/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# provenance + sbom set to false (Pattern A). Buildx's in-manifest
# attestations add per-platform `unknown/unknown` rows to the OCI
# index that GHCR's web UI renders as if they were real platforms —
# a confusing cosmetic cost with no upside unless a downstream
# consumer actually runs `cosign verify-attestation` / `trivy image
# --sbom-from registry`. Nothing does today, so the attestations
# were being produced and never read. Cosign keyless signing on the
# manifest digest below still proves origin. Switch BOTH back to
# `mode=max` / `true` (Pattern B) the moment a downstream verifier
# is wired in — the supply-chain attestations are still worth
# paying for, just not before there is a reader.
provenance: false
sbom: false
cache-from: type=gha
cache-to: type=gha,mode=max
# ----------------------------------------------------------------
# Post-push: cosign keyless signing.
# Signs the manifest digest with Sigstore via OIDC (no long-lived
# private keys). Consumers verify with:
# cosign verify ghcr.io/owner/repo/service:tag \
# --certificate-identity-regexp 'https://github\.com/.+' \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com
# Signing the digest covers all tags pointing to it (one signature
# per tag in the rekor record for clarity, but they all verify the
# same manifest).
# ----------------------------------------------------------------
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Sign image with cosign
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.push.outputs.digest }}
run: |
set -euo pipefail
echo "Signing manifest digest ${DIGEST} for tags:"
echo "${TAGS}"
while IFS= read -r tag; do
[ -z "$tag" ] && continue
echo "→ cosign sign ${tag}@${DIGEST}"
cosign sign --yes "${tag}@${DIGEST}"
done <<< "${TAGS}"
# Branch-protection aggregator. Single required status check — jobs can be
# added or renamed without touching Settings. Skipped jobs (cve-check under
# act / on non-push branches; e2e under act; docker on non-tag pushes) do NOT
# count as failure — only `failure` results trip the gate.
ci-pass:
if: always()
needs: [changes, static-check, test, integration-test, build, cve-check, image-scan, e2e, docker]
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Verify all jobs passed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more jobs were cancelled"
exit 1
fi
echo "All required jobs passed or were skipped."