name: Build and Push on: push: branches: [main] workflow_dispatch: inputs: service: description: "Service to build (all, backend, consumer, worker, tracking, realtime)" required: false default: "all" env: REGISTRY: ghcr.io IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/warmbly concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: changes: name: Detect Changes runs-on: ubuntu-latest outputs: # JSON lists consumed as matrix inputs. `go` services cross-compile both # arches on one runner; `native` services (Rust, Elixir) have no # cross-compiler, so each arch builds on a native runner and the digests # are merged into one manifest. go: ${{ steps.matrix.outputs.go }} native: ${{ steps.matrix.outputs.native }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 if: github.event_name == 'push' id: filter with: filters: | backend: - 'go.mod' - 'go.sum' - 'internal/**' - 'cmd/backend/**' - 'deploy/docker/backend.Dockerfile' consumer: - 'go.mod' - 'go.sum' - 'internal/**' - 'cmd/consumer/**' - 'deploy/docker/consumer.Dockerfile' worker: - 'go.mod' - 'go.sum' - 'internal/**' - 'cmd/worker/**' - 'deploy/docker/worker.Dockerfile' tracking: - 'tracking/**' realtime: - 'realtime/**' - 'deploy/docker/realtime.Dockerfile' - name: Assemble build matrices id: matrix env: EVENT: ${{ github.event_name }} SELECTED: ${{ github.event.inputs.service }} BACKEND: ${{ steps.filter.outputs.backend }} CONSUMER: ${{ steps.filter.outputs.consumer }} WORKER: ${{ steps.filter.outputs.worker }} TRACKING: ${{ steps.filter.outputs.tracking }} REALTIME: ${{ steps.filter.outputs.realtime }} run: | go="" native="" if [ "$EVENT" = "workflow_dispatch" ]; then sel="${SELECTED:-all}" for s in backend consumer worker; do if [ "$sel" = "all" ] || [ "$sel" = "$s" ]; then go="$go\"$s\","; fi done for s in tracking realtime; do if [ "$sel" = "all" ] || [ "$sel" = "$s" ]; then native="$native\"$s\","; fi done else if [ "$BACKEND" = "true" ]; then go="$go\"backend\","; fi if [ "$CONSUMER" = "true" ]; then go="$go\"consumer\","; fi if [ "$WORKER" = "true" ]; then go="$go\"worker\","; fi if [ "$TRACKING" = "true" ]; then native="$native\"tracking\","; fi if [ "$REALTIME" = "true" ]; then native="$native\"realtime\","; fi fi echo "go=[${go%,}]" >> "$GITHUB_OUTPUT" echo "native=[${native%,}]" >> "$GITHUB_OUTPUT" # Go services: the Dockerfiles build on $BUILDPLATFORM and cross-compile to # each target arch, so one amd64 runner produces both platforms without QEMU. build-go: name: Build ${{ matrix.service }} needs: changes if: needs.changes.outputs.go != '[]' strategy: fail-fast: false matrix: service: ${{ fromJSON(needs.changes.outputs.go) }} runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v6 with: context: . file: deploy/docker/${{ matrix.service }}.Dockerfile push: true tags: | ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.sha }} ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:dev platforms: linux/amd64,linux/arm64 cache-from: type=gha,scope=${{ matrix.service }} cache-to: type=gha,mode=max,scope=${{ matrix.service }} # Rust (tracking) and Elixir (realtime) have no cross-compiler; an emulated # arm64 build under QEMU runs for an hour or more. Build each arch on a # native runner and merge the digests into one manifest (the # Docker-documented multi-runner pattern). build-native: name: Build ${{ matrix.service }} (${{ matrix.platform }}) needs: changes if: needs.changes.outputs.native != '[]' strategy: fail-fast: false matrix: service: ${{ fromJSON(needs.changes.outputs.native) }} platform: [linux/amd64, linux/arm64] runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Prepare platform pair id: prep run: echo "pair=${PLATFORM//\//-}" >> "$GITHUB_OUTPUT" env: PLATFORM: ${{ matrix.platform }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: context: ${{ matrix.service == 'tracking' && './tracking' || '.' }} file: ${{ matrix.service == 'tracking' && './tracking/Dockerfile' || format('deploy/docker/{0}.Dockerfile', matrix.service) }} platforms: ${{ matrix.platform }} cache-from: type=gha,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }} cache-to: type=gha,mode=max,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }} outputs: type=image,name=${{ env.IMAGE_PREFIX }}/${{ matrix.service }},push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-${{ matrix.service }}-${{ steps.prep.outputs.pair }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge-native: name: Merge ${{ matrix.service }} manifest needs: [changes, build-native] if: needs.changes.outputs.native != '[]' strategy: fail-fast: false matrix: service: ${{ fromJSON(needs.changes.outputs.native) }} runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/digests pattern: digests-${{ matrix.service }}-* merge-multiple: true - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create \ -t ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.sha }} \ -t ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:dev \ $(printf '${{ env.IMAGE_PREFIX }}/${{ matrix.service }}@sha256:%s ' *)