mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-22 11:53:22 -05:00
* feat(elfldr): vendor elfldr with a read deadline, embedded in the engine elfldr serves one connection at a time and its reads had no deadline, so a client that went silent without closing (a dropped link, a machine or console asleep mid-send) left it running but deaf on :9021 — users had to load elfldr again. Measured on the Phat (FW 5.10): the stock build answered nothing while a silent connection stayed open; with a 15 s SO_RCVTIMEO it dropped that connection and answered the next request at once, and a payload sent with 10 s pauses still loaded. third_party/elfldr is upstream 02cfe91 plus ps5upload.patch (GPLv3, credited), with the tested ELF checked in so every engine build embeds it; `make elfldr` rebuilds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(elfldr): the engine swaps a console's stock elfldr for the patched one GET /api/ps5/elfldr/health tells healthy / stuck / absent from a request elfldr answers without running anything. POST /api/ps5/elfldr/ensure replaces the standard elfldr.elf with the embedded patched build when it answers and isn't the one this engine installed (a reboot or autoloader brings the stock one back), leaves any other loader on :9021 alone, and returns once the new one answers. On the Phat: upgraded (pid 685, answering at once), then 'current'; the helper was untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(publish): give each client build 75 minutes The Intel macOS build ran past the 45-minute limit for v5.37.0 (about 33 minutes for v5.35.0 and v5.36.0), which cancelled it and skipped create-release, so v5.37.0 was never published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(engine): history tests get their own temp dirs TmpDir named directories by pid and nanoseconds, but macOS time ticks in microseconds: two tests starting together shared a directory and one deleted it under the other (a_legacy_host_port_file_is_still_read read nothing). A counter makes each name unique. The elfldr embed test reads the embedded bytes directly instead of clearing an env var mid-run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(elfldr): the app keeps the patched elfldr in place and never sends into a stuck one Before sending the helper, the app checks the console's loader: a stuck one (took the connection, never answered) is waited out for 20 s — the patched build clears in 15 — and one that stays stuck is reported plainly ('load elfldr again') instead of a socket error. Once the helper is up, the engine swaps the stock elfldr for the patched build: at once after a fresh helper send (a reboot or wake brings the stock one back), else at most every 10 minutes per console. Desktop, browser and Android share the engine routes; strings in all 20 languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): 5.37.1 — elfldr recovers by itself; 5.37.0 folded in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(elfldr): swap it in when the helper is already up at launch Only ensurePayloadCurrent asked for the patched elfldr, and a helper that is already up when the app opens never goes through it — on the Phat the stock elfldr stayed. The status poll's 'arrives at up' edge (first sighting included) now asks too, after the same settle as the log capture and never during a transfer. Hardware (Phat FW 5.10, web build and Android app): stock elfldr replaced ~10 s after opening; a silent :9021 connection held while the helper was shut down — the helper came back through the patched elfldr; with the stock elfldr wedged, the Android app sent nothing for 150 s (health: stuck), then redeployed and swapped elfldr once released. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(convert): the compress stage pays for reading and writing on one thread kraken_image::compress runs the encoder on a pool, but one thread reads the source and writes each compressed block, so the stage takes the longer of encoding and read+write — not the slower of reading and encoding, as the estimate assumed. With the Windows Battlefield 6 run's speeds the model now gives 75 min for a 71-min build. The estimate also reports the speeds it measured (rates), because the small read/write samples can be fooled by caches: on a source read an hour earlier the read sample hit the file cache at 8.6 GB/s and the estimate came out at 25 min for a 54-min build. Calibrating from past builds is the follow-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): build-time estimate, stated as measured Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(docker): 25 minutes for the PR image checks Both PR gates build the whole image (engine, and the client for the web UI one) in about 7-10 minutes; the engine gate took 9m55s and the web UI gate was cancelled at its 10-minute limit on PR #338. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
494 lines
18 KiB
YAML
494 lines
18 KiB
YAML
name: docker-engine
|
|
|
|
# Builds and publishes two GHCR images from the engine crate:
|
|
# - ps5upload-engine (engine/Dockerfile) — minimal, no UI
|
|
# - ps5upload-engine-webui (engine/Dockerfile.webui) — engine + the full
|
|
# React app served over HTTP, for browser/NAS/headless use (no Tauri
|
|
# client needed). Its build context is the REPO ROOT, not engine/, since
|
|
# it needs to `npm ci && npm run build:vite` in client/ before the Rust
|
|
# build embeds the output.
|
|
#
|
|
# Tag pushes (v*) publish multi-arch (amd64 + arm64) manifests for both
|
|
# images, in lockstep — one release cuts both. PRs that touch engine/ or
|
|
# client/ get a build-only smoke-check of both Dockerfiles (no push).
|
|
#
|
|
# Both images embed the two PS5 ELF images (ps5upload.elf and the DPI
|
|
# install daemon) so a browser driving a self-hosted engine can bring the
|
|
# DPI daemon up and put the helper back afterwards — the fallback that
|
|
# installs a game PATCH. The desktop client does this from its own embeds;
|
|
# a browser has no socket and no bytes, so without them the web UI could
|
|
# install base games and nothing else (the web UI half of #152). That is why the tag
|
|
# builds now start with a `build-payload` job: the main payload is not
|
|
# committed (see .gitignore) and needs the PS5 payload SDK.
|
|
#
|
|
# Manual re-run (e.g. after an Actions outage wedges a tag-push run):
|
|
# gh workflow run docker-engine.yml -f tag=v3.3.16
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
pull_request:
|
|
paths: ['engine/**', 'client/**', '.github/workflows/docker-engine.yml']
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Release tag to publish (e.g. v3.3.16). Must already exist."
|
|
required: true
|
|
|
|
# Least privilege at the top: read-only by default. Only the jobs that actually
|
|
# push to GHCR (build*, merge*) opt into packages:write below — the PR
|
|
# `verify`/`verify-webui` jobs inherit read-only and never get a write-capable
|
|
# token.
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: docker-engine-${{ github.event.inputs.tag || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# ── PR gate: build amd64, no push ──────────────────────────────────────────
|
|
verify:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
# A full engine (and, for the web UI, client) build: ~7-10 min on a good
|
|
# runner, so 10 cancelled ordinary runs (PR #338).
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Stage the committed DPI daemon for the embed
|
|
# ps5upload.elf is not in the repo (it needs the payload SDK), so a
|
|
# PR build can only cover the DPI half. That is the half the web
|
|
# UI's patch install depends on, and staging it here means a broken
|
|
# embed fails the PR rather than the release.
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p engine/crates/ps5upload-engine/payload
|
|
cp -v payload/installer/ps5upload-installer.elf engine/crates/ps5upload-engine/payload/
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
- uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: engine/Dockerfile
|
|
platforms: linux/amd64
|
|
push: false
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
|
|
# ── PR gate for the webui image: same idea, root context + Dockerfile.webui ─
|
|
verify-webui:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
# A full engine (and, for the web UI, client) build: ~7-10 min on a good
|
|
# runner, so 10 cancelled ordinary runs (PR #338).
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Stage the committed DPI daemon for the embed
|
|
# ps5upload.elf is not in the repo (it needs the payload SDK), so a
|
|
# PR build can only cover the DPI half. That is the half the web
|
|
# UI's patch install depends on, and staging it here means a broken
|
|
# embed fails the PR rather than the release.
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p engine/crates/ps5upload-engine/payload
|
|
cp -v payload/installer/ps5upload-installer.elf engine/crates/ps5upload-engine/payload/
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
- uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: engine/Dockerfile.webui
|
|
platforms: linux/amd64
|
|
push: false
|
|
cache-from: type=gha,scope=webui
|
|
cache-to: type=gha,mode=max,scope=webui
|
|
|
|
# ── Resolve + validate the tag ONCE, hand the trusted values to build/merge ─
|
|
# Centralizing this means the tag is regex-validated before it ever reaches a
|
|
# checkout `ref:` (no ref injection from a hand-typed workflow_dispatch input),
|
|
# and the lowercased image names are computed once via the safe env pattern
|
|
# instead of interpolating ${{ github.repository_owner }} into shell.
|
|
resolve:
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
outputs:
|
|
tag: ${{ steps.t.outputs.tag }}
|
|
version: ${{ steps.t.outputs.version }}
|
|
major_minor: ${{ steps.t.outputs.major_minor }}
|
|
image: ${{ steps.t.outputs.image }}
|
|
image_webui: ${{ steps.t.outputs.image_webui }}
|
|
steps:
|
|
- name: Validate tag + derive image names
|
|
id: t
|
|
env:
|
|
INPUT_TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
|
OWNER: ${{ github.repository_owner }}
|
|
run: |
|
|
raw_tag="$INPUT_TAG"
|
|
if ! printf '%s' "$raw_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-+._A-Za-z0-9]*)?$'; then
|
|
echo "::error::Refusing to proceed: '$raw_tag' is not a valid version tag"
|
|
exit 1
|
|
fi
|
|
version="${raw_tag#v}"
|
|
owner_lc="$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]')"
|
|
{
|
|
echo "tag=$raw_tag"
|
|
echo "version=$version"
|
|
echo "major_minor=${version%.*}"
|
|
echo "image=ghcr.io/${owner_lc}/ps5upload-engine"
|
|
echo "image_webui=ghcr.io/${owner_lc}/ps5upload-engine-webui"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
# ── Build the two PS5 ELF images the engine embeds ─────────────────────────
|
|
# Mirrors publish.yml's job of the same name. Duplicated rather than shared
|
|
# because the two workflows run independently on a tag push and neither can
|
|
# wait on the other's artifacts. The DPI daemon is committed, but the main
|
|
# payload is not (see .gitignore), and the engine needs BOTH: it streams the
|
|
# daemon to the loader on :9021 and then puts ps5upload back afterwards.
|
|
build-payload:
|
|
needs: resolve
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.resolve.outputs.tag }}
|
|
|
|
- name: Install payload toolchain
|
|
timeout-minutes: 12
|
|
run: |
|
|
set -euo pipefail
|
|
# apt has no default network timeout, so a stalled mirror hangs
|
|
# until the job dies. Retry with explicit timeouts.
|
|
apt_retry() {
|
|
for attempt in 1 2 3; do
|
|
if sudo DEBIAN_FRONTEND=noninteractive apt-get \
|
|
-o Acquire::Retries=3 \
|
|
-o Acquire::http::Timeout=20 \
|
|
-o Acquire::https::Timeout=20 \
|
|
"$@"; then
|
|
return 0
|
|
fi
|
|
echo "apt attempt $attempt failed; retrying in 15s" >&2
|
|
sleep 15
|
|
done
|
|
echo "apt failed after 3 attempts: apt-get $*" >&2
|
|
return 1
|
|
}
|
|
apt_retry update
|
|
# llvm-22 is not in Ubuntu 24.04's default repos.
|
|
curl -fsSL --retry 3 --retry-delay 5 https://apt.llvm.org/llvm.sh | sudo bash -s -- 22
|
|
apt_retry install -y clang-22 llvm-22-dev lld-22 unzip python3
|
|
|
|
- name: Install pinned PS5 Payload SDK
|
|
run: sudo env PS5_SDK_INSTALL_DIR=/opt/ps5-payload-sdk bash scripts/install-ps5-sdk.sh
|
|
|
|
- name: Build payload ELFs
|
|
env:
|
|
PS5_PAYLOAD_SDK: /opt/ps5-payload-sdk
|
|
# SDK v0.42's prospero-llvm-config candidate search maxes at 21,
|
|
# so point it at the CI-installed LLVM 22 explicitly.
|
|
LLVM_CONFIG: /usr/bin/llvm-config-22
|
|
run: make payload
|
|
|
|
- name: Sanity-check ELF shape
|
|
run: |
|
|
file payload/ps5upload.elf payload/installer/ps5upload-installer.elf
|
|
test -s payload/ps5upload.elf
|
|
test -s payload/installer/ps5upload-installer.elf
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ps5upload-payload-elves
|
|
path: |
|
|
payload/ps5upload.elf
|
|
payload/installer/ps5upload-installer.elf
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
# ── Per-arch native build, push by digest (no tag yet) ─────────────────────
|
|
build:
|
|
needs: [resolve, build-payload]
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- { os: ubuntu-24.04, platform: linux/amd64 }
|
|
- { os: ubuntu-24.04-arm, platform: linux/arm64 }
|
|
env:
|
|
IMAGE: ${{ needs.resolve.outputs.image }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.resolve.outputs.tag }}
|
|
|
|
- name: Prepare environment
|
|
env:
|
|
PLATFORM: ${{ matrix.platform }}
|
|
run: echo "PLATFORM_PAIR=$(printf '%s' "$PLATFORM" | tr '/' '-')" >> "$GITHUB_ENV"
|
|
|
|
- name: Stage the PS5 ELF images for the engine embed
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: ps5upload-payload-elves
|
|
path: /tmp/payload-elves
|
|
|
|
- name: Move them into the engine's embed directory
|
|
# `build.rs` looks here first — the same convention `webui/` uses for
|
|
# the React bundle, so the images ride along in the existing
|
|
# `COPY engine/` layer and no Dockerfile needs a second context path.
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p engine/crates/ps5upload-engine/payload
|
|
cp -v /tmp/payload-elves/ps5upload.elf \
|
|
/tmp/payload-elves/installer/ps5upload-installer.elf \
|
|
engine/crates/ps5upload-engine/payload/
|
|
ls -la engine/crates/ps5upload-engine/payload/
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
- uses: docker/login-action@v4.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- uses: docker/metadata-action@v6
|
|
id: meta
|
|
with:
|
|
images: ${{ env.IMAGE }}
|
|
|
|
- name: Build and push by digest
|
|
id: push
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: engine/Dockerfile
|
|
platforms: ${{ matrix.platform }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
|
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
|
|
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
|
|
|
|
- name: Export digest
|
|
env:
|
|
DIGEST: ${{ steps.push.outputs.digest }}
|
|
run: |
|
|
mkdir -p /tmp/digests
|
|
touch "/tmp/digests/${DIGEST#sha256:}"
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: digests-${{ env.PLATFORM_PAIR }}
|
|
path: /tmp/digests/*
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
# ── Per-arch native build of the webui image, push by digest ───────────────
|
|
# Mirrors `build` above; only the context/file/image/artifact-name/cache-
|
|
# scope differ. Kept as a separate job (not a matrix dimension on `build`)
|
|
# so a webui-only failure doesn't need `fail-fast: false` gymnastics to
|
|
# keep the plain engine image publishing on time.
|
|
build-webui:
|
|
needs: [resolve, build-payload]
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- { os: ubuntu-24.04, platform: linux/amd64 }
|
|
- { os: ubuntu-24.04-arm, platform: linux/arm64 }
|
|
env:
|
|
IMAGE: ${{ needs.resolve.outputs.image_webui }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.resolve.outputs.tag }}
|
|
|
|
- name: Prepare environment
|
|
env:
|
|
PLATFORM: ${{ matrix.platform }}
|
|
run: echo "PLATFORM_PAIR=$(printf '%s' "$PLATFORM" | tr '/' '-')" >> "$GITHUB_ENV"
|
|
|
|
- name: Stage the PS5 ELF images for the engine embed
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: ps5upload-payload-elves
|
|
path: /tmp/payload-elves
|
|
|
|
- name: Move them into the engine's embed directory
|
|
# `build.rs` looks here first — the same convention `webui/` uses for
|
|
# the React bundle, so the images ride along in the existing
|
|
# `COPY engine/` layer and no Dockerfile needs a second context path.
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p engine/crates/ps5upload-engine/payload
|
|
cp -v /tmp/payload-elves/ps5upload.elf \
|
|
/tmp/payload-elves/installer/ps5upload-installer.elf \
|
|
engine/crates/ps5upload-engine/payload/
|
|
ls -la engine/crates/ps5upload-engine/payload/
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
- uses: docker/login-action@v4.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- uses: docker/metadata-action@v6
|
|
id: meta
|
|
with:
|
|
images: ${{ env.IMAGE }}
|
|
|
|
- name: Build and push by digest
|
|
id: push
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: engine/Dockerfile.webui
|
|
platforms: ${{ matrix.platform }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
|
cache-from: type=gha,scope=webui-${{ env.PLATFORM_PAIR }}
|
|
cache-to: type=gha,mode=max,scope=webui-${{ env.PLATFORM_PAIR }}
|
|
|
|
- name: Export digest
|
|
env:
|
|
DIGEST: ${{ steps.push.outputs.digest }}
|
|
run: |
|
|
mkdir -p /tmp/digests
|
|
touch "/tmp/digests/${DIGEST#sha256:}"
|
|
|
|
# Distinct "webui-digests-" prefix (not just a "webui-" infix) so the
|
|
# plain engine `merge` job's `pattern: digests-*` can't glob-match
|
|
# these artifacts and fold a webui digest into the minimal image's
|
|
# manifest.
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: webui-digests-${{ env.PLATFORM_PAIR }}
|
|
path: /tmp/digests/*
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
# ── Merge per-arch digests into a multi-arch manifest ──────────────────────
|
|
merge:
|
|
needs: [resolve, build]
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
env:
|
|
IMAGE: ${{ needs.resolve.outputs.image }}
|
|
VERSION: ${{ needs.resolve.outputs.version }}
|
|
MAJOR_MINOR: ${{ needs.resolve.outputs.major_minor }}
|
|
steps:
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: /tmp/digests
|
|
pattern: digests-*
|
|
merge-multiple: true
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
- uses: docker/login-action@v4.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- uses: docker/metadata-action@v6
|
|
id: meta
|
|
with:
|
|
images: ${{ env.IMAGE }}
|
|
tags: |
|
|
type=raw,value=${{ env.VERSION }}
|
|
type=raw,value=${{ env.MAJOR_MINOR }}
|
|
type=raw,value=latest
|
|
|
|
- name: Create and push multi-arch manifest
|
|
working-directory: /tmp/digests
|
|
env:
|
|
META_JSON: ${{ steps.meta.outputs.json }}
|
|
run: |
|
|
# Word-splitting here is intentional: the jq output expands to
|
|
# `-t tagA -t tagB ...` and the printf to space-separated
|
|
# `image@sha256:<digest>` operands, each a distinct argv entry.
|
|
# shellcheck disable=SC2046
|
|
docker buildx imagetools create \
|
|
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$META_JSON") \
|
|
$(printf "${IMAGE}@sha256:%s " *)
|
|
|
|
- name: Inspect manifest
|
|
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
|
|
|
|
# ── Merge the webui image's per-arch digests ────────────────────────────────
|
|
merge-webui:
|
|
needs: [resolve, build-webui]
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
env:
|
|
IMAGE: ${{ needs.resolve.outputs.image_webui }}
|
|
VERSION: ${{ needs.resolve.outputs.version }}
|
|
MAJOR_MINOR: ${{ needs.resolve.outputs.major_minor }}
|
|
steps:
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: /tmp/digests
|
|
pattern: webui-digests-*
|
|
merge-multiple: true
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
- uses: docker/login-action@v4.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- uses: docker/metadata-action@v6
|
|
id: meta
|
|
with:
|
|
images: ${{ env.IMAGE }}
|
|
tags: |
|
|
type=raw,value=${{ env.VERSION }}
|
|
type=raw,value=${{ env.MAJOR_MINOR }}
|
|
type=raw,value=latest
|
|
|
|
- name: Create and push multi-arch manifest
|
|
working-directory: /tmp/digests
|
|
env:
|
|
META_JSON: ${{ steps.meta.outputs.json }}
|
|
run: |
|
|
# shellcheck disable=SC2046
|
|
docker buildx imagetools create \
|
|
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$META_JSON") \
|
|
$(printf "${IMAGE}@sha256:%s " *)
|
|
|
|
- name: Inspect manifest
|
|
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
|