-
Notifications
You must be signed in to change notification settings - Fork 423
326 lines (300 loc) · 13.5 KB
/
Copy pathpublish-loreserver-image.yml
File metadata and controls
326 lines (300 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
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
# SPDX-FileCopyrightText: 2026 Epic Games, Inc.
# SPDX-License-Identifier: MIT
name: Publish loreserver image
# Builds lore-server/Dockerfile for linux/amd64 and linux/arm64, publishes
# multi-arch images to ghcr.io/epicgames/lore/loreserver, and signs them with
# keyless cosign so the community Helm chart can reference a verifiable tag.
#
# Two variants ship, differing only in how arm64 is compiled:
#
# :X.Y.Z baseline armv8-a arm64 — runs on any arm64 host
# :X.Y.Z-graviton arm64 tuned for Graviton3+, as Lore is deployed
#
# The default is the portable one, so a community chart works everywhere; the
# tuned build is opt-in for Graviton deployments. amd64 is baseline in both and
# is therefore built once, with both manifest lists pointing at that one digest.
#
# `meta` resolves tags and labels once, so the later jobs cannot disagree and
# a ref with no tags fails early. Each variant then builds on its own native
# runner (QEMU is far too slow for a release Rust build) and is pushed by
# digest; `merge` stitches the digests into manifest lists and signs them.
#
# No secrets: GITHUB_TOKEN authenticates to GHCR, and cosign signs keylessly
# against Fulcio via the job's OIDC token.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: >-
Extra tag to publish, for proving the workflow from a branch
(for example "edge"). Blank tags from the ref alone.
type: string
required: false
permissions: {}
concurrency:
group: publish-loreserver-image-${{ github.ref }}
cancel-in-progress: false
env:
IMAGE: ghcr.io/epicgames/lore/loreserver
# Passed to the Dockerfile for the tuned arm64 leg only.
GRAVITON_TARGET_CPU: neoverse-512tvb
jobs:
meta:
name: resolve tags
runs-on: ubuntu-latest
permissions:
contents: read # metadata-action reads repository metadata for OCI labels
outputs:
json: ${{ steps.meta.outputs.json }}
graviton-json: ${{ steps.meta-graviton.outputs.json }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
primary-tag: ${{ steps.primary.outputs.tag }}
graviton-primary-tag: ${{ steps.primary.outputs.graviton-tag }}
steps:
# The input lands verbatim in the `tags` list below, where a newline
# would smuggle in a further directive. Hold it to Docker's grammar.
- name: Validate the tag input
env:
TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
case "${TAG}" in
"") exit 0 ;;
[!A-Za-z0-9_]* | *[!A-Za-z0-9._-]*)
echo "::error::the 'tag' input is not a valid image tag: expected [A-Za-z0-9_][A-Za-z0-9._-]*"
exit 1
;;
esac
if [ "${#TAG}" -gt 128 ]; then
echo "::error::the 'tag' input is longer than the 128 characters a tag allows"
exit 1
fi
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.IMAGE }}
# Only a stable semver tag moves `latest`; a prerelease publishes
# its own tag and nothing else.
flavor: latest=auto
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=raw,value=${{ inputs.tag }},enable=${{ inputs.tag != '' }}
# The same tag set, suffixed. onlatest carries the suffix onto `latest`
# so the tuned stream has its own moving tag rather than fighting for the
# shared one.
- id: meta-graviton
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.IMAGE }}
flavor: |
latest=auto
suffix=-graviton,onlatest=true
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=raw,value=${{ inputs.tag }},enable=${{ inputs.tag != '' }}
# An empty list has to stop the run here, not surface as an invalid
# reference in the merge job once every architecture is pushed.
- id: primary
name: Require at least one tag
env:
JSON: ${{ steps.meta.outputs.json }}
GRAVITON_JSON: ${{ steps.meta-graviton.outputs.json }}
run: |
set -euo pipefail
tag=$(jq -r '.tags[0] // empty' <<< "$JSON")
graviton_tag=$(jq -r '.tags[0] // empty' <<< "$GRAVITON_JSON")
if [ -z "${tag}" ] || [ -z "${graviton_tag}" ]; then
echo "::error::no image tags resolved for ${GITHUB_REF}; push a semver tag or pass the 'tag' input"
exit 1
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "graviton-tag=${graviton_tag}" >> "$GITHUB_OUTPUT"
build:
name: build (${{ matrix.arch }}${{ matrix.variant == 'graviton' && ', graviton' || '' }})
needs: meta
runs-on: ${{ matrix.runner }}
permissions:
contents: read # Check out the source the image is built from
packages: write # Push the per-arch manifest, addressed by digest
strategy:
fail-fast: false
matrix:
include:
# amd64 is baseline for both variants, so it is built once and both
# manifest lists reference this digest.
- { arch: amd64, variant: base, platform: linux/amd64, runner: ubuntu-latest, target_cpu: "" }
- { arch: arm64, variant: base, platform: linux/arm64, runner: ubuntu-24.04-arm, target_cpu: "" }
- { arch: arm64, variant: graviton, platform: linux/arm64, runner: ubuntu-24.04-arm, target_cpu: neoverse-512tvb }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: lore-server/Dockerfile
platforms: ${{ matrix.platform }}
build-args: ARM64_TARGET_CPU=${{ matrix.target_cpu }}
labels: ${{ needs.meta.outputs.labels }}
annotations: ${{ needs.meta.outputs.annotations }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
# Only the apt and toolchain layers survive a source change. The
# cargo registry and target directory live in BuildKit cache mounts,
# which the gha backend does not carry between runs, so the compile
# itself starts cold every time. Scoped per variant so the two arm64
# legs, built with different codegen flags, cannot share layers.
cache-from: type=gha,scope=loreserver-${{ matrix.arch }}-${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=loreserver-${{ matrix.arch }}-${{ matrix.variant }}
# A clean build proves only that the image compiles, so start the binary
# too. Know the limit: this arm64 runner is Neoverse-N2 and reports SVE,
# so it executes the Graviton-tuned build quite happily. This catches a
# broken entrypoint or a missing shared library — it does NOT catch
# codegen aimed at a CPU the eventual host lacks, which is exactly how a
# SIGILL-ing arm64 image once passed a fully green run.
- name: Smoke test the pushed image
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: docker run --rm "${IMAGE}@${DIGEST}" --version
# The merge job addresses each build by digest. Hand the digests over as
# empty files named after themselves, one artifact per matrix leg so the
# merge job can pick which legs belong in which manifest list.
- name: Export digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p "${{ runner.temp }}/digests"
touch "${{ runner.temp }}/digests/${DIGEST#sha256:}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digests-${{ matrix.arch }}-${{ matrix.variant }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: merge and sign (${{ matrix.variant }})
needs: [meta, build]
runs-on: ubuntu-latest
permissions:
packages: write # Push the manifest list and the cosign signature
id-token: write # Federate to Fulcio for keyless signing
strategy:
# One variant failing should not withhold the other; both are
# independently useful once published.
fail-fast: false
matrix:
include:
- variant: default
legs: digests-amd64-base digests-arm64-base
- variant: graviton
legs: digests-amd64-base digests-arm64-graviton
env:
JSON: ${{ matrix.variant == 'graviton' && needs.meta.outputs.graviton-json || needs.meta.outputs.json }}
PRIMARY_TAG: ${{ matrix.variant == 'graviton' && needs.meta.outputs.graviton-primary-tag || needs.meta.outputs.primary-tag }}
LEGS: ${{ matrix.legs }}
# Anchored on this workflow's path: a bare repository prefix would
# accept a certificate minted by any workflow here.
IDENTITY_REGEXP: '^https://github\.com/${{ github.repository }}/\.github/workflows/publish-loreserver-image\.yml@'
steps:
# No merge-multiple: each leg keeps its own subdirectory so the right
# subset can be selected below.
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create the multi-arch manifest list
working-directory: ${{ runner.temp }}/digests
run: |
set -euo pipefail
args=()
while IFS= read -r tag; do
args+=(--tag "${tag}")
done < <(jq -r '.tags[]' <<< "$JSON")
# The build job annotated each per-arch manifest; the index is
# created here, so it needs the same annotations applying to it.
while IFS= read -r annotation; do
args+=(--annotation "index:${annotation}")
done < <(jq -r '.labels | to_entries[] | "\(.key)=\(.value)"' <<< "$JSON")
for leg in ${LEGS}; do
if [ ! -d "${leg}" ]; then
echo "::error::${leg} did not produce a digest; refusing to publish a partial manifest list"
exit 1
fi
for digest in "${leg}"/*; do
args+=("${IMAGE}@sha256:$(basename "${digest}")")
done
done
docker buildx imagetools create "${args[@]}"
- name: Inspect the manifest list
run: docker buildx imagetools inspect "${PRIMARY_TAG}"
- id: digest
name: Resolve the manifest list digest
run: |
set -euo pipefail
digest=$(docker buildx imagetools inspect "${PRIMARY_TAG}" \
--format '{{json .Manifest}}' | jq -r .digest)
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
# Sign the manifest list by digest, not by tag: a tag can later be moved
# to point at something else, a digest cannot.
- name: Sign the image with cosign
env:
DIGEST: ${{ steps.digest.outputs.digest }}
run: cosign sign --yes "${IMAGE}@${DIGEST}"
- name: Verify the signature
env:
DIGEST: ${{ steps.digest.outputs.digest }}
run: |
set -euo pipefail
cosign verify "${IMAGE}@${DIGEST}" \
--certificate-identity-regexp "${IDENTITY_REGEXP}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
> /dev/null
echo "signature verified"
- name: Summarise what was published
env:
DIGEST: ${{ steps.digest.outputs.digest }}
VARIANT: ${{ matrix.variant }}
run: |
set -euo pipefail
{
echo "## Published \`${IMAGE}\` (${VARIANT})"
echo ""
echo "Digest: \`${DIGEST}\`"
echo ""
if [ "${VARIANT}" = "graviton" ]; then
echo "Platforms: linux/amd64 (baseline), linux/arm64 (tuned for Graviton3+ — will not run on older arm64)"
else
echo "Platforms: linux/amd64, linux/arm64 (baseline armv8-a)"
fi
echo ""
echo "Tags:"
jq -r '.tags[] | "- `" + . + "`"' <<< "$JSON"
echo ""
echo "Verify the signature with:"
echo ""
echo '```sh'
echo "cosign verify ${IMAGE}@${DIGEST} \\"
echo " --certificate-identity-regexp '${IDENTITY_REGEXP}' \\"
echo " --certificate-oidc-issuer https://token.actions.githubusercontent.com"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"