Skip to content

match a download to its job by infohash, not title substring #102

match a download to its job by infohash, not title substring

match a download to its job by infohash, not title substring #102

Workflow file for this run

name: Build & Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: {go-version: '1.25.x', cache: false, check-latest: true}
- name: gofmt
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "::error::These files are not gofmt'd. Run 'gofmt -w .' locally:"
echo "$unformatted"
exit 1
fi
- name: go build
run: go build -o gamarr ./cmd/gamarr/
- name: go test (race)
run: go test ./... -v -count=1 -race
- name: go vet
run: go vet ./...
- name: staticcheck
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: smoke test — binary boots and responds
run: |
mkdir -p /tmp/smoke-data
GAMARR_PORT=15001 \
DATA_DIR=/tmp/smoke-data \
./gamarr > /tmp/smoke.log 2>&1 &
SMOKE_PID=$!
for i in $(seq 1 30); do
if curl -sf -o /dev/null http://127.0.0.1:15001/api/health; then
echo "binary up after ${i}*500ms"
break
fi
sleep 0.5
done
if ! curl -sf -o /dev/null http://127.0.0.1:15001/api/health; then
echo "::error::binary failed to respond within 15s"
cat /tmp/smoke.log
kill $SMOKE_PID 2>/dev/null
exit 1
fi
# Confirm the new Torznab + OpenAPI surfaces are reachable.
curl -sf http://127.0.0.1:15001/api/openapi.json > /dev/null \
|| { echo "::error::/api/openapi.json failed"; cat /tmp/smoke.log; kill $SMOKE_PID 2>/dev/null; exit 1; }
curl -sf "http://127.0.0.1:15001/torznab/api?t=caps" > /dev/null \
|| { echo "::error::/torznab/api?t=caps failed"; cat /tmp/smoke.log; kill $SMOKE_PID 2>/dev/null; exit 1; }
kill $SMOKE_PID 2>/dev/null
if grep -qiE "panic|fatal" /tmp/smoke.log; then
echo "::error::panic/fatal in boot log:"
cat /tmp/smoke.log
exit 1
fi
docker:
runs-on: ubuntu-latest
permissions: {contents: read, packages: write}
steps:
- uses: actions/checkout@v4
# QEMU is needed only for the arm64 runtime stage (apk add / adduser) --
# the Go compile cross-compiles on the native runner. See the Dockerfile.
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
# The plain path a user gets with `docker build`, no buildx involved.
- name: docker build (native)
run: docker build -t gamarr:ci .
# Release publishes linux/amd64 + linux/arm64. Build both here so an
# arm64-breaking change fails on the PR instead of at `git tag`.
# cache-to seeds the gha cache that the release job restores from.
#
# Exactly one of the two build steps below runs per event: PRs validate
# the build, pushes to main publish it as :edge. They are separate steps
# rather than one step with a conditional `outputs:` because `--push` and
# `--output type=cacheonly` are mutually exclusive on the same build.
- name: docker build (multi-arch)
if: github.ref != 'refs/heads/main'
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
outputs: type=cacheonly
cache-from: type=gha
cache-to: type=gha,mode=max
# Fork PRs get a read-only GITHUB_TOKEN, so login is gated on main too --
# not just for correctness, but because a failed login would fail the job.
- name: Lowercase owner
if: github.ref == 'refs/heads/main'
id: owner
run: echo "name=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- uses: docker/login-action@v3
if: github.ref == 'refs/heads/main'
with: {registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}"}
# :edge tracks main. Releases move :latest and add an immutable :vX.Y.Z;
# this gives people who want unreleased fixes a prebuilt image instead of
# a local `docker build`, on both architectures.
- name: docker build (multi-arch) and publish :edge
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
# Stamped so `/api/health` on an edge image names the commit it came
# from -- "1.3.0" on a moving tag is not a bug report anyone can act on.
build-args: VERSION=edge-${{ github.sha }}
tags: ghcr.io/${{ steps.owner.outputs.name }}/gamarr:edge
cache-from: type=gha
cache-to: type=gha,mode=max
# A green multi-arch build does NOT prove the binary is arm64: if the
# TARGETARCH plumbing regressed, the arm64 image would still build, with an
# amd64 binary inside, and only fail at `exec` on a real arm64 host. So
# look at the binary itself.
#
# This exports the built filesystem straight to disk rather than loading an
# image and copying out of a container. `--load` + `docker cp` would route
# the answer through the daemon's image store, and on a containerd-store
# daemon that resolves to the HOST platform -- which silently turns this
# into an amd64-vs-amd64 comparison that can never fail.
- name: arm64 image contains an arm64 binary
run: |
docker buildx build --platform linux/arm64 \
--cache-from type=gha \
--output type=local,dest=/tmp/arm64-rootfs .
file /tmp/arm64-rootfs/usr/local/bin/gamarr
file /tmp/arm64-rootfs/usr/local/bin/gamarr | grep -q 'ARM aarch64' || {
echo "::error::/usr/local/bin/gamarr in the linux/arm64 image is not an aarch64 binary"
exit 1
}
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: {go-version: '1.25.x', cache: false, check-latest: true}
- name: build binary
run: go build -o /tmp/gamarr ./cmd/gamarr/
- uses: actions/setup-python@v5
with: {python-version: '3.12'}
- name: install e2e deps
run: |
pip install -r e2e/requirements.txt
playwright install --with-deps chromium
- name: e2e user journey (hermetic — stubbed services, real browser)
env:
GAMARR_E2E_BIN: /tmp/gamarr
run: pytest e2e/ -v
- name: server log on failure
if: failure()
run: cat /tmp/pytest-of-*/pytest-*/data*/gamarr.log 2>/dev/null || true