diff --git a/.gitea/workflows/build-and-publish.yaml b/.gitea/workflows/build-and-publish.yaml new file mode 100644 index 0000000..057279f --- /dev/null +++ b/.gitea/workflows/build-and-publish.yaml @@ -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- 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}" diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml deleted file mode 100644 index c73f41d..0000000 --- a/.gitea/workflows/ci.yaml +++ /dev/null @@ -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 . diff --git a/README.md b/README.md index 8e7fa45..f070bf5 100644 --- a/README.md +++ b/README.md @@ -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-`. 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 diff --git a/compose.yaml b/compose.yaml index 0e12800..a157ab5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -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