Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 192 additions & 10 deletions .github/workflows/publish-loreserver-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,207 @@

name: Publish loreserver image

# Stub. Manual trigger only, so the workflow is registered on the default
# branch and can be dispatched against a pull request branch. The multi-arch
# build of lore-server/Dockerfile, the push to
# ghcr.io/epicgames/lore/loreserver and keyless cosign signing land in a
# follow-up pull request.
# Builds lore-server/Dockerfile for linux/amd64 and linux/arm64, publishes a
# multi-arch image to ghcr.io/epicgames/lore/loreserver, and signs it with
# keyless cosign so the community Helm chart can reference a public,
# verifiable tag.
#
# Each architecture builds on its own native runner and is pushed by digest
# alone; the merge job stitches those digests into one manifest list and signs
# that. Native runners rather than QEMU, because a release Rust build under
# emulation is far too slow to be practical.
#
# No secrets are involved: the built-in 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

jobs:
publish:
name: publish
build:
name: build (${{ matrix.arch }})
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:
- { arch: amd64, platform: linux/amd64, runner: ubuntu-latest }
- { arch: arm64, platform: linux/arm64, runner: ubuntu-24.04-arm }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.IMAGE }}

- 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 }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.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.
cache-from: type=gha,scope=loreserver-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=loreserver-${{ matrix.arch }}

# A clean build proves only that the image compiles. Run the binary on
# the native runner as well: a codegen flag aimed at the wrong CPU
# produces an image that builds happily and then dies with SIGILL the
# first time anyone starts it.
- name: Smoke test the pushed image
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: docker run --rm "${IMAGE}@${DIGEST}" --version

# The merge job addresses each architecture by digest. Hand the digests
# over as empty files named after themselves.
- 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 }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

merge:
name: merge and sign
needs: 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
steps:
# TODO: build, push and sign the multi-arch image.
- name: Publish
run: echo "publish-loreserver-image stub — no build or publish yet"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true

- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && inputs.tag != '' }}

- 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
# shellcheck disable=SC2046 # word splitting is how both lists are built
docker buildx imagetools create \
$(jq -cr '.tags | map("--tag " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf "${IMAGE}@sha256:%s " *)

- name: Inspect the manifest list
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
env:
VERSION: ${{ steps.meta.outputs.version }}

- id: digest
name: Resolve the manifest list digest
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -euo pipefail
digest=$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" \
--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 "^https://github.com/${GITHUB_REPOSITORY}/" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
> /dev/null
echo "signature verified"

- name: Summarise what was published
env:
DIGEST: ${{ steps.digest.outputs.digest }}
run: |
set -euo pipefail
{
echo "## Published \`${IMAGE}\`"
echo ""
echo "Digest: \`${DIGEST}\`"
echo ""
echo "Platforms: linux/amd64, linux/arm64"
echo ""
echo "Tags:"
jq -r '.tags[] | "- `" + . + "`"' <<< "$DOCKER_METADATA_OUTPUT_JSON"
echo ""
echo "Verify the signature with:"
echo ""
echo '```sh'
echo "cosign verify ${IMAGE}@${DIGEST} \\"
echo " --certificate-identity-regexp '^https://github.com/${GITHUB_REPOSITORY}/' \\"
echo " --certificate-oidc-issuer https://token.actions.githubusercontent.com"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
11 changes: 8 additions & 3 deletions lore-server/DOCKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ telemetry integration, or replication is configured.
## Prerequisites

- Docker with BuildKit support
- On Apple Silicon (M-series Macs), builds must target `linux/amd64` due to Graviton-specific
compiler flags in `.cargo/config.toml` for `aarch64-unknown-linux-gnu`

Both `linux/amd64` and `linux/arm64` build. The Dockerfile overrides the Graviton-specific
`target-cpu` that `.cargo/config.toml` sets for `aarch64-unknown-linux-gnu`, so the arm64
image runs on any armv8-a host rather than Graviton3 and newer only.

## Building

From the repository root:

```sh
docker build --platform linux/amd64 -f lore-server/Dockerfile -t loreserver .
docker build -f lore-server/Dockerfile -t loreserver .
```

Pass `--platform linux/amd64` or `--platform linux/arm64` to cross-build; expect it to be slow,
since a release Rust build under emulation is far slower than a native one.

The build compiles the `loreserver` binary and generates self-signed TLS certificates for QUIC
using `scripts/server/make-certs.sh`.

Expand Down
15 changes: 15 additions & 0 deletions lore-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ RUN apt-get update && apt-get install -y \
WORKDIR /build
COPY . .

# .cargo/config.toml tunes aarch64-unknown-linux-gnu for Epic's own Graviton3+
# deployment with -C target-cpu=neoverse-512tvb, which emits instructions that
# fault on any older arm64 part. A published image has to run anywhere armv8-a
# runs, so that tuning has to go.
#
# It has to go via RUSTFLAGS specifically. CARGO_TARGET_<triple>_RUSTFLAGS is
# only another source for the same config key, and cargo *joins* config arrays
# rather than replacing them, so setting it leaves the Graviton flag in place
# and the image still dies with SIGILL. RUSTFLAGS is a separate, mutually
# exclusive source: when it is set, cargo ignores [build] and [target.*]
# rustflags entirely. So everything the build needs has to be repeated here.
# The two --cfg values are load-bearing, the code does not compile without
# them; the [build] clippy -W lints are dropped, as they only affect warnings.
ENV RUSTFLAGS="--cfg tokio_unstable --cfg uuid_unstable -C force-unwind-tables=yes -C force-frame-pointers=yes"

RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
Expand Down
Loading