Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
95 changes: 94 additions & 1 deletion .github/workflows/docker-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ jobs:
# `buildcache-*` registry cache tags written by --cache-backend=
# registry. `contents: read` is the implicit default but pinned
# explicitly so a future workflow edit can't silently lose
# checkout access.
# checkout access. `id-token: write` + `attestations: write` let
# the keyless build-provenance step below mint Sigstore-backed
# attestations for the pushed images (same OIDC model the SBOM
# workflow already uses).
permissions:
contents: read
packages: write
id-token: write
attestations: write
strategy:
# Don't cancel sibling jobs on the first failure: any platform that
# has already finished building its images will have pushed the
Expand Down Expand Up @@ -150,6 +155,94 @@ jobs:
--service=redis \
${{ github.event_name == 'push' && '--push --skip-latest-tag' || '' }}

# Sign the freshly-pushed images with keyless build-provenance
# attestations (Sigstore via GitHub OIDC — the same mechanism the
# SBOM workflow already uses). Done here, per board, because this
# is where the immutable <short-hash>-<board> images are pushed
# and their digests are known. The floating latest-<board> tags
# added later by publish-latest re-point at these same manifests
# (identical digest), so they inherit the attestation for free.
#
# Attestation is by digest, and GitHub's attestation store is keyed
# on (repository, digest) rather than on the container registry, so
# a single attestation makes the image verifiable on BOTH registries:
# gh attestation verify \
# oci://ghcr.io/screenly/anthias-server:<short-hash>-<board> \
# --repo ${{ github.repository }}
# gh attestation verify \
# oci://docker.io/screenly/anthias-server:<short-hash>-<board> \
# --repo ${{ github.repository }}
# We additionally push an in-registry copy (push-to-registry) to
# each registry so registry-native / offline tooling can discover
# the attestation without the GitHub API. GHCR is hard-fail; the
# Docker Hub copy is best-effort (continue-on-error) because its
# referrers API periodically 429s under the free-tier pull limit —
# the same reason publish-latest soft-fails its Docker Hub mirror.
- name: Resolve pushed image digests
if: success() && github.event_name == 'push'
id: digests
run: |
set -euo pipefail
short_hash="$(git rev-parse --short=7 HEAD)"
for service in server viewer redis; do
ref="ghcr.io/screenly/anthias-${service}:${short_hash}-${{ matrix.board }}"
digest="$(docker buildx imagetools inspect "${ref}" \
--format '{{ .Manifest.Digest }}')"
echo "${service}=${digest}" >> "${GITHUB_OUTPUT}"
echo "Resolved ${ref} -> ${digest}"
done

- name: Attest anthias-server provenance (GHCR)
if: success() && github.event_name == 'push'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: ghcr.io/screenly/anthias-server
subject-digest: ${{ steps.digests.outputs.server }}
push-to-registry: true

- name: Attest anthias-viewer provenance (GHCR)
if: success() && github.event_name == 'push'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: ghcr.io/screenly/anthias-viewer
subject-digest: ${{ steps.digests.outputs.viewer }}
push-to-registry: true

- name: Attest anthias-redis provenance (GHCR)
if: success() && github.event_name == 'push'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: ghcr.io/screenly/anthias-redis
subject-digest: ${{ steps.digests.outputs.redis }}
push-to-registry: true

- name: Attest anthias-server provenance (Docker Hub)
if: success() && github.event_name == 'push'
continue-on-error: true
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: docker.io/screenly/anthias-server
subject-digest: ${{ steps.digests.outputs.server }}
push-to-registry: true

- name: Attest anthias-viewer provenance (Docker Hub)
if: success() && github.event_name == 'push'
continue-on-error: true
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: docker.io/screenly/anthias-viewer
subject-digest: ${{ steps.digests.outputs.viewer }}
push-to-registry: true

- name: Attest anthias-redis provenance (Docker Hub)
if: success() && github.event_name == 'push'
continue-on-error: true
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: docker.io/screenly/anthias-redis
subject-digest: ${{ steps.digests.outputs.redis }}
push-to-registry: true

