Repository navigation
839 lines (783 loc) · 32.9 KB
/
Copy pathrelease.yml
File metadata and controls
839 lines (783 loc) · 32.9 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
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
name: Release
# Each architecture is built on a native runner (ubuntu-24.04 for amd64,
# ubuntu-24.04-arm for arm64) from that architecture's nesso-build-env image.
# There is no QEMU. BuildKit attestations are off so the pushed arch image is
# one platform manifest. scripts/publish-runtime-index merges those digests
# into one manifest list and tags it vX.Y.Z, X.Y.Z, and latest. The arch tags
# vX.Y.Z-amd64 and vX.Y.Z-arm64 stay so each platform digest remains
# addressable; they are not the tags users pull.
#
# workflow_dispatch builds, gates, packages, and uploads workflow artifacts.
# It does not run approve-release. The release environment allows only v*
# tags, so that job on a branch would fail the run before any step. Dispatch
# still runs verify-package, image (build, scan, determinism, smoke, and
# workflow artifacts), and subjects. Push, cosign, provenance, and the
# GitHub Release stay off, including when the dispatch ref is a tag: those
# steps and jobs run only after approve-release succeeds, and that job runs
# only on a tag push.
#
# A tag push runs approve-release after verify-package. The job uses the
# release environment and an empty token. The reviewer approves once the
# package gate is green, and a failed package check skips the approval.
# image needs that job. image is what pushes each arch image, so the push
# cannot start earlier. index, sign, the SLSA jobs, and release need
# approve-release too.
#
# The image jobs wait on Package (.github/workflows/package.yml). That
# workflow builds each .deb twice and installs it on Ubuntu 26.04 and 26.10
# for both architectures. A failed check does not push the image or publish
# the GitHub Release. Package also runs on its own nightly schedule and when
# its packaging files change.
#
# Tag releases sign ghcr.io/<owner>/nesso with keyless cosign (cosign 2.6.1).
# `cosign verify` on a release tag resolves the tag to the manifest-list
# digest, so that index is signed. A pin to one platform digest does not
# inherit the index signature; each platform digest is signed too.
# SLSA Build Level 3 provenance comes from slsa-github-generator v2.1.0:
# the generic generator for both .debs and both SPDX SBOMs, the container
# generator for the index digest and each platform digest. Those reusable
# workflows are referenced by tag. They verify their own ref and reject a
# commit SHA. Every other action stays pinned by SHA.
#
# step-security/harden-runner v2.22.2 (egress-policy: audit) is the first
# step of jobs that have steps. verify-package is the Package workflow.
# The SLSA generator jobs are reusable workflows that check their own tag
# and do not accept an added step. verify-package passes scan_runtime: false
# so Package does not scan again. Package's scan-runtime job still declares
# security-events: write, and GitHub checks nested permissions at startup,
# before if, so this caller grants that scope. The image job scans the
# runtime image it built, before the push, and uploads that SARIF.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: Semver without v; must match conanfile.py version (build and smoke only)
required: true
type: string
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
REGISTRY: ghcr.io
CONAN_HOME: /opt/conan2
permissions: {}
jobs:
verify-package:
uses: ./.github/workflows/package.yml # zizmor: ignore[self-repository] same-commit reusable workflow; actionlint 1.7.12 rejects the self-repository form
with:
scan_runtime: false
permissions:
contents: read
packages: read
actions: write
security-events: write
# Empty token on purpose. The environment is the gate. A branch dispatch
# must not select it: the v* deployment rule would reject the run.
approve-release:
needs: verify-package
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && needs.verify-package.result == 'success'
runs-on: ubuntu-latest
environment: release
timeout-minutes: 15
permissions: {}
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Record approval
run: echo "release environment approved"
image:
name: image (${{ matrix.arch }})
needs: [verify-package, approve-release]
# success() is false when approve-release is skipped, which would drop
# the dry run. Dispatch still builds. A tag push waits for approval.
if: ${{ !cancelled() && needs.verify-package.result == 'success' && (github.event_name == 'workflow_dispatch' || needs.approve-release.result == 'success') }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
permissions:
contents: read
packages: write
actions: write
security-events: write
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
env:
ARCH: ${{ matrix.arch }}
PLATFORM: linux/${{ matrix.arch }}
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Require the native architecture
run: |
set -euo pipefail
case "${ARCH}:$(uname -m)" in
amd64:x86_64|arm64:aarch64) ;;
*)
echo "error: refusing to build ${ARCH} on $(uname -m)" >&2
exit 1
;;
esac
- name: Resolve version
id: ver
env:
REF: ${{ github.ref }}
REF_NAME: ${{ github.ref_name }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [[ "${REF}" == refs/tags/* ]]; then
tag="${REF_NAME}"
if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "error: tag must be vX.Y.Z, got ${tag}" >&2
exit 1
fi
ver="${tag#v}"
else
ver="${INPUT_VERSION}"
if [[ ! "${ver}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "error: version must be X.Y.Z, got ${ver}" >&2
exit 1
fi
fi
chmod +x scripts/check-versions
scripts/check-versions "${ver}"
epoch="$(git log -1 --format=%ct)"
if [[ ! "${epoch}" =~ ^[0-9]+$ ]]; then
echo "error: could not read the commit timestamp" >&2
exit 1
fi
{
echo "ver=${ver}"
echo "tag=v${ver}"
echo "epoch=${epoch}"
} >> "${GITHUB_OUTPUT}"
- name: Resolve build-env image
id: select
env:
OWNER: ${{ github.repository_owner }}
run: |
set -euo pipefail
owner_lc="$(printf '%s' "${OWNER}" | tr '[:upper:]' '[:lower:]')"
reg="${REGISTRY}/${owner_lc}/nesso-build-env"
deps_hash="$(./scripts/build-env-hash "${ARCH}")"
suffix=""
if [[ "${ARCH}" == "arm64" ]]; then
suffix="-arm64"
fi
{
echo "build_image=${reg}:deps-${deps_hash}${suffix}"
echo "runtime_image=${REGISTRY}/${owner_lc}/nesso"
} >> "${GITHUB_OUTPUT}"
- name: Log into ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Require nesso-build-env
env:
BUILD_IMAGE: ${{ steps.select.outputs.build_image }}
run: |
set -euo pipefail
if ! docker pull "${BUILD_IMAGE}"; then
echo "error: missing ${BUILD_IMAGE}; run CI on main so nesso-build-env is published first" >&2
exit 1
fi
# --provenance=false keeps this a single-platform image. The default
# BuildKit attestation is its own manifest and would make imagetools
# create an index of indexes. SLSA provenance is attached later, by
# digest, on the tag.
- name: Build runtime image
env:
BUILD_IMAGE: ${{ steps.select.outputs.build_image }}
RUNTIME_IMAGE: ${{ steps.select.outputs.runtime_image }}
TAG: ${{ steps.ver.outputs.tag }}
SOURCE_DATE_EPOCH: ${{ steps.ver.outputs.epoch }}
BUILDX_NO_DEFAULT_ATTESTATIONS: "1"
run: |
set -euo pipefail
docker buildx build \
--platform "${PLATFORM}" \
--build-arg "BUILD_IMAGE=${BUILD_IMAGE}" \
--build-arg "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}" \
--provenance=false \
--sbom=false \
--load \
-f packaging/Dockerfile.runtime \
-t "${RUNTIME_IMAGE}:${TAG}" \
.
got="$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "${RUNTIME_IMAGE}:${TAG}")"
if [[ "${got}" != "${PLATFORM}" ]]; then
echo "error: built ${got}, want ${PLATFORM}" >&2
exit 1
fi
- name: Trivy cache day
id: trivy-day
run: echo "day=$(date -u +%Y%m%d)" >> "${GITHUB_OUTPUT}"
- name: Cache Trivy DB and binary
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
${{ runner.temp }}/trivy-db
${{ runner.temp }}/nesso-cache/nesso/trivy
key: trivy-0.76.1-${{ runner.arch }}-${{ hashFiles('scripts/trivy') }}-${{ steps.trivy-day.outputs.day }}
restore-keys: |
trivy-0.76.1-${{ runner.arch }}-${{ hashFiles('scripts/trivy') }}-
lookup-only: true
- name: Scan runtime image
env:
RUNTIME_IMAGE: ${{ steps.select.outputs.runtime_image }}
TAG: ${{ steps.ver.outputs.tag }}
ARCH: ${{ matrix.arch }}
XDG_CACHE_HOME: ${{ runner.temp }}/nesso-cache
TRIVY_CACHE_DIR: ${{ runner.temp }}/trivy-db
GOPROXY: "off"
GOTOOLCHAIN: local
GOFLAGS: "-mod=readonly -buildvcs=false"
run: |
set -euo pipefail
if ! command -v go >/dev/null 2>&1 || ! go version | grep -Eq 'go1\.(2[2-9]|[3-9][0-9])'; then
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends golang-go
fi
chmod +x scripts/scan-runtime-image scripts/trivy
./scripts/scan-runtime-image \
--image "${RUNTIME_IMAGE}:${TAG}" \
--sarif trivy.sarif \
--json trivy.json
go build -C scripts/ci -o /tmp/nesso-ci .
/tmp/nesso-ci trivy-summary --json trivy.json --arch "${ARCH}"
- name: Upload Trivy SARIF
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
sarif_file: trivy.sarif
category: trivy-runtime-${{ matrix.arch }}
- name: Write determinism input
run: ./scripts/check-determinism --emit-input determinism-input.log
- name: Fetch fp32 MiniLM
run: ./scripts/fetch-model models-fp32 fp32
# The published binary needs the runtime image's glibc. A failure here
# stops the job before the image push and the GitHub release.
# stress-ng stays. scripts/check-determinism defaults to it for CPU load.
# The slim runtime image does not ship it. This install is inside the
# throwaway gate container, not the image that gets pushed. The smoke
# test does not use stress-ng.
- name: Gate int8 and fp32 determinism
env:
RUNTIME_IMAGE: ${{ steps.select.outputs.runtime_image }}
TAG: ${{ steps.ver.outputs.tag }}
run: |
set -euo pipefail
docker run --rm \
--platform "${PLATFORM}" \
--entrypoint bash \
-v "${PWD}/scripts/check-determinism:/opt/check-determinism:ro" \
-v "${PWD}/determinism-input.log:/opt/determinism-input.log:ro" \
-v "${PWD}/models-fp32:/opt/models-fp32:ro" \
"${RUNTIME_IMAGE}:${TAG}" \
-c '
set -euo pipefail
apt-get update
apt-get install -y --no-install-recommends python3 stress-ng
rm -rf /var/lib/apt/lists/*
gate() {
python3 /opt/check-determinism \
--binary /usr/local/bin/nesso \
--model-dir "$1" \
--input /opt/determinism-input.log \
--runs 10 \
--index-runs 3
}
gate /usr/share/nesso
gate /opt/models-fp32
'
- name: Extract binary and model
env:
RUNTIME_IMAGE: ${{ steps.select.outputs.runtime_image }}
TAG: ${{ steps.ver.outputs.tag }}
run: |
set -euo pipefail
cid="$(docker create --platform "${PLATFORM}" "${RUNTIME_IMAGE}:${TAG}")"
docker cp "${cid}:/usr/local/bin/nesso" "nesso-${ARCH}"
mkdir -p models
docker cp "${cid}:/usr/share/nesso/." models/
# package-deb reads copyright and THIRD_PARTY_NOTICES from doc/ next to the binary.
mkdir -p doc
docker cp "${cid}:/usr/share/doc/nesso/." doc/
docker rm "${cid}"
chmod +x "nesso-${ARCH}"
- name: Build .deb
env:
VER: ${{ steps.ver.outputs.ver }}
SOURCE_DATE_EPOCH: ${{ steps.ver.outputs.epoch }}
run: |
set -euo pipefail
if ! command -v objdump >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y --no-install-recommends binutils
fi
chmod +x scripts/package-deb
scripts/package-deb "${VER}" "./nesso-${ARCH}" ./models .
- name: Smoke-test image and .deb
env:
RUNTIME_IMAGE: ${{ steps.select.outputs.runtime_image }}
TAG: ${{ steps.ver.outputs.tag }}
VER: ${{ steps.ver.outputs.ver }}
run: |
set -euo pipefail
chmod +x scripts/smoke-runtime-image scripts/smoke-deb
./scripts/smoke-runtime-image "${RUNTIME_IMAGE}:${TAG}"
./scripts/smoke-deb "./nesso_${VER}_${ARCH}.deb"
- name: Generate SPDX SBOM
uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
with:
image: ${{ steps.select.outputs.runtime_image }}:${{ steps.ver.outputs.tag }}
format: spdx-json
output-file: nesso_${{ steps.ver.outputs.ver }}_${{ matrix.arch }}.spdx.json
upload-artifact: false
upload-release-assets: false
- name: Upload artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-image-${{ matrix.arch }}
path: |
nesso_${{ steps.ver.outputs.ver }}_${{ matrix.arch }}.deb
nesso_${{ steps.ver.outputs.ver }}_${{ matrix.arch }}.spdx.json
nesso-${{ matrix.arch }}
if-no-files-found: error
- name: Push arch image
id: push
if: github.event_name == 'push' && needs.approve-release.result == 'success'
env:
RUNTIME_IMAGE: ${{ steps.select.outputs.runtime_image }}
TAG: ${{ steps.ver.outputs.tag }}
VER: ${{ steps.ver.outputs.ver }}
run: |
set -euo pipefail
dest="${RUNTIME_IMAGE}:${TAG}-${ARCH}"
docker tag "${RUNTIME_IMAGE}:${TAG}" "${dest}"
push_log="$(docker push "${dest}")"
printf '%s\n' "${push_log}"
digest="$(printf '%s\n' "${push_log}" | sed -n 's/.*digest: \(sha256:[0-9a-f]\{64\}\).*/\1/p' | tail -n1)"
if [[ -z "${digest}" ]]; then
digest="$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${dest}" \
| sed -n 's/.*@\(sha256:[0-9a-f]\{64\}\)$/\1/p' | head -n1)"
fi
if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "error: could not resolve digest for ${dest}" >&2
exit 1
fi
{
echo "ver=${VER}"
echo "digest=${digest}"
} > runtime-digest.txt
cat runtime-digest.txt
- name: Upload digest
if: github.event_name == 'push' && needs.approve-release.result == 'success'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: runtime-digest-${{ matrix.arch }}
path: runtime-digest.txt
if-no-files-found: error
subjects:
needs: image
if: ${{ !cancelled() && needs.image.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
actions: write
outputs:
ver: ${{ steps.hash.outputs.ver }}
hashes: ${{ steps.hash.outputs.hashes }}
provenance_name: ${{ steps.hash.outputs.provenance_name }}
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Download arch artifacts
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: release-image-*
merge-multiple: true
path: release-files
- name: Hash release subjects
id: hash
run: |
set -euo pipefail
ver=""
for arch in amd64 arm64; do
deb="$(find release-files -type f -name "nesso_*_${arch}.deb" -print -quit)"
sbom="$(find release-files -type f -name "nesso_*_${arch}.spdx.json" -print -quit)"
if [[ -z "${deb}" || -z "${sbom}" ]]; then
echo "error: missing ${arch} deb or SBOM" >&2
find release-files -type f -print >&2
exit 1
fi
this="$(basename "${deb}" | sed -n 's/^nesso_\([0-9][0-9.]*\)_'"${arch}"'\.deb$/\1/p')"
if [[ ! "${this}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "error: could not read version from ${deb}" >&2
exit 1
fi
if [[ -n "${ver}" && "${this}" != "${ver}" ]]; then
echo "error: ${arch} version ${this} != ${ver}" >&2
exit 1
fi
ver="${this}"
cp -a "${deb}" "${sbom}" .
done
hashes="$(sha256sum \
"nesso_${ver}_amd64.deb" \
"nesso_${ver}_arm64.deb" \
"nesso_${ver}_amd64.spdx.json" \
"nesso_${ver}_arm64.spdx.json" | base64 -w0)"
sha256sum \
"nesso_${ver}_amd64.deb" \
"nesso_${ver}_arm64.deb" \
"nesso_${ver}_amd64.spdx.json" \
"nesso_${ver}_arm64.spdx.json" \
> SHA256SUMS
cat SHA256SUMS
{
echo "ver=${ver}"
echo "hashes=${hashes}"
echo "provenance_name=nesso_${ver}.intoto.jsonl"
} >> "${GITHUB_OUTPUT}"
- name: Upload release files
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: nesso-${{ steps.hash.outputs.ver }}-release
path: |
nesso_${{ steps.hash.outputs.ver }}_amd64.deb
nesso_${{ steps.hash.outputs.ver }}_arm64.deb
nesso_${{ steps.hash.outputs.ver }}_amd64.spdx.json
nesso_${{ steps.hash.outputs.ver }}_arm64.spdx.json
SHA256SUMS
if-no-files-found: error
index:
needs: [image, approve-release]
if: success() && needs.approve-release.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
actions: read
outputs:
image: ${{ steps.merge.outputs.image }}
index_digest: ${{ steps.merge.outputs.index_digest }}
amd64_digest: ${{ steps.merge.outputs.amd64_digest }}
arm64_digest: ${{ steps.merge.outputs.arm64_digest }}
ver: ${{ steps.merge.outputs.ver }}
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: runtime-digest-*
merge-multiple: false
path: digests
- name: Log into ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Publish manifest list
id: merge
env:
OWNER: ${{ github.repository_owner }}
run: |
set -euo pipefail
read_field() {
local file="$1" key="$2" value
value="$(sed -n "s/^${key}=//p" "${file}")"
if [[ -z "${value}" ]]; then
echo "error: ${file} has no ${key}" >&2
exit 1
fi
printf '%s\n' "${value}"
}
amd64_ver="$(read_field digests/runtime-digest-amd64/runtime-digest.txt ver)"
arm64_ver="$(read_field digests/runtime-digest-arm64/runtime-digest.txt ver)"
amd64_digest="$(read_field digests/runtime-digest-amd64/runtime-digest.txt digest)"
arm64_digest="$(read_field digests/runtime-digest-arm64/runtime-digest.txt digest)"
if [[ "${amd64_ver}" != "${arm64_ver}" ]]; then
echo "error: digest versions differ (${amd64_ver} vs ${arm64_ver})" >&2
exit 1
fi
owner_lc="$(printf '%s' "${OWNER}" | tr '[:upper:]' '[:lower:]')"
image="${REGISTRY}/${owner_lc}/nesso"
chmod +x scripts/publish-runtime-index
log="$(scripts/publish-runtime-index "${image}" "v${amd64_ver}" "${amd64_ver}" "${amd64_digest}" "${arm64_digest}")"
printf '%s\n' "${log}"
index_digest="$(printf '%s\n' "${log}" | tail -n1)"
if [[ ! "${index_digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "error: publish-runtime-index did not print a digest" >&2
exit 1
fi
{
echo "image=${image}"
echo "ver=${amd64_ver}"
echo "index_digest=${index_digest}"
echo "amd64_digest=${amd64_digest}"
echo "arm64_digest=${arm64_digest}"
} >> "${GITHUB_OUTPUT}"
# Keyless signatures. The certificate SAN is
# https://github.com/<owner>/nesso/.github/workflows/release.yml@refs/tags/vX.Y.Z
# The tag resolves to the index digest. Platform digests are signed as well:
# cosign does not treat an index signature as a signature of a child manifest.
sign:
needs: [index, approve-release]
if: success() && needs.approve-release.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
id-token: write
packages: write
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: v2.6.1
- name: Log into ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Sign runtime image by digest
env:
IMAGE: ${{ needs.index.outputs.image }}
INDEX_DIGEST: ${{ needs.index.outputs.index_digest }}
AMD64_DIGEST: ${{ needs.index.outputs.amd64_digest }}
ARM64_DIGEST: ${{ needs.index.outputs.arm64_digest }}
run: |
set -euo pipefail
for digest in "${INDEX_DIGEST}" "${AMD64_DIGEST}" "${ARM64_DIGEST}"; do
if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "error: refusing to sign without a digest (${digest})" >&2
exit 1
fi
cosign sign --yes "${IMAGE}@${digest}"
done
# SLSA Build L3 for both .debs and both SPDX SBOMs. upload-assets stays
# false so the release job can checksum the provenance before publishing.
# contents: write is declared because the called workflow's upload job
# requests it; that job does not run when upload-assets is false.
# Tag ref: the generator rejects a SHA pin. See CONTRIBUTING.md.
provenance:
needs: [subjects, approve-release]
if: success() && needs.approve-release.result == 'success'
permissions:
actions: read
id-token: write
contents: write
# zizmor: ignore[unpinned-uses] generator verifies its own vX.Y.Z ref
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.subjects.outputs.hashes }}
provenance-name: ${{ needs.subjects.outputs.provenance_name }}
upload-assets: false
# SLSA Build L3 for one digest. The attestation is stored on that digest
# in GHCR. slsa-verifier verify-image reads it from the registry.
# Tag ref: same generator constraint as the generic workflow.
image-provenance:
name: image-provenance (index)
needs: [index, approve-release]
if: success() && needs.approve-release.result == 'success'
permissions:
actions: read
id-token: write
packages: write
# zizmor: ignore[unpinned-uses] generator verifies its own vX.Y.Z ref
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.index.outputs.image }}
digest: ${{ needs.index.outputs.index_digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
image-provenance-amd64:
name: image-provenance (amd64)
needs: [index, approve-release]
if: success() && needs.approve-release.result == 'success'
permissions:
actions: read
id-token: write
packages: write
# zizmor: ignore[unpinned-uses] generator verifies its own vX.Y.Z ref
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.index.outputs.image }}
digest: ${{ needs.index.outputs.amd64_digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
image-provenance-arm64:
name: image-provenance (arm64)
needs: [index, approve-release]
if: success() && needs.approve-release.result == 'success'
permissions:
actions: read
id-token: write
packages: write
# zizmor: ignore[unpinned-uses] generator verifies its own vX.Y.Z ref
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.index.outputs.image }}
digest: ${{ needs.index.outputs.arm64_digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
# Scorecard Signed-Releases ignores the registry. releasesHaveProvenance
# matches a release asset whose name ends in .intoto.jsonl.
release:
needs: [subjects, index, sign, provenance, image-provenance, image-provenance-amd64, image-provenance-arm64, approve-release]
if: success() && needs.approve-release.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: write
packages: read
# This job has no checkout. gh reads GH_REPO instead of a git remote,
# which is what lets `gh run download` fetch the .deb provenance.
env:
GH_REPO: ${{ github.repository }}
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: v2.6.1
- name: Log into ${{ env.REGISTRY }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download build artifacts
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: nesso-${{ needs.subjects.outputs.ver }}-release
- name: Download .deb provenance
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROVENANCE_NAME: ${{ needs.provenance.outputs.provenance-name }}
run: |
set -euo pipefail
dest="${RUNNER_TEMP}/provenance"
mkdir -p "${dest}"
gh run download "${GITHUB_RUN_ID}" --name "${PROVENANCE_NAME}" --dir "${dest}"
found="$(find "${dest}" -type f -name "${PROVENANCE_NAME}" -print -quit)"
if [[ -z "${found}" ]]; then
echo "error: missing ${PROVENANCE_NAME} in the provenance artifact" >&2
find "${dest}" -print >&2
exit 1
fi
install -m 0644 "${found}" "./${PROVENANCE_NAME}"
test -s "./${PROVENANCE_NAME}"
- name: Download image provenance
env:
IMAGE: ${{ needs.index.outputs.image }}
VER: ${{ needs.index.outputs.ver }}
SUBJECT_VER: ${{ needs.subjects.outputs.ver }}
INDEX_DIGEST: ${{ needs.index.outputs.index_digest }}
AMD64_DIGEST: ${{ needs.index.outputs.amd64_digest }}
ARM64_DIGEST: ${{ needs.index.outputs.arm64_digest }}
run: |
set -euo pipefail
if [[ "${VER}" != "${SUBJECT_VER}" ]]; then
echo "error: image version ${VER} != package version ${SUBJECT_VER}" >&2
exit 1
fi
download() {
local digest="$1" out="$2" tmp ok
tmp="$(mktemp)"
ok=0
for _ in 1 2 3 4 5 6; do
if cosign download attestation "${IMAGE}@${digest}" > "${tmp}" && [[ -s "${tmp}" ]]; then
ok=1
break
fi
sleep 5
done
if [[ "${ok}" != 1 ]]; then
echo "error: image attestation was not readable for ${digest}" >&2
exit 1
fi
if [[ "$(head -c 1 "${tmp}")" != '{' ]]; then
echo "error: image attestation was not JSON for ${digest}" >&2
exit 1
fi
mv "${tmp}" "${out}"
}
download "${INDEX_DIGEST}" "nesso_${VER}_ghcr.intoto.jsonl"
download "${AMD64_DIGEST}" "nesso_${VER}_ghcr_amd64.intoto.jsonl"
download "${ARM64_DIGEST}" "nesso_${VER}_ghcr_arm64.intoto.jsonl"
- name: Generate SHA256SUMS
env:
VER: ${{ needs.subjects.outputs.ver }}
PROVENANCE_NAME: ${{ needs.provenance.outputs.provenance-name }}
run: |
set -euo pipefail
sha256sum \
"nesso_${VER}_amd64.deb" \
"nesso_${VER}_arm64.deb" \
"nesso_${VER}_amd64.spdx.json" \
"nesso_${VER}_arm64.spdx.json" \
"${PROVENANCE_NAME}" \
"nesso_${VER}_ghcr.intoto.jsonl" \
"nesso_${VER}_ghcr_amd64.intoto.jsonl" \
"nesso_${VER}_ghcr_arm64.intoto.jsonl" \
> SHA256SUMS
cat SHA256SUMS
- name: GitHub Release
# zizmor: ignore[superfluous-actions] tag publish keeps generate_release_notes and the fixed asset list
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
files: |
nesso_${{ needs.subjects.outputs.ver }}_amd64.deb
nesso_${{ needs.subjects.outputs.ver }}_arm64.deb
nesso_${{ needs.subjects.outputs.ver }}_amd64.spdx.json
nesso_${{ needs.subjects.outputs.ver }}_arm64.spdx.json
${{ needs.provenance.outputs.provenance-name }}
nesso_${{ needs.subjects.outputs.ver }}_ghcr.intoto.jsonl
nesso_${{ needs.subjects.outputs.ver }}_ghcr_amd64.intoto.jsonl
nesso_${{ needs.subjects.outputs.ver }}_ghcr_arm64.intoto.jsonl
SHA256SUMS
generate_release_notes: true