# This workflow is used to build and push the Docker image for n8nio/n8n and n8nio/runners
#
# - Uses docker-config.mjs for context determination, this determines what needs to be built based on the trigger
# - Uses docker-tags.mjs for tag generation, this generates the tags for the images

name: 'Docker: Build and Push'

env:
  NODE_OPTIONS: '--max-old-space-size=7168'
  NODE_VERSION: '24.15.0'

on:
  schedule:
    - cron: '0 0 * * *'

  workflow_call:
    inputs:
      n8n_version:
        description: 'N8N version to build'
        required: true
        type: string
      release_type:
        description: 'Release type (stable, nightly, dev)'
        required: false
        type: string
        default: 'stable'
      push_enabled:
        description: 'Whether to push the built images'
        required: false
        type: boolean
        default: true

  workflow_dispatch:
    inputs:
      push_enabled:
        description: 'Push image to registry'
        required: false
        type: boolean
        default: true
      success_url:
        description: 'URL to call after the build is successful'
        required: false
        type: string

jobs:
  determine-build-context:
    name: Determine Build Context
    runs-on: ubuntu-latest
    outputs:
      release_type: ${{ steps.context.outputs.release_type }}
      n8n_version: ${{ steps.context.outputs.version }}
      push_enabled: ${{ steps.context.outputs.push_enabled }}
      push_to_docker: ${{ steps.context.outputs.push_to_docker }}
      build_matrix: ${{ steps.context.outputs.build_matrix }}
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Determine build context
        id: context
        env:
          N8N_VERSION: ${{ inputs.n8n_version }}
          RELEASE_TYPE: ${{ inputs.release_type }}
          PUSH_ENABLED: ${{ inputs.push_enabled }}
        run: |
          node .github/scripts/docker/docker-config.mjs \
            --event "${{ github.event_name }}" \
            --pr "${{ github.event.pull_request.number }}" \
            --branch "${{ github.ref_name }}" \
            --version "$N8N_VERSION" \
            --release-type "$RELEASE_TYPE" \
            --push-enabled "$PUSH_ENABLED"

  build-and-push-docker:
    name: Build App, then Build and Push Docker Image (${{ matrix.platform }})
    needs: determine-build-context
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 35
    strategy:
      matrix: ${{ fromJSON(needs.determine-build-context.outputs.build_matrix) }}
    outputs:
      image_ref: ${{ steps.determine-tags.outputs.n8n_primary_tag }}
      primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_primary_tag }}
      runners_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_primary_tag }}
      runners_distroless_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_distroless_primary_tag }}
      n8n_sha_manifest_tag: ${{ steps.determine-tags.outputs.n8n_sha_primary_tag }}
      runners_sha_manifest_tag: ${{ steps.determine-tags.outputs.runners_sha_primary_tag }}
      runners_distroless_sha_manifest_tag: ${{ steps.determine-tags.outputs.runners_distroless_sha_primary_tag }}
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup and Build
        uses: ./.github/actions/setup-nodejs
        with:
          build-command: pnpm build:n8n
          enable-docker-cache: 'true'
        env:
          RELEASE: ${{ needs.determine-build-context.outputs.n8n_version }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

      - name: Determine Docker tags for all images
        id: determine-tags
        run: |
          node .github/scripts/docker/docker-tags.mjs \
            --all \
            --version "${{ needs.determine-build-context.outputs.n8n_version }}" \
            --platform "${{ matrix.docker_platform }}" \
            --sha "${GITHUB_SHA::7}" \
            ${{ needs.determine-build-context.outputs.push_to_docker == 'true' && '--include-docker' || '' }}

          echo "=== Generated Docker Tags ==="
          cat "$GITHUB_OUTPUT" | grep "_tags=" | while IFS='=' read -r key value; do
            echo "${key}: ${value%%,*}..."  # Show first tag for brevity
          done

      - name: Login to Docker registries
        if: needs.determine-build-context.outputs.push_enabled == 'true'
        uses: ./.github/actions/docker-registry-login
        with:
          login-ghcr: true
          login-dockerhub: ${{ needs.determine-build-context.outputs.push_to_docker == 'true' }}
          dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
          dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push n8n Docker image
        id: build-n8n
        uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
        with:
          context: .
          file: ./docker/images/n8n/Dockerfile
          build-args: |
            NODE_VERSION=${{ env.NODE_VERSION }}
            N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }}
            N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }}
          platforms: ${{ matrix.docker_platform }}
          provenance: false # Disabled - using SLSA L3 generator for isolated provenance
          sbom: true
          push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
          tags: ${{ steps.determine-tags.outputs.n8n_tags }}

      - name: Build and push task runners Docker image (Alpine)
        id: build-runners
        uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
        with:
          context: .
          file: ./docker/images/runners/Dockerfile
          build-args: |
            NODE_VERSION=${{ env.NODE_VERSION }}
            N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }}
            N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }}
          platforms: ${{ matrix.docker_platform }}
          provenance: false # Disabled - using SLSA L3 generator for isolated provenance
          sbom: true
          push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
          tags: ${{ steps.determine-tags.outputs.runners_tags }}

      - name: Build and push task runners Docker image (distroless)
        id: build-runners-distroless
        uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
        with:
          context: .
          file: ./docker/images/runners/Dockerfile.distroless
          build-args: |
            NODE_VERSION=${{ env.NODE_VERSION }}
            N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }}
            N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }}
          platforms: ${{ matrix.docker_platform }}
          provenance: false # Disabled - using SLSA L3 generator for isolated provenance
          sbom: true
          push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
          tags: ${{ steps.determine-tags.outputs.runners_distroless_tags }}

  create_multi_arch_manifest:
    name: Create Multi-Arch Manifest
    needs: [determine-build-context, build-and-push-docker]
    runs-on: ubuntu-latest
    if: |
      needs.build-and-push-docker.result == 'success' &&
      needs.determine-build-context.outputs.push_enabled == 'true'
    outputs:
      n8n_digest: ${{ steps.get-digests.outputs.n8n_digest }}
      n8n_image: ${{ steps.get-digests.outputs.n8n_image }}
      runners_digest: ${{ steps.get-digests.outputs.runners_digest }}
      runners_image: ${{ steps.get-digests.outputs.runners_image }}
      runners_distroless_digest: ${{ steps.get-digests.outputs.runners_distroless_digest }}
      runners_distroless_image: ${{ steps.get-digests.outputs.runners_distroless_image }}
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

      - name: Login to Docker registries
        uses: ./.github/actions/docker-registry-login
        with:
          login-ghcr: true
          login-dockerhub: ${{ needs.determine-build-context.outputs.push_to_docker == 'true' }}
          dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
          dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Create GHCR multi-arch manifests
        run: |
          RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}"

          # Function to create manifest for an image
          create_manifest() {
            local IMAGE_NAME=$1
            local MANIFEST_TAG=$2

            if [[ -z "$MANIFEST_TAG" ]]; then
              echo "Skipping $IMAGE_NAME - no manifest tag"
              return
            fi

            echo "Creating GHCR manifest for $IMAGE_NAME: $MANIFEST_TAG"

            # For branch builds, only AMD64 is built
            if [[ "$RELEASE_TYPE" == "branch" ]]; then
              docker buildx imagetools create \
                --tag "$MANIFEST_TAG" \
                "${MANIFEST_TAG}-amd64"
            else
              docker buildx imagetools create \
                --tag "$MANIFEST_TAG" \
                "${MANIFEST_TAG}-amd64" \
                "${MANIFEST_TAG}-arm64"
            fi
          }

          # Create manifests for all images
          create_manifest "n8n" "${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}"
          create_manifest "runners" "${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}"
          create_manifest "runners-distroless" "${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }}"

          # Create SHA-tagged manifests (immutable references for deployments)
          create_manifest "n8n (sha)" "${{ needs.build-and-push-docker.outputs.n8n_sha_manifest_tag }}"
          create_manifest "runners (sha)" "${{ needs.build-and-push-docker.outputs.runners_sha_manifest_tag }}"
          create_manifest "runners-distroless (sha)" "${{ needs.build-and-push-docker.outputs.runners_distroless_sha_manifest_tag }}"

      - name: Create Docker Hub manifests
        if: needs.determine-build-context.outputs.push_to_docker == 'true'
        run: |
          VERSION="${{ needs.determine-build-context.outputs.n8n_version }}"
          DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}"

          # Create manifests for each image type
          declare -A images=(
            ["n8n"]="${VERSION}"
            ["runners"]="${VERSION}"
            ["runners-distroless"]="${VERSION}-distroless"
          )

          SHORT_SHA="${GITHUB_SHA::7}"

          for image in "${!images[@]}"; do
            TAG_SUFFIX="${images[$image]}"
            IMAGE_NAME="${image//-distroless/}"  # Remove -distroless from image name

            echo "Creating Docker Hub manifest for $image"
            docker buildx imagetools create \
              --tag "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}" \
              "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-amd64" \
              "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-arm64"

            # Create SHA-tagged manifest (immutable reference)
            # For distroless, insert SHA between version and -distroless suffix
            # to match docker-tags.mjs format: nightly-abc1234-distroless (not nightly-distroless-abc1234)
            if [[ "$image" == *"-distroless"* ]]; then
              SHA_SUFFIX="${VERSION}-${SHORT_SHA}-distroless"
            else
              SHA_SUFFIX="${TAG_SUFFIX}-${SHORT_SHA}"
            fi
            echo "Creating Docker Hub SHA manifest for $image: ${SHA_SUFFIX}"
            docker buildx imagetools create \
              --tag "${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}" \
              "${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}-amd64" \
              "${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}-arm64"
          done

      - name: Get manifest digests for attestation
        id: get-digests
        env:
          N8N_TAG: ${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}
          RUNNERS_TAG: ${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}
          DISTROLESS_TAG: ${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }}
        run: node .github/scripts/docker/get-manifest-digests.mjs

  call-success-url:
    name: Call Success URL
    needs: [create_multi_arch_manifest]
    runs-on: ubuntu-latest
    if: needs.create_multi_arch_manifest.result == 'success' || needs.create_multi_arch_manifest.result == 'skipped'
    steps:
      - name: Call Success URL
        env:
          SUCCESS_URL: ${{ github.event.inputs.success_url }}
        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.success_url != '' }}
        run: |
          echo "Calling success URL: ${{ env.SUCCESS_URL }}"
          curl -v "${{ env.SUCCESS_URL }}" || echo "Failed to call success URL"
        shell: bash

  provenance-n8n:
    name: SLSA Provenance (n8n)
    needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
    if: |
      needs.create_multi_arch_manifest.result == 'success' &&
      needs.create_multi_arch_manifest.outputs.n8n_digest != ''
    permissions:
      id-token: write
      packages: write
      actions: read
    # SLSA L3 Provenance - Must use version tags (@vX.Y.Z), NOT SHAs
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
    with:
      image: ${{ needs.create_multi_arch_manifest.outputs.n8n_image }}
      digest: ${{ needs.create_multi_arch_manifest.outputs.n8n_digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

  provenance-runners:
    name: SLSA Provenance (runners)
    needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
    if: |
      needs.create_multi_arch_manifest.result == 'success' &&
      needs.create_multi_arch_manifest.outputs.runners_digest != ''
    permissions:
      id-token: write
      packages: write
      actions: read
    # SLSA L3 Provenance - Must use version tags (@vX.Y.Z), NOT SHAs
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
    with:
      image: ${{ needs.create_multi_arch_manifest.outputs.runners_image }}
      digest: ${{ needs.create_multi_arch_manifest.outputs.runners_digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

  provenance-runners-distroless:
    name: SLSA Provenance (runners-distroless)
    needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
    if: |
      needs.create_multi_arch_manifest.result == 'success' &&
      needs.create_multi_arch_manifest.outputs.runners_distroless_digest != ''
    permissions:
      id-token: write
      packages: write
      actions: read
    # SLSA L3 Provenance - Must use version tags (@vX.Y.Z), NOT SHAs
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
    with:
      image: ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }}
      digest: ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

  # VEX Attestation - Documents which CVEs affect us (security/vex.openvex.json)
  vex-attestation:
    name: VEX Attestation
    needs:
      [
        determine-build-context,
        build-and-push-docker,
        create_multi_arch_manifest,
        provenance-n8n,
        provenance-runners,
        provenance-runners-distroless,
      ]
    if: |
      always() &&
      needs.create_multi_arch_manifest.result == 'success' &&
      (needs.determine-build-context.outputs.release_type == 'stable' ||
       needs.determine-build-context.outputs.release_type == 'rc' ||
       needs.determine-build-context.outputs.release_type == 'nightly')
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      packages: write
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install Cosign
        uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1

      - name: Login to GHCR
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Attest VEX to n8n image
        if: needs.create_multi_arch_manifest.outputs.n8n_digest != ''
        run: |
          cosign attest --yes \
            --type openvex \
            --predicate security/vex.openvex.json \
            ${{ needs.create_multi_arch_manifest.outputs.n8n_image }}@${{ needs.create_multi_arch_manifest.outputs.n8n_digest }}

      - name: Attest VEX to runners image
        if: needs.create_multi_arch_manifest.outputs.runners_digest != ''
        run: |
          cosign attest --yes \
            --type openvex \
            --predicate security/vex.openvex.json \
            ${{ needs.create_multi_arch_manifest.outputs.runners_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_digest }}

      - name: Attest VEX to runners-distroless image
        if: needs.create_multi_arch_manifest.outputs.runners_distroless_digest != ''
        run: |
          cosign attest --yes \
            --type openvex \
            --predicate security/vex.openvex.json \
            ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_distroless_digest }}

  security-scan:
    name: Security Scan
    needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
    if: |
      success() &&
      (needs.determine-build-context.outputs.release_type == 'stable' ||
       needs.determine-build-context.outputs.release_type == 'nightly' ||
       needs.determine-build-context.outputs.release_type == 'rc')
    uses: ./.github/workflows/security-trivy-scan-callable.yml
    with:
      image_ref: ${{ needs.build-and-push-docker.outputs.image_ref }}
    secrets: inherit

  security-scan-runners:
    name: Security Scan (runners)
    needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
    if: |
      success() &&
      (needs.determine-build-context.outputs.release_type == 'stable' ||
       needs.determine-build-context.outputs.release_type == 'nightly' ||
       needs.determine-build-context.outputs.release_type == 'rc')
    uses: ./.github/workflows/security-trivy-scan-callable.yml
    with:
      image_ref: ${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}
    secrets: inherit

  notify-on-failure:
    name: Notify Cats on nightly build failure
    runs-on: ubuntu-latest
    needs: [build-and-push-docker]
    if: needs.build-and-push-docker.result == 'failure' && github.event_name == 'schedule'
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          sparse-checkout: .github/scripts/slack
          sparse-checkout-cone-mode: false
      - name: Notify Slack
        env:
          SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
        run: |
          node .github/scripts/slack/notify.mjs \
            --channel '#team-catalysts' \
            --text 'Nightly Docker build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