# Mirror the immutable <short-hash>-<board> tags pushed by the buildx
# matrix onto the floating latest-<board> tag. Runs only after every
# buildx job has succeeded, so latest-* is never advanced from a
Expand Down
37 changes: 27 additions & 10 deletions docker/Dockerfile.qt5-webview-builder.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@
1. qt5-sysroot — armhf Debian + Raspbian/Pi userland; provides
the link-time sysroot the Qt 5 toolchain's qmake.conf points
at via `-sysroot /sysroot`.
2. webview-builder — host x86 (`$BUILDPLATFORM`); installs the
Linaro gcc-7.4.1 cross-compiler plus the pre-built Qt 5
2. webview-builder — host x86, pinned to linux/amd64 via the
QT5_HOST_PLATFORM ARG (not $BUILDPLATFORM — see the stage's
own comment); installs Debian's armhf cross-compiler
(crossbuild-essential-armhf, symlinked under the legacy Linaro
path the frozen qmake.conf bakes in) plus the pre-built Qt 5
toolchain pinned to WebView-v2026.07.1, then compiles the
in-tree src/anthias_webview/ source.

Qt 5 is frozen for pi2/pi3 boards, so the toolchain artifact at
WebView-v2026.07.1 never changes. To CVE-patch the toolchain run
bin/rebuild_qt5_toolchain.sh and re-upload to that tag.
#}
FROM --platform=linux/arm/v7 {{ base_image }}:{{ base_image_tag }} AS qt5-sysroot

This template is always included into Dockerfile.viewer.j2 (never
built standalone), so QT5_HOST_PLATFORM below resolves to the global
ARG that template declares before its first FROM. #}
Comment thread
vpetersson marked this conversation as resolved.

{# No --platform: the sysroot stage builds at the armhf target platform,
which is exactly what a bare FROM defaults to (the viewer image's
target is linux/arm/v7 for pi2/pi3). A literal --platform=linux/arm/v7
here would trip BuildKit's FromPlatformFlagConstDisallowed lint, and
spelling it $TARGETPLATFORM would trip RedundantTargetPlatform — so we
just let it default. #}
FROM {{ base_image }}:{{ base_image_tag }} AS qt5-sysroot

USER root

Expand Down Expand Up @@ -142,15 +155,19 @@ RUN --mount=type=cache,target=/var/cache/apt,id=qt5-sysroot-apt-{{ artifact_boar
dpkg --purge libraspberrypi-dev


FROM --platform=linux/amd64 {{ base_image }}:{{ base_image_tag }} AS webview-builder
FROM --platform=${QT5_HOST_PLATFORM} {{ base_image }}:{{ base_image_tag }} AS webview-builder

USER root

# Pinned to linux/amd64 (not $BUILDPLATFORM): the pre-built Qt 5
# toolchain bundle ships an x86_64 host qmake under qt5pi/bin, and the
# Debian armhf cross-compiler installed below is the x86_64-hosted
# toolchain — so this stage must run on x86_64. CI is amd64; arm64
# build hosts (e.g. Apple Silicon) will execute this stage under QEMU.
# Pinned to linux/amd64 via the QT5_HOST_PLATFORM global ARG (declared
# in Dockerfile.viewer.j2) rather than $BUILDPLATFORM: the pre-built
# Qt 5 toolchain bundle ships an x86_64 host qmake under qt5pi/bin, and
# the Debian armhf cross-compiler installed below is the x86_64-hosted
# toolchain — so this stage must run on x86_64 regardless of the build
# host. $BUILDPLATFORM would be arm64 on an Apple Silicon host and break
# it. CI is amd64; arm64 build hosts will execute this stage under QEMU.
# The ARG default keeps the pin while avoiding BuildKit's
# FromPlatformFlagConstDisallowed lint on literal --platform values.
#
# Host build tools plus the armhf cross-toolchain. The webview app
# builds against the pre-built Qt 5 toolchain, so we don't need the
Expand Down
8 changes: 8 additions & 0 deletions docker/Dockerfile.viewer.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# syntax=mirror.gcr.io/docker/dockerfile:1.24
# vim: ft=dockerfile
{% if not is_qt6 %}
{# Global ARG for the Qt 5 cross-compile host stage's pinned
--platform (see Dockerfile.qt5-webview-builder.j2). Declared here,
before the first FROM, so it is in scope for that stage's
FROM --platform — an ARG declared mid-file would be stage-scoped and
unusable in a later FROM. #}
ARG QT5_HOST_PLATFORM=linux/amd64
{% endif %}
{% include 'uv-builder.j2' %}

{% if is_qt6 %}
Expand Down