Cockatrice/.github/workflows/docker-release.yml
Lukas Brübach ac316bed26 [CI] Size each compiler cache against the cache quota
The 600M bound sat below what a leg writes: ccache spent each build
deleting the entries the next run asked for, with 2624-3720 cleanup
passes and about 8% of calls served in the runs inspected on
2026-11-04.

Raising it needs room first. The repository's 10 GB quota currently
holds 1.1 GB of CodeQL trap, 1.1 GB of vcpkg, 0.7 GB of Qt and 2.1 GB
of docker buildkit layers. Two of those are now cheap to reclaim:

- docker-release wrote its layer cache with mode=max, but the build
  stage sits behind COPY . ., so its compile layer is invalidated by
  every source change and was never read back. mode=min keeps only the
  final image stage.
- the five nightly-only legs kept their own compiler caches, 600M
  each, that no pull request ever reads. They now keep none, and the
  prune job retires their leftover keys immediately instead of waiting
  for GitHub's whole-entry LRU eviction to pick them.

That funds per-leg bounds: 700M for one configuration, 850M for the
Debian 13 release that also runs the suite, 900M for Fedora 44's
Debug and Release, 800M for the instrumented leg, 400M for the
server-only leg, and 700M for each macOS leg. 6.45G of ccache plus the
3.2G of fixed entries leaves about 350M under the quota.

The prune step also covers the buildkit entries by age now, so the
mode=max generation of layer blobs is reclaimed instead of living
forever: a layer a build needs is downloaded and its date refreshed,
and dropping one that an index still lists only costs docker-release a
rebuilt layer, which gates nothing.
2026-11-04 09:22:40 +01:00

203 lines
7.4 KiB
YAML

name: Build Docker
permissions:
contents: read # needed to checkout repo
packages: write # needed for interacting with GHCR
on:
push:
branches:
- master
pull_request:
branches:
- master
paths:
- '.github/workflows/docker-release.yml'
- '.dockerignore'
- 'Dockerfile'
- 'docker-compose.yml'
- 'docker-compose.yml.windows'
release:
types:
- released # publishing of stable releases
workflow_dispatch: # (re)publish the image for an existing stable release
inputs:
tag:
description: 'Existing release tag to build and publish (e.g. 2026-10-03-Release-3.1.0)'
required: true
type: string
latest:
description: 'Also update the "latest" tag'
required: true
type: boolean
default: true
# Cancel earlier, unfinished runs of this workflow on the same branch (unless publishing)
concurrency:
group: "${{ github.workflow }} @ ${{ inputs.tag || github.ref_name }}"
cancel-in-progress: ${{ github.event_name != 'release' && github.event_name != 'workflow_dispatch' }}
env:
GHCR_IMAGE: ghcr.io/cockatrice/servatrice
OCI_DESCRIPTION: Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games
OCI_TITLE: Servatrice
OCI_URL: https://cockatrice.github.io/
jobs:
# Create one platform-specific image and publish its OCI image manifest per matrix job
build:
name: "Servatrice (${{ matrix.label }})"
if: github.repository_owner == 'Cockatrice'
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- label: x86
platform: linux/amd64
runner: ubuntu-latest # https://github.com/actions/runner-images
- label: arm
platform: linux/arm64
runner: ubuntu-24.04-arm # https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Arm64-Readme.md, replace with "ubuntu-latest-arm" once available
env:
CACHE_SCOPE: servatrice-${{ matrix.label }}
steps:
- name: "Checkout"
uses: actions/checkout@v7
with:
ref: ${{ case(github.event_name == 'workflow_dispatch', format('refs/tags/{0}', inputs.tag), '') }}
- name: "Set up Docker buildx"
uses: docker/setup-buildx-action@v4
- name: "Login to GitHub Container Registry (GHCR)"
if: (github.event_name == 'release' && github.event.release.prerelease == false) || github.event_name == 'workflow_dispatch'
id: login
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
# Don't push for non-publishing triggers
#
# The layer cache shares the repository's 10 GB Actions cache quota with the compiler
# caches (see CCACHE_SIZE in desktop-build.yml). mode=min keeps only the final image
# stage: the build stage sits behind `COPY . .`, so its compile layer is invalidated by
# every source change and was never read back, yet it was the bulk of the ~2 GB this
# cache held. The final runtime stage is the part that still pays off.
- name: "Build image"
if: steps.login.outcome != 'success'
uses: docker/build-push-action@v7
with:
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=min,scope={0}', env.CACHE_SCOPE), '') }}
context: .
platforms: ${{ matrix.platform }}
push: false
# Add OCI labels and push single-platform image by digest (without tags)
- name: "Build image and push by digest"
if: steps.login.outcome == 'success'
id: build
uses: docker/build-push-action@v7
with:
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
cache-to: type=gha,mode=min,scope=${{ env.CACHE_SCOPE }}
context: .
labels: |
org.opencontainers.image.description=${{ env.OCI_DESCRIPTION }}
org.opencontainers.image.title=${{ env.OCI_TITLE }}
org.opencontainers.image.url=${{ env.OCI_URL }}
outputs: type=image,name=${{ env.GHCR_IMAGE }},name-canonical=true,push=true,push-by-digest=true
platforms: ${{ matrix.platform }}
provenance: mode=max # Do not pass secrets as build arguments with this option
sbom: true
- name: "Export digest"
if: steps.login.outcome == 'success'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p "$RUNNER_TEMP/digests"
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"
- name: "Upload digest"
if: steps.login.outcome == 'success'
uses: actions/upload-artifact@v7
with:
if-no-files-found: error
name: digest-${{ matrix.label }}
path: ${{ runner.temp }}/digests/*
retention-days: 1
# Create an OCI image index from the platform-specific image manifests
index:
name: "Publish multi-platform Servatrice image"
if: github.repository_owner == 'Cockatrice' && ((github.event_name == 'release' && github.event.release.prerelease == false) || github.event_name == 'workflow_dispatch')
needs: build
runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
steps:
- name: "Download digests"
uses: actions/download-artifact@v8
with:
path: ${{ runner.temp }}/digests
pattern: digest-*
merge-multiple: true
- name: "Login to GitHub Container Registry (GHCR)"
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: "Docker metadata"
id: metadata
uses: docker/metadata-action@v6
with:
images: ${{ env.GHCR_IMAGE }}
flavor: |
latest=auto
# if semver, also: type=semver,pattern={{version}} / {{major}}.{{minor}}
tags: |
type=ref,event=tag
type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.latest }}
# Add OCI annotations to image index and publish tags
- name: "Create image index"
env:
DOCKER_TAGS: ${{ steps.metadata.outputs.tags }}
working-directory: ${{ runner.temp }}/digests
run: |
TAG_ARGS=()
while IFS= read -r tag; do
TAG_ARGS+=(--tag "$tag")
done <<< "$DOCKER_TAGS"
DIGEST_ARGS=()
for digest in *; do
DIGEST_ARGS+=("$GHCR_IMAGE@sha256:$digest")
done
docker buildx imagetools create \
--prefer-index=true \
--annotation "index:org.opencontainers.image.description=$OCI_DESCRIPTION" \
--annotation "index:org.opencontainers.image.title=$OCI_TITLE" \
--annotation "index:org.opencontainers.image.url=$OCI_URL" \
"${TAG_ARGS[@]}" \
"${DIGEST_ARGS[@]}"
- name: "Inspect images"
env:
GITHUB_TAG: ${{ inputs.tag || github.ref_name }}
run: |
docker buildx imagetools inspect "$GHCR_IMAGE:latest"
docker buildx imagetools inspect "$GHCR_IMAGE:$GITHUB_TAG"