Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ name: Release
# image floats as `beta` rather than `latest`, and the release wears the
# pre-release badge. Same rule as the desktop repository.
#
# The image goes to two registries, GHCR and Docker Hub, from one build: each
# architecture is pushed to both by digest, and one manifest list is written
# under every tag in both. A pull from either therefore resolves to the same
# digest — two builds would have produced two images that only share a name.
# Docker Hub is reached with the `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`
# repository secrets; without them the build stops at its login rather than
# publishing to one registry and announcing two.
#
# The tag is the source of truth for the version. `Cargo.toml` is not read
# here — a tag that disagrees with it would produce an image labelled one way
# and reporting itself another, so the two are checked against each other
Expand Down Expand Up @@ -37,6 +45,10 @@ concurrency:

env:
REGISTRY: ghcr.io
# Docker Hub is spelled out rather than derived from `github.repository`:
# the GitHub owner and the Docker Hub namespace are two accounts that only
# happen to share a name. Lowercase, which Docker Hub requires as GHCR does.
DOCKERHUB_IMAGE: docker.io/instazdll/waveflow-server

jobs:
# Cheap, and it runs before any build minute is spent: a tag that does not
Expand Down Expand Up @@ -115,6 +127,12 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
Expand All @@ -123,7 +141,11 @@ jobs:
# Pushed by digest; the tags are attached by the merge job. Pushing a
# tag from each runner would leave whichever finished last as the
# whole image rather than one architecture of it.
outputs: type=image,name=${{ needs.verify.outputs.image }},push-by-digest=true,name-canonical=true,push=true
#
# Both names in one output, quoted because the comma between them
# would otherwise end the `name` field: one build, so one digest,
# pushed to each registry.
outputs: type=image,"name=${{ needs.verify.outputs.image }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}

Expand All @@ -148,6 +170,17 @@ jobs:
needs: [verify, build]
runs-on: ubuntu-24.04
timeout-minutes: 15
# The run's own group is per tag, but `beta` and `latest` are shared by
# every tag: two releases publishing at once could interleave their
# writes and leave the floating tag on one version in GHCR and another on
# Docker Hub. Only this job writes tags, so only this job is serialized;
# the builds still run side by side. `queue: max` keeps every waiting
# publish in line: by default GitHub keeps only one, and a third release
# arriving meanwhile would cancel the second's publish outright.
concurrency:
group: release-floating-tags
cancel-in-progress: false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
queue: max
permissions:
contents: read
packages: write
Expand All @@ -166,14 +199,26 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

# `latest` is deliberately absent. It is a pre-release: a reader who
# pulls `latest` should get a stable build or nothing, never a beta they
# did not ask for. The floating tag for this line is `beta`.
#
# One `imagetools create` writes the manifest list under every tag in
# both registries, so the list itself is the same object everywhere.
# Its sources are named in GHCR only; the build pushed each architecture
# to Docker Hub too, so nothing has to cross between the registries.
- name: Create the manifest
working-directory: /tmp/digests
run: |
version="${{ needs.verify.outputs.version }}"
image="${{ needs.verify.outputs.image }}"
hub_image="${{ env.DOCKERHUB_IMAGE }}"
case "$version" in
*-*) extra="beta" ;;
*) extra="latest" ;;
Expand All @@ -192,8 +237,19 @@ jobs:
docker buildx imagetools create \
--tag "$image:$version" \
--tag "$image:$extra" \
--tag "$hub_image:$version" \
--tag "$hub_image:$extra" \
Comment thread
InstaZDLL marked this conversation as resolved.
"${refs[@]}"
docker buildx imagetools inspect "$image:$version"
# Read back rather than assumed: the promise is that both registries
# answer the same digest, and a mismatch is a failed release.
ghcr_digest="$(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "$image:$version")"
hub_digest="$(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "$hub_image:$version")"
echo "ghcr=$ghcr_digest hub=$hub_digest"
if [ "$ghcr_digest" != "$hub_digest" ]; then
echo "::error::GHCR answers $ghcr_digest but Docker Hub answers $hub_digest"
exit 1
fi

