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 ' *)
