Build and publish container / build (pull_request) Canceled after 14m59s
The deployment target is a container on TrueNAS Scale, so the flake sat on no path between the source and the NAS. It was carried over from a sibling project where the flake is the deployment mechanism; here it only added a second build path and a second dependency pin. Worse, it tested the wrong thing: `nix flake check` ran the suite against nixpkgs' ffmpeg while the shipped artefact contains Debian's, and encoder and muxer behaviour is exactly what these tests cover. The Dockerfile gains a `test` stage that installs pytest and runs the suite against the image's own ffmpeg; a failing test fails the build. CI runs `docker build --target test` in place of the host-based Python setup, and the push build states `target: runtime` so the published image is the lean stage rather than the last one in the file. The runtime image carries neither the tests nor pytest. The release step now calls python3 rather than python, since setup-python is no longer in the job to provide the alias. Removes package.nix, flake.nix and flake.lock. A dev shell is a `nix shell` away for anyone who wants one, and the README says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
196 lines
7.3 KiB
YAML
196 lines
7.3 KiB
YAML
name: Build and publish container
|
|
|
|
on:
|
|
# On merge to main, only build/release when image-affecting files change;
|
|
# CI-config and docs changes do not produce a new image. pyproject.toml is
|
|
# deliberately absent: the release step below commits to it, and that commit
|
|
# must not start another run.
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "Dockerfile"
|
|
- ".dockerignore"
|
|
- "music_mirror.py"
|
|
# Pull requests always run (tests and the image build are the checks); no
|
|
# path filter.
|
|
pull_request:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
# A newer run cancels an older in-flight run in the same group (keyed by ref),
|
|
# so a fresh merge to main supersedes the previous build and only the latest
|
|
# release is produced. Each pull request likewise supersedes only its own
|
|
# earlier runs.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
# Full history and tags are required to derive the next version
|
|
# from the conventional-commit messages since the last release.
|
|
fetch-depth: 0
|
|
|
|
# The suite runs inside the image, against the ffmpeg that ships, rather
|
|
# than against whatever the runner happens to provide. A failing test
|
|
# fails the build. Layers are shared with the push build below.
|
|
- name: Run the test suite inside the image
|
|
run: docker build --target test -t music-mirror:test .
|
|
|
|
- name: Determine registry host
|
|
run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV"
|
|
|
|
# Derive the release version from conventional commits since the last
|
|
# v* tag: feat -> minor, fix/perf -> patch, ! or BREAKING CHANGE -> major.
|
|
# Anything else (chore, ci, docs, build) produces no release; those builds
|
|
# are published under a sha-<short> tag only.
|
|
- name: Compute version and image tags
|
|
id: version
|
|
run: |
|
|
set -euo pipefail
|
|
image="${REGISTRY}/${GITHUB_REPOSITORY,,}"
|
|
|
|
last_tag="$(git tag --list 'v*' --sort=-v:refname | head -n1 || true)"
|
|
if [ -n "$last_tag" ]; then
|
|
range="${last_tag}..HEAD"
|
|
base="${last_tag#v}"
|
|
else
|
|
range=""
|
|
base="0.0.0"
|
|
fi
|
|
|
|
subjects="$(git log ${range} --format='%s')"
|
|
bodies="$(git log ${range} --format='%B')"
|
|
|
|
bump="none"
|
|
if printf '%s\n' "$bodies" | grep -qiE 'BREAKING[ -]CHANGE' \
|
|
|| printf '%s\n' "$subjects" | grep -qE '^[a-z]+([(][^)]*[)])?!:'; then
|
|
bump="major"
|
|
elif printf '%s\n' "$subjects" | grep -qE '^feat([(][^)]*[)])?:'; then
|
|
bump="minor"
|
|
elif printf '%s\n' "$subjects" | grep -qE '^(fix|perf)([(][^)]*[)])?:'; then
|
|
bump="patch"
|
|
fi
|
|
|
|
major="${base%%.*}"
|
|
rest="${base#*.}"
|
|
minor="${rest%%.*}"
|
|
patch="${rest##*.}"
|
|
|
|
release="false"
|
|
if [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "$bump" != "none" ]; then
|
|
release="true"
|
|
case "$bump" in
|
|
major) major=$((major + 1)); minor=0; patch=0 ;;
|
|
minor) minor=$((minor + 1)); patch=0 ;;
|
|
patch) patch=$((patch + 1)) ;;
|
|
esac
|
|
version="${major}.${minor}.${patch}"
|
|
{
|
|
echo "tags<<__EOT__"
|
|
echo "${image}:${version}"
|
|
echo "${image}:${major}.${minor}"
|
|
echo "${image}:${major}"
|
|
echo "${image}:latest"
|
|
echo "__EOT__"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
short="$(git rev-parse --short HEAD)"
|
|
{
|
|
echo "tags<<__EOT__"
|
|
echo "${image}:sha-${short}"
|
|
echo "__EOT__"
|
|
} >> "$GITHUB_OUTPUT"
|
|
fi
|
|
echo "release=${release}" >> "$GITHUB_OUTPUT"
|
|
echo "Computed bump=${bump}, release=${release}, base=${base}"
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4
|
|
|
|
- name: Set up Buildx
|
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
|
|
|
- name: Log in to the Gitea container registry
|
|
if: github.event_name != 'pull_request'
|
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.PACKAGES_SECRET }}
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
|
with:
|
|
context: .
|
|
# Without this the last stage in the Dockerfile -- the test stage --
|
|
# would be what gets published.
|
|
target: runtime
|
|
# amd64 for the NAS, arm64 so the same image runs on a Pi.
|
|
platforms: linux/amd64,linux/arm64
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.version.outputs.tags }}
|
|
labels: |
|
|
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
|
|
# Record the release: write the computed version into pyproject.toml, then
|
|
# commit and tag it, so the packaging metadata always matches the release
|
|
# instead of drifting behind it. The version is derived from commit
|
|
# messages and only known here, after the build, so it cannot be set by
|
|
# hand in the pull request that causes the release.
|
|
- name: Record and tag the release
|
|
if: steps.version.outputs.release == 'true'
|
|
env:
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
python3 - "$VERSION" <<'PY'
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
version = sys.argv[1]
|
|
path = pathlib.Path("pyproject.toml")
|
|
text = path.read_text()
|
|
text, count = re.subn(
|
|
r'(?m)^version = ".*"$', f'version = "{version}"', text, count=1
|
|
)
|
|
if count != 1:
|
|
raise SystemExit("no version line found in pyproject.toml")
|
|
path.write_text(text)
|
|
PY
|
|
|
|
git config user.name "${{ github.actor }}"
|
|
git config user.email "${{ github.actor }}@users.noreply.${REGISTRY}"
|
|
git add pyproject.toml
|
|
|
|
# The file may already carry this version, in which case there is
|
|
# nothing to commit and `git commit` would fail the job.
|
|
if git diff --cached --quiet; then
|
|
echo "pyproject.toml is already at ${VERSION}"
|
|
else
|
|
git commit -m "chore(release): v${VERSION}"
|
|
# Push the branch before the tag. If main has moved on and this push
|
|
# is rejected, the job fails without having left a tag pointing at a
|
|
# commit that is not on main.
|
|
git push origin "HEAD:${GITHUB_REF_NAME}"
|
|
fi
|
|
|
|
git tag -a "v${VERSION}" -m "v${VERSION}"
|
|
git push origin "v${VERSION}"
|