ci: publish the container image to the Gitea registry
Build and publish container / build (pull_request) Canceled after 14m40s
Build and publish container / build (pull_request) Canceled after 14m40s
Adopts the release scheme from legacy-email-proxy so both repositories behave the same way: the version is derived from conventional commits since the last v* tag, the image is pushed under the full version, the truncated major.minor and major forms, and latest, and non-release builds are published as sha-<short>. Multi-arch (amd64 for the NAS, arm64 so the same image runs on a Pi). Authentication uses the PACKAGES_SECRET repository secret. The release step also writes the computed version into pyproject.toml and commits it as chore(release) before tagging, so the packaging metadata cannot drift behind the release. It skips the commit when the file already carries that version, which would otherwise fail the job after the image had been pushed. compose.yaml and the README now reference the published image instead of instructing the NAS to build one locally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
177761bb75
commit
82c9da6d62
@@ -0,0 +1,201 @@
|
||||
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
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
# The test suite runs real encodes, so ffmpeg is a test dependency.
|
||||
- name: Install ffmpeg
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y ffmpeg
|
||||
|
||||
- name: Install test dependencies
|
||||
run: python -m pip install --upgrade pip && pip install pytest
|
||||
|
||||
- name: Run unit tests
|
||||
run: python -m pytest
|
||||
|
||||
- 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: .
|
||||
# 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
|
||||
|
||||
python - "$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}"
|
||||
@@ -1,41 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
# The test suite runs real encodes, so ffmpeg is a test dependency.
|
||||
- name: Install ffmpeg
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y ffmpeg
|
||||
|
||||
- name: Install test dependencies
|
||||
run: python -m pip install --upgrade pip && pip install pytest
|
||||
|
||||
- name: Run unit tests
|
||||
run: python -m pytest
|
||||
|
||||
- name: Build the container image
|
||||
run: docker build -t music-mirror:ci .
|
||||
@@ -60,14 +60,16 @@ both provide them.
|
||||
|
||||
## Running it on TrueNAS Scale
|
||||
|
||||
`compose.yaml` is a Custom App definition. Build the image on the NAS, adjust
|
||||
the two host paths and the `user:` to match your pool, then add it as a custom
|
||||
app:
|
||||
`compose.yaml` is a Custom App definition. Adjust the two host paths and the
|
||||
`user:` to match your pool, then add it as a custom app. The image is published
|
||||
to this Gitea's registry on every release:
|
||||
|
||||
```sh
|
||||
git clone https://code.emmathe.dev/lyrathorpe/music-mirror
|
||||
cd music-mirror && docker build -t music-mirror:latest .
|
||||
```
|
||||
code.emmathe.dev/lyrathorpe/music-mirror:latest
|
||||
```
|
||||
|
||||
Tags are `latest`, the full version, and the truncated `major.minor` and
|
||||
`major` forms; builds that are not releases are published as `sha-<short>`.
|
||||
|
||||
Point the mirror at its own dataset rather than a directory inside the music
|
||||
dataset — it is derived data, so it wants its own snapshot policy, its own
|
||||
|
||||
+1
-5
@@ -1,15 +1,11 @@
|
||||
# TrueNAS Scale "Custom App" definition.
|
||||
#
|
||||
# Build the image on the NAS first (there is no published image yet):
|
||||
# git clone https://code.emmathe.dev/lyrathorpe/music-mirror
|
||||
# cd music-mirror && docker build -t music-mirror:latest .
|
||||
#
|
||||
# Adjust the two host paths and the user to match your pool. The source is
|
||||
# mounted read-only on purpose: nothing here should ever be able to write to
|
||||
# the lossless library.
|
||||
services:
|
||||
music-mirror:
|
||||
image: music-mirror:latest
|
||||
image: code.emmathe.dev/lyrathorpe/music-mirror:latest
|
||||
container_name: music-mirror
|
||||
restart: unless-stopped
|
||||
# The dataset owner, so the mirror is not written as root. `id apps` or the
|
||||
|
||||
Reference in New Issue
Block a user