Skip to content

Publish loreserver image #2

Publish loreserver image

Publish loreserver image #2

# 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 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:
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:
- 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"