diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7e44e722..ca0bb160 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -68,21 +68,30 @@ jobs: echo "services=$SERVICES" >> "$GITHUB_OUTPUT" echo "Building services: $SERVICES" - build-and-push: - runs-on: ubuntu-latest + # One job per service × platform. arm64 runs on a native arm runner instead of + # QEMU emulation on x86 (7× slower). Each job pushes its single-arch image by + # digest; `merge` below stitches the digests into one multi-arch manifest. + build: + runs-on: ${{ matrix.runner }} needs: [prepare] if: needs.prepare.outputs.services != '[]' timeout-minutes: 30 permissions: contents: read - id-token: write packages: write strategy: fail-fast: false matrix: service: ${{ fromJson(needs.prepare.outputs.services) }} + platform: [linux/amd64, linux/arm64] include: + - platform: linux/amd64 + arch: amd64 + runner: ubuntu-latest + - platform: linux/arm64 + arch: arm64 + runner: ubuntu-24.04-arm - service: server context: . dockerfile: apps/server/Dockerfile @@ -110,7 +119,9 @@ jobs: uses: actions/checkout@v6 - name: Lowercase owner - run: echo "OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV + run: | + echo "OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV + echo "IMAGE=${{ env.REGISTRY }}/${GITHUB_REPOSITORY_OWNER,,}/${{ env.IMAGE_NAME }}-${{ matrix.service }}" >> $GITHUB_ENV - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -126,31 +137,113 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.OWNER_LC }}/${{ env.IMAGE_NAME }}-${{ matrix.service }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=sha,prefix= - type=raw,value=latest,enable={{is_default_branch}} + images: ${{ env.IMAGE }} - - name: Build and push Docker image + # Layer cache lives in the registry (one ref per service × arch) rather than + # the 10 GB GitHub Actions cache, which the multi-GB builder layers of the + # Next.js apps kept evicting. Blobs already in the registry are not re-uploaded. + - name: Build and push Docker image (by digest) id: build uses: docker/build-push-action@v6 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} - push: true - tags: ${{ steps.meta.outputs.tags }} + platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max + outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache-${{ matrix.arch }} + cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache-${{ matrix.arch }},mode=max,image-manifest=true,oci-mediatypes=true provenance: false + - 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 }}-${{ matrix.arch }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + # Combine the per-arch digests into a multi-arch manifest list carrying the + # real tags, then generate the SBOM from that manifest. + # Runs even if some `build` legs failed so one broken service does not block + # publishing the others; a service missing a digest fails its own merge below. + merge: + runs-on: ubuntu-latest + needs: [prepare, build] + if: ${{ !cancelled() && needs.prepare.outputs.services != '[]' }} + timeout-minutes: 15 + permissions: + contents: read + id-token: write + packages: write + + strategy: + fail-fast: false + matrix: + service: ${{ fromJson(needs.prepare.outputs.services) }} + + steps: + - name: Lowercase owner + run: | + echo "OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV + echo "IMAGE=${{ env.REGISTRY }}/${GITHUB_REPOSITORY_OWNER,,}/${{ env.IMAGE_NAME }}-${{ matrix.service }}" >> $GITHUB_ENV + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-${{ matrix.service }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Create manifest list and push + id: manifest + working-directory: /tmp/digests + run: | + # Never publish a partial (single-arch) manifest under the real tags. + count=$(ls | wc -l) + if [ "$count" -ne 2 ]; then + echo "::error::expected 2 digests (amd64 + arm64) for ${{ matrix.service }}, found $count" + ls -la + exit 1 + fi + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.IMAGE }}@sha256:%s ' *) + digest=$(docker buildx imagetools inspect "${{ env.IMAGE }}:${{ steps.meta.outputs.version }}" --format '{{json .Manifest.Digest}}' | tr -d '"') + echo "digest=$digest" >> "$GITHUB_OUTPUT" + docker buildx imagetools inspect "${{ env.IMAGE }}@${digest}" + - name: Generate SBOM uses: anchore/sbom-action@v0 with: - image: ${{ env.REGISTRY }}/${{ env.OWNER_LC }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}@${{ steps.build.outputs.digest }} + image: ${{ env.IMAGE }}@${{ steps.manifest.outputs.digest }} format: spdx-json output-file: sbom-${{ matrix.service }}.spdx.json diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile index d6a3b8ee..ce4faba7 100644 --- a/apps/dashboard/Dockerfile +++ b/apps/dashboard/Dockerfile @@ -2,41 +2,85 @@ # This file is generated by Dofigen v2.8.0 # See https://github.com/lenra-io/dofigen +# manifests +FROM node@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 AS manifests +LABEL \ + org.opencontainers.image.base.digest="sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6" \ + org.opencontainers.image.base.name="docker.io/node:24-slim" +WORKDIR /app +COPY \ + --link \ + "." "/app/" +RUN <