diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8f02700..23c35eb 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,28 +6,20 @@ on: - next jobs: - build: + # Prepare metadata and tags for all builds + prepare: runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - + outputs: + is_release: ${{ steps.check-release.outputs.is_release }} + release_tag: ${{ steps.check-release.outputs.release_tag }} + tags: ${{ steps.tags.outputs.tags }} + image: ${{ steps.tags.outputs.image }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup Turborepo cache - uses: actions/cache@v4 - with: - path: .turbo - key: ${{ runner.os }}-turbo-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-turbo- - - name: Check if this commit is a release id: check-release run: | @@ -64,26 +56,11 @@ jobs: fi fi - - name: Free disk space - run: | - sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL - docker system prune -af --volumes - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - buildkitd-flags: --debug - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Compute tags id: tags run: | + IMAGE="ghcr.io/${{ github.repository }}" + if [[ "${{ steps.check-release.outputs.is_release }}" == "true" ]]; then # This is a release: use semver tags TAG="${{ steps.check-release.outputs.release_tag }}" @@ -99,31 +76,145 @@ jobs: echo "Building COMMIT with tags: $TAGS" fi echo "tags=$TAGS" >> $GITHUB_OUTPUT + echo "image=$IMAGE" >> $GITHUB_OUTPUT - - name: Build and push + # Build natively on each platform (faster, no QEMU emulation) + build: + needs: prepare + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Turborepo cache + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-${{ runner.arch }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-turbo- + + - name: Free disk space run: | - IMAGE="ghcr.io/${{ github.repository }}" - TAGS="${{ steps.tags.outputs.tags }}" + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + docker system prune -af --volumes - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --push \ - $(echo "$TAGS" | tr ',' '\n' | sed "s|^|--tag ${IMAGE}:|") \ - --cache-from type=gha \ - --cache-to type=gha,mode=max \ - --build-arg BUILDTIME="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ - --build-arg VERSION="${{ github.ref_name }}" \ - --build-arg REVISION="${{ github.sha }}" \ - . + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ needs.prepare.outputs.image }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + push: true + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ needs.prepare.outputs.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 }} + build-args: | + BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + VERSION=${{ github.ref_name }} + REVISION=${{ github.sha }} + + - 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-${{ strategy.job-index }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + # Merge platform-specific images into multi-arch manifest + merge: + needs: [prepare, build] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + IMAGE="${{ needs.prepare.outputs.image }}" + TAGS="${{ needs.prepare.outputs.tags }}" + + # Convert comma-separated tags to array and create docker tag arguments + TAG_ARGS="" + IFS=',' read -ra TAG_ARRAY <<< "$TAGS" + for tag in "${TAG_ARRAY[@]}"; do + TAG_ARGS="$TAG_ARGS -t ${IMAGE}:${tag}" + done + + # Create and push manifest using all digests + docker buildx imagetools create $TAG_ARGS \ + $(printf '${{ needs.prepare.outputs.image }}@sha256:%s ' *) + + - name: Inspect image + run: | + IMAGE="${{ needs.prepare.outputs.image }}" + TAGS="${{ needs.prepare.outputs.tags }}" + FIRST_TAG=$(echo "$TAGS" | cut -d',' -f1) + docker buildx imagetools inspect ${IMAGE}:${FIRST_TAG} - name: Summary run: | - IMAGE="ghcr.io/${{ github.repository }}" - TAGS="${{ steps.tags.outputs.tags }}" + IMAGE="${{ needs.prepare.outputs.image }}" + TAGS="${{ needs.prepare.outputs.tags }}" echo "## Docker Image Published 🚀" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - if [[ "${{ steps.check-release.outputs.is_release }}" == "true" ]]; then - echo "**Type:** Release (${{ steps.check-release.outputs.release_tag }})" >> $GITHUB_STEP_SUMMARY + if [[ "${{ needs.prepare.outputs.is_release }}" == "true" ]]; then + echo "**Type:** Release (${{ needs.prepare.outputs.release_tag }})" >> $GITHUB_STEP_SUMMARY else echo "**Type:** Commit (sha-${GITHUB_SHA:0:7})" >> $GITHUB_STEP_SUMMARY fi diff --git a/Dockerfile b/Dockerfile index b3ec2cd..f274d88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -182,6 +182,8 @@ RUN mkdir -p \ # Stage 3: Production Runtime # ============================================ FROM node:20-alpine AS runner +ARG TARGETPLATFORM +ARG BUILDPLATFORM WORKDIR /app # Install OpenSSL for Prisma, curl for health checks, nginx, and gettext (for envsubst)