name: docker # Tests gate the image. A published `latest` that fails its own suite is worse # than no published image, because somebody else is now running it. on: push: branches: [main] tags: ["v*"] pull_request: workflow_dispatch: permissions: contents: read packages: write jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install run: | python -m pip install --upgrade pip pip install -r requirements.txt pytest - name: Test run: python -m pytest tests/ -q image: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # armv7 has no musl wheel for charset-normalizer, so that leg compiles # under emulation. It is the slow one; the others are native-speed. - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - name: Log in to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: # Lowercase literal: GHCR rejects uppercase, and the repository owner # is spelled with capitals. images: ghcr.io/blizzhacker/romarr # These are passed to the build as `labels:` below, which means they # are applied *after* the Dockerfile's own LABEL and win. Left to its # defaults this action titles the image `romarr` from the repository # name and takes the description from the GitHub About field, so the # Dockerfile's labels were dead and every registry showed the brand # in lowercase. labels: | org.opencontainers.image.title=ROMarr org.opencontainers.image.description=The *arr for games: request a ROM, ROMarr finds it via Prowlarr, grabs it, and files it into your game library tags: | type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha,format=short # The suite gates the code; nothing gated the image. A broken entrypoint, # a dependency that failed to install, or a missing bsdtar would all pass # every test and publish anyway -- and the first person to find out would # be somebody who pulled it. So one leg is built, booted and questioned # before anything is pushed. The multi-arch build below reuses this # build's cache, so the cost is close to nothing. - name: Build the amd64 leg for a smoke test uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64 load: true tags: romarr:smoke # The release tag, not the image tag: metadata-action gives the browser # build 1.7.0-browser and a branch build "latest", and neither parses # as a version. A non-tag build states that it does not know. build-args: | ROMARR_VERSION=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '0.0.0+unknown' }} cache-from: type=gha cache-to: type=gha,mode=max provenance: false - name: Boot the image and ask it whether it is alive run: | set -e mkdir -p "$RUNNER_TEMP/romarr-config" "$RUNNER_TEMP/romarr-roms" \ "$RUNNER_TEMP/romarr-downloads" printf 'download fixture' > "$RUNNER_TEMP/romarr-downloads/source.rar" chmod 0775 "$RUNNER_TEMP/romarr-downloads/source.rar" before=$(stat -c '%u:%g:%a' "$RUNNER_TEMP/romarr-downloads/source.rar") docker run -d --name smoke -p 6868:6868 \ -e PUID=1000 -e PGID=1000 -e UMASK=002 \ -v "$RUNNER_TEMP/romarr-config:/config" \ -v "$RUNNER_TEMP/romarr-roms:/roms" \ -v "$RUNNER_TEMP/romarr-downloads:/downloads" \ romarr:smoke for _ in $(seq 1 30); do curl -fsS -o /dev/null http://127.0.0.1:6868/api/health && break sleep 2 done echo "--- liveness ---" curl -fsS http://127.0.0.1:6868/api/health # No credential is set anywhere in this job, deliberately. An open # install is the failure mode that matters most for an image people # put on their LAN, and it is checkable without one. echo "--- the gate is on ---" code=$(curl -s -o /dev/null -w '%{http_code}' \ http://127.0.0.1:6868/api/v1/system/status) test "$code" = "401" || { echo "status answered $code, not 401"; exit 1; } # bsdtar is not optional: without it every disc-platform import fails # on a format it cannot open, and nothing else in this job would # notice its absence. echo "--- the pieces that are easy to lose ---" docker exec smoke sh -c 'command -v bsdtar >/dev/null' \ || { echo "bsdtar missing"; exit 1; } docker exec smoke sh -c 'command -v git >/dev/null' \ || { echo "git missing; catalogue plugins cannot install"; exit 1; } docker exec smoke python -c "import requests, rom_hub; from rom_hub.sandbox import install; install()" \ || { echo "a dependency did not install"; exit 1; } docker exec smoke python -c "from pathlib import Path; from rom_hub.catalog_sources import load_all; c=load_all(Path('/tmp/hub-smoke')); print(f'{len(c.entries)} bundled plugins'); assert c.entries" \ || { echo "ROM Hub installed without its plugin catalogue"; exit 1; } docker exec smoke sh -c 'ROM_HUB_HOME=/tmp/hub-install rom-hub plugin install archive-org' \ || { echo "bundled Archive.org plugin did not install"; exit 1; } echo "--- privileges were dropped ---" docker logs smoke 2>&1 | grep -q "starting as 1000:1000 with umask 002" \ || { echo "entrypoint did not drop to PUID:PGID"; exit 1; } echo "--- mounted downloads were not rewritten ---" after=$(stat -c '%u:%g:%a' "$RUNNER_TEMP/romarr-downloads/source.rar") test "$after" = "$before" \ || { echo "source permissions changed: $before -> $after"; exit 1; } test "$(awk '/^Umask:/ {print $2}' /proc/$(docker inspect -f '{{.State.Pid}}' smoke)/status)" = "0002" \ || { echo "server process did not inherit UMASK=002"; exit 1; } docker rm -f smoke - name: Build and push uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v7 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # The release tag, not the image tag: metadata-action gives the browser # build 1.7.0-browser and a branch build "latest", and neither parses # as a version. A non-tag build states that it does not know. build-args: | ROMARR_VERSION=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '0.0.0+unknown' }} cache-from: type=gha cache-to: type=gha,mode=max # Provenance attestations add manifest entries that several self-host # UIs display as an "unknown/unknown" platform. The image is what is # wanted here, not the attestation. provenance: false # The browser edition: Debian + the Playwright client, for the Headless # Browser download client. The default image is Alpine, where Playwright # cannot be installed at all (issue #35), so this is a separate tag rather # than a flag. No Chromium inside: it talks to a `playwright run-server`. image-browser: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - name: Log in to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/blizzhacker/romarr flavor: | latest=false tags: | type=raw,value=browser,enable={{is_default_branch}} type=semver,pattern={{version}}-browser type=semver,pattern={{major}}.{{minor}}-browser - name: Build the amd64 leg and prove the client is importable uses: docker/build-push-action@v6 with: context: . file: Dockerfile.browser platforms: linux/amd64 load: true tags: romarr:browser-smoke # The release tag, not the image tag: metadata-action gives the browser # build 1.7.0-browser and a branch build "latest", and neither parses # as a version. A non-tag build states that it does not know. build-args: | ROMARR_VERSION=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '0.0.0+unknown' }} cache-from: type=gha,scope=browser cache-to: type=gha,scope=browser,mode=max provenance: false - name: Boot it run: | set -e docker run -d --name smoke -p 6868:6868 romarr:browser-smoke for _ in $(seq 1 30); do curl -fsS -o /dev/null http://127.0.0.1:6868/api/health && break sleep 2 done docker exec smoke python -c "import playwright, rom_hub; from playwright.sync_api import sync_playwright" docker exec smoke sh -c 'command -v bsdtar >/dev/null && command -v git >/dev/null' docker rm -f smoke - name: Build and push uses: docker/build-push-action@v6 with: context: . file: Dockerfile.browser platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # The release tag, not the image tag: metadata-action gives the browser # build 1.7.0-browser and a branch build "latest", and neither parses # as a version. A non-tag build states that it does not know. build-args: | ROMARR_VERSION=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '0.0.0+unknown' }} cache-from: type=gha,scope=browser cache-to: type=gha,scope=browser,mode=max provenance: false