Files
allstarr/.github/workflows/docker.yml
T

290 lines
11 KiB
YAML

name: Docker Build & Push
on:
workflow_dispatch:
push:
tags: ["v*"]
branches: [main, beta]
env:
DOTNET_VERSION: "10.0.301"
NODE_VERSION: "22.23.1"
ALLSTARR_TEST_POSTGRES: "Host=127.0.0.1;Port=5432;Database=allstarr_test;Username=allstarr;Password=ci-postgres-password;SSL Mode=Disable"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-test:
runs-on: ubuntu-latest
outputs:
app-version: ${{ steps.app-version.outputs.version }}
services:
postgres:
image: postgres:18.4-alpine3.23@sha256:996d0920e4ff9df1fc19dacb904492f3c1ec0ec1cc338f0ad7123be7731c5f5e
env:
POSTGRES_DB: allstarr_test
POSTGRES_USER: allstarr
POSTGRES_PASSWORD: ci-postgres-password
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U allstarr -d allstarr_test"
--health-interval 5s
--health-timeout 5s
--health-retries 20
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Read canonical application version
id: app-version
shell: bash
run: |
set -euo pipefail
version="$(sed -n 's/.*Version = "\([^"]*\)";.*/\1/p' allstarr/AppVersion.cs)"
test -n "${version}"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
if [[ "${GITHUB_REF_TYPE}" == "tag" && "${GITHUB_REF_NAME}" != "v${version}" ]]; then
echo "Release tag ${GITHUB_REF_NAME} does not match canonical app version v${version}." >&2
exit 1
fi
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Setup Node
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: webui/package-lock.json
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.10"
- name: Install PostgreSQL 18 client
shell: bash
run: bash tools/ci/postgres-client.sh install
- name: Restore dependencies
run: dotnet restore -p:NuGetAudit=true
- name: Restore Svelte WebUI
working-directory: webui
run: npm ci
- name: Verify C# formatting and analyzers
run: dotnet format allstarr.sln --no-restore --verify-no-changes --verbosity minimal
- name: Build
run: dotnet build --configuration Release --no-restore -p:TreatWarningsAsErrors=true
- name: Verify Svelte WebUI
working-directory: webui
run: |
npm run check
npm test
npm run build
npm run check:budgets
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Test Apple gateway contracts
working-directory: sidecars/apple-gateway
run: |
python -m pip install uv==0.11.29
UV_CACHE_DIR=/tmp/allstarr-uv-cache uv sync --frozen --extra test
UV_CACHE_DIR=/tmp/allstarr-uv-cache uv run --frozen --extra test pytest
- name: Validate Compose contracts
run: |
docker compose -f docker-compose.yml config --quiet
docker compose -f docker-compose.yml --profile spotify-lyrics config --quiet
docker compose -f docker-compose.yml --profile apple config --quiet
docker:
needs: build-and-test
runs-on: ubuntu-latest
# Publish only from an explicit dispatch or a branch/tag push. Pull requests
# are tested by CI and do not have a stable image tag to publish.
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push'
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Build smoke-test image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
load: true
platforms: linux/amd64
push: false
tags: allstarr:release-smoke
build-args: ALLSTARR_VERSION=${{ needs.build-and-test.outputs.app-version }}
cache-from: type=gha
- name: Smoke test built image
shell: bash
run: |
set -euo pipefail
smoke_dir="${RUNNER_TEMP}/allstarr-release-smoke"
container_name="allstarr-release-smoke"
postgres_name="allstarr-release-smoke-postgres"
network_name="allstarr-release-smoke"
mkdir -p "${smoke_dir}/state" "${smoke_dir}/downloads" "${smoke_dir}/kept"
printf '%s\n' 'ci-postgres-password' > "${smoke_dir}/postgres-password"
key="$(openssl rand -base64 32)"
printf '{"activeKeyId":"key-1","keys":{"key-1":"%s"}}\n' "${key}" > "${smoke_dir}/allstarr-keyring.json"
chmod 600 "${smoke_dir}/postgres-password" "${smoke_dir}/allstarr-keyring.json"
cleanup() {
docker logs "${container_name}" || true
docker rm --force "${container_name}" >/dev/null 2>&1 || true
docker logs "${postgres_name}" || true
docker rm --force "${postgres_name}" >/dev/null 2>&1 || true
docker network rm "${network_name}" >/dev/null 2>&1 || true
rm -rf "${smoke_dir}"
}
trap cleanup EXIT
docker network create "${network_name}"
docker run --detach \
--name "${postgres_name}" \
--network "${network_name}" \
--env 'POSTGRES_DB=allstarr' \
--env 'POSTGRES_USER=allstarr' \
--env 'POSTGRES_PASSWORD=ci-postgres-password' \
postgres:18.4-alpine3.23@sha256:996d0920e4ff9df1fc19dacb904492f3c1ec0ec1cc338f0ad7123be7731c5f5e
for attempt in {1..30}; do
if docker exec "${postgres_name}" pg_isready -U allstarr -d allstarr >/dev/null; then
break
fi
if [ "${attempt}" -eq 30 ]; then
echo 'Smoke-test PostgreSQL did not become ready.' >&2
exit 1
fi
sleep 2
done
docker run --detach \
--name "${container_name}" \
--network "${network_name}" \
--volume "${smoke_dir}/state:/app/state" \
--volume "${smoke_dir}/downloads:/app/downloads" \
--volume "${smoke_dir}/kept:/app/kept" \
--volume "${smoke_dir}/postgres-password:/run/secrets/postgres_password:ro" \
--volume "${smoke_dir}/allstarr-keyring.json:/run/secrets/allstarr_keyring:ro" \
--env 'ASPNETCORE_ENVIRONMENT=Production' \
--env 'Backend__Type=Jellyfin' \
--env 'Storage__Provider=Postgres' \
--env "Storage__ConnectionString=Host=${postgres_name};Port=5432;Database=allstarr;Username=allstarr;SSL Mode=Disable" \
--env 'Storage__PasswordFile=/run/secrets/postgres_password' \
--env 'Storage__AutoMigrate=true' \
--env 'Secrets__KeyRingPath=/run/secrets/allstarr_keyring' \
allstarr:release-smoke
for attempt in {1..60}; do
if docker exec "${container_name}" \
curl --fail --silent http://127.0.0.1:8080/health/ready >/dev/null; then
exit 0
fi
if [ "$(docker inspect --format '{{.State.Running}}' "${container_name}")" != 'true' ]; then
echo 'Smoke-test container exited before becoming ready.' >&2
exit 1
fi
sleep 2
done
echo 'Smoke-test container did not become ready within 120 seconds.' >&2
exit 1
- name: Login to GitHub Container Registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' }}
- name: Build and push
id: publish
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: ALLSTARR_VERSION=${{ needs.build-and-test.outputs.app-version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Verify published manifest digest
shell: bash
env:
PUBLISHED_DIGEST: ${{ steps.publish.outputs.digest }}
PUBLISHED_TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
mapfile -t published_tags < <(printf '%s\n' "${PUBLISHED_TAGS}" | sed '/^[[:space:]]*$/d')
if [ "${#published_tags[@]}" -eq 0 ] || [[ ! "${PUBLISHED_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo 'Publish action did not return a usable tag and digest.' >&2
exit 1
fi
for published_tag in "${published_tags[@]}"; do
verified=false
for attempt in {1..12}; do
manifest_output="$(docker buildx imagetools inspect "${published_tag}" 2>/dev/null || true)"
remote_digest="$(printf '%s\n' "${manifest_output}" | awk '/^Digest:/ { print $2; exit }')"
if [ "${remote_digest}" = "${PUBLISHED_DIGEST}" ]; then
printf 'Verified %s at %s\n' "${published_tag}" "${PUBLISHED_DIGEST}"
verified=true
break
fi
sleep 5
done
if [ "${verified}" != 'true' ]; then
echo "Published tag ${published_tag} did not resolve to ${PUBLISHED_DIGEST}." >&2
exit 1
fi
done