# The GitHub release comes last, once the image it announces exists. A
# release page whose pull command 404s is worse than no page.
Expand All @@ -216,6 +272,7 @@ jobs:
TAG: ${{ inputs.tag || github.ref_name }}
VERSION: ${{ needs.verify.outputs.version }}
IMAGE: ${{ needs.verify.outputs.image }}
HUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
run: |
case "$VERSION" in
*-*) release_flag="--prerelease" ; channel="beta" ;;
Expand All @@ -225,8 +282,8 @@ jobs:
# printf with a single-quoted format rather than a heredoc: the body
# is mostly backticks, and every one of them would be a command
# substitution in an unquoted here-document.
printf '## Container image\n\n```bash\ndocker pull %s:%s\n```\n\nAlso published as `%s`, for `linux/amd64` and `linux/arm64`.\n' \
"$IMAGE" "$VERSION" "$channel" > notes.md
printf '## Container image\n\n```bash\ndocker pull %s:%s\n```\n\nThe same image, digest for digest, is on Docker Hub:\n\n```bash\ndocker pull %s:%s\n```\n\nAlso published as `%s` in both, for `linux/amd64` and `linux/arm64`.\n' \
"$IMAGE" "$VERSION" "${HUB_IMAGE#docker.io/}" "$VERSION" "$channel" > notes.md

# `--generate-notes` appends the merged pull requests below this, so
# the install line stays at the top where it is useful.
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<img src="https://img.shields.io/badge/rust-1.98%2B-orange?style=flat-square&logo=rust" alt="Rust 1.98+" />
<img src="https://img.shields.io/badge/database-SQLite%20only-003b57?style=flat-square&logo=sqlite&logoColor=white" alt="SQLite only" />
<img src="https://img.shields.io/badge/API-OpenSubsonic-6a4bd6?style=flat-square" alt="OpenSubsonic" />
<img src="https://img.shields.io/badge/docker-ghcr.io-2496ed?style=flat-square&logo=docker&logoColor=white" alt="Docker image on GHCR" />
<img src="https://img.shields.io/badge/docker-ghcr.io%20%7C%20Docker%20Hub-2496ed?style=flat-square&logo=docker&logoColor=white" alt="Docker image on GHCR and Docker Hub" />
<img src="https://img.shields.io/badge/license-AGPL--3.0-green?style=flat-square" alt="License AGPL-3.0" />
</p>

Expand All @@ -39,6 +39,15 @@ docker run -d --name waveflow \
ghcr.io/instazdll/waveflow-server:2.0.0-beta.1
```

The image is published to two registries, for `linux/amd64` and `linux/arm64`, under the same tags and the same digest — pull from whichever your host reaches:

| Registry | Image |
| --- | --- |
| GitHub Container Registry | `ghcr.io/instazdll/waveflow-server` |
| Docker Hub | `docker.io/instazdll/waveflow-server` |

Each release is tagged with its version; `beta` follows the latest pre-release, and `latest` will follow the latest stable release once there is one. Docker Hub carries the releases after `2.0.0-beta.1` — that one and earlier are on GHCR only, which is why the commands here pull from GHCR.

Or with Compose — set `WAVEFLOW_MUSIC_PATH` to your music directory, which is mounted read-only:

```bash
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Pulls the published image. To run your own build instead, uncomment `build`
# below — with it present Compose builds from this checkout rather than pulling,
# which is what a contributor wants and what an operator does not.
# The same image is on Docker Hub as `docker.io/instazdll/waveflow-server`, with
# the same tags and digest, for a host that pulls from there instead.
services:
waveflow:
image: ghcr.io/instazdll/waveflow-server:2.0.0-beta.1
Expand Down
Loading