feat: mirror a lossless library to MP3 for iPod sync #1
@@ -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