diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 00000000..6c49b22e --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,223 @@ +name: docker + +# Builds the streamplace container images and publishes them to ghcr.io. +# +# ghcr.io/streamplace/streamplace:next - release image (amd64+arm64) +# ghcr.io/streamplace/streamplace: - immutable release image +# ghcr.io/streamplace/streamplace:latest - newest tagged release +# ghcr.io/streamplace/streamplace:next-mistserver - MistServer companion (amd64) +# ghcr.io/streamplace/streamplace:bunny - leak-test fixture image (amd64) +# ghcr.io/streamplace/streamplace:builder - cross-compile toolchain (amd64) +# +# Pushes happen on `next` and on `v*` tags. Pull requests build everything but +# do not push, so the Dockerfiles stay validated without publishing. + +on: + pull_request: + push: + branches: + - next + tags: + - "v*" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +env: + REGISTRY_IMAGE: ghcr.io/streamplace/streamplace + +jobs: + # The cross-compilation toolchain image, published so it can be pulled for + # complex builds locally. amd64 only: the toolchain (clang/llvm, golangci, + # aptly, the winehq apt repo) is x86-64. Layer-cached to ghcr so an unchanged + # docker/build.Dockerfile is a fast cache hit. + builder: + name: builder image + runs-on: ubuntu-latest + steps: + - name: Maximize build space + run: + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + /opt/hostedtoolcache/CodeQL + + - name: Check out code + uses: actions/checkout@v4.1.7 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Compute tags + id: tags + run: | + short=${GITHUB_SHA::7} + echo "tags=${REGISTRY_IMAGE}:builder,${REGISTRY_IMAGE}:builder-${short}" >> "$GITHUB_OUTPUT" + + - name: Build and push builder + uses: docker/build-push-action@v6 + with: + context: . + file: docker/build.Dockerfile + target: builder + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.tags.outputs.tags }} + # The builder image is large; use a dedicated registry cache tag + # (no 10GB cap, unlike the GitHub Actions cache). + cache-from: + type=registry,ref=${{ env.REGISTRY_IMAGE }}:builder-buildcache + cache-to: + ${{ github.event_name != 'pull_request' && + format('type=registry,ref={0}:builder-buildcache,mode=max', + env.REGISTRY_IMAGE) || '' }} + + # Cross-compile the linux binaries inside the builder container (the same + # mechanism as build.yaml) and hand them to the image jobs as an artifact. + binaries: + name: linux binaries + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Maximize build space + run: + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + /opt/hostedtoolcache/CodeQL + + - name: Check out code + uses: actions/checkout@v4.1.7 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Log in to ghcr.io (build-layer cache) + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Cross-compile linux amd64 + arm64 + run: | + sudo apt install podman -y + make in-container \ + BUILDER_TARGET=builder-no-darwin \ + DOCKER_PWD_MOUNT_PATH=/app \ + DOCKER_BUILD_OPTS="--layers --cache-to ghcr.io/streamplace/streamplace --cache-from ghcr.io/streamplace/streamplace" \ + DOCKER_OPTS="-e CI=true -e GITHUB_ACTION=true" \ + IN_CONTAINER_CMD="make linux-amd64 && make linux-arm64 && go run ./pkg/config/git/git.go -v > sp-version.txt" + + - name: Capture version + id: version + run: echo "version=$(cat sp-version.txt)" >> "$GITHUB_OUTPUT" + + - name: Upload binaries + uses: actions/upload-artifact@v4 + with: + name: streamplace-linux-binaries + path: | + build-linux-amd64/streamplace + build-linux-arm64/streamplace + retention-days: 1 + if-no-files-found: error + + # Assemble and publish the product images from the freshly-built binaries. + images: + name: ${{ matrix.image }} image + runs-on: ubuntu-latest + needs: binaries + strategy: + fail-fast: false + matrix: + include: + - image: release + file: docker/release.ghcr.Dockerfile + platforms: linux/amd64,linux/arm64 + suffix: "" + - image: mistserver + file: docker/mistserver.ghcr.Dockerfile + platforms: linux/amd64 + suffix: "-mistserver" + - image: bunny + file: docker/bunny.Dockerfile + platforms: linux/amd64 + suffix: "-bunny" + steps: + - name: Check out code + uses: actions/checkout@v4.1.7 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Download binaries + uses: actions/download-artifact@v4 + with: + name: streamplace-linux-binaries + path: . + + - name: Stage build context + run: | + mkdir -p stage/build-linux-amd64 stage/build-linux-arm64 + cp build-linux-amd64/streamplace stage/build-linux-amd64/streamplace + cp build-linux-arm64/streamplace stage/build-linux-arm64/streamplace + cp docker/mistserver.json stage/mistserver.json + cp docker/bunny.sh stage/bunny.sh + + - uses: docker/setup-qemu-action@v3 + if: contains(matrix.platforms, 'arm64') + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Compute image tags + id: tags + env: + SUFFIX: ${{ matrix.suffix }} + VERSION: ${{ needs.binaries.outputs.version }} + run: | + set -euo pipefail + tags=("${REGISTRY_IMAGE}:${VERSION}${SUFFIX}") + if [ "${{ github.event_name }}" = "pull_request" ]; then + tags+=("${REGISTRY_IMAGE}:pr-${{ github.event.number }}${SUFFIX}") + elif [ "${{ github.ref_type }}" = "tag" ]; then + tags+=("${REGISTRY_IMAGE}:latest${SUFFIX}") + else + tags+=("${REGISTRY_IMAGE}:${{ github.ref_name }}${SUFFIX}") + fi + # bunny gets a stable, version-independent tag (its contents never change). + if [ "${{ matrix.image }}" = "bunny" ]; then + tags+=("${REGISTRY_IMAGE}:bunny") + fi + printf -v joined '%s,' "${tags[@]}" + echo "tags=${joined%,}" >> "$GITHUB_OUTPUT" + echo "tags: ${joined%,}" + + - name: Build and push ${{ matrix.image }} + uses: docker/build-push-action@v6 + with: + context: stage + file: ${{ matrix.file }} + platforms: ${{ matrix.platforms }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha,scope=${{ matrix.image }} + cache-to: type=gha,mode=max,scope=${{ matrix.image }} diff --git a/docker/mistserver.ghcr.Dockerfile b/docker/mistserver.ghcr.Dockerfile new file mode 100644 index 00000000..3e5e2b2a --- /dev/null +++ b/docker/mistserver.ghcr.Dockerfile @@ -0,0 +1,12 @@ +# amd64-only MistServer companion image published to ghcr.io by +# .github/workflows/docker.yaml. amd64 only because the upstream MistServer +# release (mistserver_64) is x86-64. The streamplace binary is baked in from the +# build context; see docker/release.ghcr.Dockerfile for the rationale. +FROM --platform=linux/amd64 ubuntu:24.04 +RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* +COPY build-linux-amd64/streamplace /usr/local/bin/streamplace +RUN chmod +x /usr/local/bin/streamplace +RUN cd /usr/bin && curl -L -o - https://r.mistserver.org/dl/mistserver_64V3.7.tar.gz | tar xzv +RUN mkdir -p /config +ADD mistserver.json /config/mistserver.json +CMD ["MistController", "-c", "/config/mistserver.json"] diff --git a/docker/release.ghcr.Dockerfile b/docker/release.ghcr.Dockerfile new file mode 100644 index 00000000..06f2e59a --- /dev/null +++ b/docker/release.ghcr.Dockerfile @@ -0,0 +1,24 @@ +# Multi-arch release image published to ghcr.io by .github/workflows/docker.yaml. +# +# Unlike docker/release.Dockerfile (which curls a prebuilt tarball from a +# package registry), the streamplace binary is baked in from the build context +# that the workflow just cross-compiled, so there is no external download +# dependency. buildx populates TARGETARCH per platform, selecting the matching +# binary that the workflow staged under build-linux-/. +ARG TARGETARCH +FROM --platform=linux/$TARGETARCH ubuntu:24.04 +RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* +ARG TARGETARCH +ARG BUILDARCH +COPY build-linux-${TARGETARCH}/streamplace /usr/local/bin/streamplace +# upload-artifact drops the executable bit, so restore it. Only self-test on the +# native arch — under QEMU emulation (e.g. arm64 built on an amd64 runner) the +# self-test is slow and flaky, so skip it there. +RUN chmod +x /usr/local/bin/streamplace \ + && if [ "$TARGETARCH" = "$BUILDARCH" ]; then \ + streamplace self-test; \ + else \ + echo "skipping self-test: $TARGETARCH image built under emulation on $BUILDARCH"; \ + fi +ENV SP_DATA_DIR=/var/lib/streamplace +CMD ["streamplace"]