mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-06 21:55:39 -04:00
290 lines
11 KiB
YAML
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
|