Docker: Build and Push #124
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This workflow is used to build and push the Docker image for n8nio/n8n and n8nio/runners | |
| # - determine-build-context: Determines what needs to be built based on the trigger | |
| # - build-and-push-docker: This builds on both an ARM64 and AMD64 runner. Uses standard GitHub runners and docker/build-push-action | |
| # - create_multi_arch_manifest: This creates the multi-arch manifest for the Docker image. Needed to recombine the images from the build-and-push-docker job since they are separate runners. | |
| # - security-scan: This scans the n8nio/n8n Docker image for security vulnerabilities using Trivy. | |
| # - security-scan-runners: This scans the n8nio/runners Docker image for security vulnerabilities using Trivy. | |
| name: 'Docker: Build and Push' | |
| env: | |
| NODE_OPTIONS: '--max-old-space-size=7168' | |
| NODE_VERSION: '22.21.0' | |
| on: | |
| # Build on push to master | |
| push: | |
| branches: [master, main] | |
| 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 | |
| pull_request: | |
| types: | |
| - opened | |
| - ready_for_review | |
| paths: | |
| - '.github/workflows/docker-build-push.yml' | |
| - 'docker/images/n8n/Dockerfile' | |
| - 'docker/images/runners/Dockerfile' | |
| 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.n8n_version }} | |
| push_enabled: ${{ steps.context.outputs.push_enabled }} | |
| build_matrix: ${{ steps.matrix.outputs.matrix }} | |
| steps: | |
| - name: Determine build context values | |
| id: context | |
| run: | | |
| # Debug info | |
| echo "Event: ${{ github.event_name }}" | |
| echo "Ref: ${{ github.ref }}" | |
| echo "Ref Name: ${{ github.ref_name }}" | |
| # Check if called by another workflow (has n8n_version input) | |
| if [[ -n "${{ inputs.n8n_version }}" ]]; then | |
| # workflow_call - used for releases | |
| { | |
| echo "release_type=${{ inputs.release_type }}" | |
| echo "n8n_version=${{ inputs.n8n_version }}" | |
| echo "push_enabled=${{ inputs.push_enabled }}" | |
| } >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ github.event_name }}" == "schedule" ]]; then | |
| # Nightly builds | |
| { | |
| echo "release_type=nightly" | |
| echo "n8n_version=snapshot" | |
| echo "push_enabled=true" | |
| } >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| # Build branches for Nathan deploy | |
| BRANCH_NAME="${{ github.ref_name }}" | |
| # Fallback to parsing ref if ref_name is empty | |
| if [[ -z "$BRANCH_NAME" ]] && [[ "${{ github.ref }}" =~ ^refs/heads/(.+)$ ]]; then | |
| BRANCH_NAME="${BASH_REMATCH[1]}" | |
| fi | |
| # Sanitize branch name for Docker tag | |
| SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-' | tr -cd '[:alnum:]-_') | |
| if [[ -z "$SAFE_BRANCH_NAME" ]]; then | |
| echo "Error: Could not determine valid branch name" | |
| exit 1 | |
| fi | |
| { | |
| echo "release_type=branch" | |
| echo "n8n_version=branch-${SAFE_BRANCH_NAME}" | |
| echo "push_enabled=${{ inputs.push_enabled }}" | |
| } >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| # Direct PR triggers for testing Dockerfile changes | |
| { | |
| echo "release_type=dev" | |
| echo "n8n_version=pr-${{ github.event.pull_request.number }}" | |
| echo "push_enabled=false" | |
| } >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ github.event_name }}" == "push" ]]; then | |
| # Push to master - build and deploy | |
| { | |
| echo "release_type=dev" | |
| echo "n8n_version=latest" | |
| echo "push_enabled=true" | |
| } >> "$GITHUB_OUTPUT" | |
| else | |
| # Fallback for any other event type | |
| { | |
| echo "release_type=dev" | |
| echo "n8n_version=dev" | |
| echo "push_enabled=false" | |
| } >> "$GITHUB_OUTPUT" | |
| fi | |
| # Output summary for logs | |
| echo "=== Build Context Summary ===" | |
| echo "Release type: $(grep release_type "$GITHUB_OUTPUT" | cut -d= -f2)" | |
| echo "N8N version: $(grep n8n_version "$GITHUB_OUTPUT" | cut -d= -f2)" | |
| echo "Push enabled: $(grep push_enabled "$GITHUB_OUTPUT" | cut -d= -f2)" | |
| - name: Determine build matrix | |
| id: matrix | |
| run: | | |
| RELEASE_TYPE="${{ steps.context.outputs.release_type }}" | |
| # Branch builds only need AMD64, everything else needs both platforms | |
| if [[ "$RELEASE_TYPE" == "branch" ]]; then | |
| MATRIX='{ | |
| "platform": ["amd64"], | |
| "include": [{ | |
| "platform": "amd64", | |
| "runner": "ubuntu-latest", | |
| "docker_platform": "linux/amd64" | |
| }] | |
| }' | |
| else | |
| # All other builds (stable, nightly, dev, PR) need both platforms | |
| # Note: For ARM64, we use ubuntu-latest with QEMU emulation | |
| MATRIX='{ | |
| "platform": ["amd64", "arm64"], | |
| "include": [{ | |
| "platform": "amd64", | |
| "runner": "ubuntu-latest", | |
| "docker_platform": "linux/amd64" | |
| }, { | |
| "platform": "arm64", | |
| "runner": "ubuntu-latest", | |
| "docker_platform": "linux/arm64" | |
| }] | |
| }' | |
| fi | |
| # Output matrix as single line for GITHUB_OUTPUT | |
| echo "matrix=$(echo "$MATRIX" | jq -c .)" >> "$GITHUB_OUTPUT" | |
| echo "Build matrix: $(echo "$MATRIX" | jq .)" | |
| 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: 15 | |
| strategy: | |
| matrix: ${{ fromJSON(needs.determine-build-context.outputs.build_matrix) }} | |
| outputs: | |
| image_ref: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }} | |
| primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }} | |
| runners_primary_ghcr_manifest_tag: ${{ steps.determine-runners-tags.outputs.primary_ghcr_manifest_tag }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup and Build | |
| uses: ./.github/actions/setup-nodejs-github | |
| with: | |
| build-command: pnpm build:n8n | |
| - name: Determine Docker tags | |
| id: determine-tags | |
| run: | | |
| RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" | |
| N8N_VERSION_TAG="${{ needs.determine-build-context.outputs.n8n_version }}" | |
| # Convert repository owner to lowercase for Docker compatibility | |
| REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| GHCR_BASE="ghcr.io/${REPO_OWNER_LOWER}/n8n" | |
| # Convert Docker Hub username to lowercase | |
| DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]') | |
| DOCKER_BASE="${DOCKER_USERNAME_LOWER}/n8n" | |
| PLATFORM="${{ matrix.platform }}" | |
| GHCR_TAGS_FOR_PUSH="" | |
| DOCKER_TAGS_FOR_PUSH="" | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="" | |
| # Validate inputs | |
| if [[ "$RELEASE_TYPE" == "stable" && -z "$N8N_VERSION_TAG" ]]; then | |
| echo "Error: N8N_VERSION_TAG is empty for a stable release." | |
| exit 1 | |
| fi | |
| if [[ "$RELEASE_TYPE" == "branch" && -z "$N8N_VERSION_TAG" ]]; then | |
| echo "Error: N8N_VERSION_TAG is empty for a branch release." | |
| exit 1 | |
| fi | |
| # Determine tags based on release type | |
| case "$RELEASE_TYPE" in | |
| "stable") | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:${N8N_VERSION_TAG}-${PLATFORM}" | |
| ;; | |
| "nightly") | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:nightly" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:nightly-${PLATFORM}" | |
| ;; | |
| "branch") | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| # No Docker Hub tags for branch builds | |
| DOCKER_TAGS_FOR_PUSH="" | |
| ;; | |
| "dev"|*) | |
| if [[ "$N8N_VERSION_TAG" == pr-* ]]; then | |
| # PR builds only go to GHCR | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="" | |
| else | |
| # Regular dev builds go to both registries, using the version tag (e.g., "latest" for push builds) | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:${N8N_VERSION_TAG}-${PLATFORM}" | |
| fi | |
| ;; | |
| esac | |
| # Combine all tags | |
| ALL_TAGS="${GHCR_TAGS_FOR_PUSH}" | |
| if [[ -n "$DOCKER_TAGS_FOR_PUSH" ]]; then | |
| ALL_TAGS="${ALL_TAGS}\n${DOCKER_TAGS_FOR_PUSH}" | |
| fi | |
| echo "Generated Tags for push: $ALL_TAGS" | |
| { | |
| echo "tags<<EOF" | |
| echo -e "$ALL_TAGS" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo "ghcr_platform_tag=${GHCR_TAGS_FOR_PUSH}" | |
| echo "dockerhub_platform_tag=${DOCKER_TAGS_FOR_PUSH}" | |
| } >> "$GITHUB_OUTPUT" | |
| # Only output manifest tags from the first platform to avoid duplicates | |
| if [[ "$PLATFORM" == "amd64" ]]; then | |
| echo "primary_ghcr_manifest_tag=${PRIMARY_GHCR_MANIFEST_TAG_VALUE}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Determine Docker tags (runners) | |
| id: determine-runners-tags | |
| run: | | |
| RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" | |
| N8N_VERSION_TAG="${{ needs.determine-build-context.outputs.n8n_version }}" | |
| # Convert repository owner to lowercase for Docker compatibility | |
| REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| GHCR_BASE="ghcr.io/${REPO_OWNER_LOWER}/runners" | |
| # Convert Docker Hub username to lowercase | |
| DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]') | |
| DOCKER_BASE="${DOCKER_USERNAME_LOWER}/runners" | |
| PLATFORM="${{ matrix.platform }}" | |
| GHCR_TAGS_FOR_PUSH="" | |
| DOCKER_TAGS_FOR_PUSH="" | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="" | |
| case "$RELEASE_TYPE" in | |
| "stable") | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:${N8N_VERSION_TAG}-${PLATFORM}" | |
| ;; | |
| "nightly") | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:nightly" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:nightly-${PLATFORM}" | |
| ;; | |
| "branch") | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="" # mirror n8n logic: no Docker Hub for branch | |
| ;; | |
| "dev"|*) | |
| if [[ "$N8N_VERSION_TAG" == pr-* ]]; then | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="" | |
| else | |
| # Regular dev builds go to both registries, using the version tag (e.g., "latest" for push builds) | |
| PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" | |
| GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" | |
| DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:${N8N_VERSION_TAG}-${PLATFORM}" | |
| fi | |
| ;; | |
| esac | |
| ALL_TAGS="${GHCR_TAGS_FOR_PUSH}" | |
| if [[ -n "$DOCKER_TAGS_FOR_PUSH" ]]; then | |
| ALL_TAGS="${ALL_TAGS}\n${DOCKER_TAGS_FOR_PUSH}" | |
| fi | |
| { | |
| echo "tags<<EOF" | |
| echo -e "$ALL_TAGS" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo "ghcr_platform_tag=${GHCR_TAGS_FOR_PUSH}" | |
| echo "dockerhub_platform_tag=${DOCKER_TAGS_FOR_PUSH}" | |
| } >> "$GITHUB_OUTPUT" | |
| # Only output manifest tags from the first platform to avoid duplicates | |
| if [[ "$PLATFORM" == "amd64" ]]; then | |
| echo "primary_ghcr_manifest_tag=${PRIMARY_GHCR_MANIFEST_TAG_VALUE}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 | |
| - name: Login to GitHub Container Registry | |
| if: needs.determine-build-context.outputs.push_enabled == 'true' | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Convert Docker Hub username to lowercase | |
| if: needs.determine-build-context.outputs.push_enabled == 'true' | |
| id: docker_username | |
| run: | | |
| DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]') | |
| echo "username=${DOCKER_USERNAME_LOWER}" >> "$GITHUB_OUTPUT" | |
| - name: Login to Docker Hub | |
| if: needs.determine-build-context.outputs.push_enabled == 'true' && ( | |
| steps.determine-tags.outputs.dockerhub_platform_tag != '' || | |
| steps.determine-runners-tags.outputs.dockerhub_platform_tag != '') | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
| with: | |
| username: ${{ steps.docker_username.outputs.username }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Build and push n8n Docker image | |
| uses: docker/build-push-action@v5 # v1.2 | |
| 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: true | |
| sbom: true | |
| push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }} | |
| tags: ${{ steps.determine-tags.outputs.tags }} | |
| - name: Build and push task runners Docker image | |
| uses: docker/build-push-action@v5 # v1.2 | |
| 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: true | |
| sbom: true | |
| push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }} | |
| tags: ${{ steps.determine-runners-tags.outputs.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' | |
| steps: | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Determine Docker Hub manifest tag | |
| id: dockerhub_check | |
| run: | | |
| RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" | |
| N8N_VERSION="${{ needs.determine-build-context.outputs.n8n_version }}" | |
| # Convert Docker Hub username to lowercase | |
| DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]') | |
| DOCKER_BASE="${DOCKER_USERNAME_LOWER}/n8n" | |
| # Determine if Docker Hub manifest is needed and construct the tag | |
| case "$RELEASE_TYPE" in | |
| "stable") | |
| { | |
| echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:${N8N_VERSION}" | |
| echo "CREATE_DOCKERHUB_MANIFEST=true" | |
| } >> "$GITHUB_OUTPUT" | |
| ;; | |
| "nightly") | |
| { | |
| echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:nightly" | |
| echo "CREATE_DOCKERHUB_MANIFEST=true" | |
| } >> "$GITHUB_OUTPUT" | |
| ;; | |
| "dev") | |
| if [[ "$N8N_VERSION" != pr-* ]]; then | |
| { | |
| echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:dev" | |
| echo "CREATE_DOCKERHUB_MANIFEST=true" | |
| } >> "$GITHUB_OUTPUT" | |
| else | |
| echo "CREATE_DOCKERHUB_MANIFEST=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| ;; | |
| *) | |
| echo "CREATE_DOCKERHUB_MANIFEST=false" >> "$GITHUB_OUTPUT" | |
| ;; | |
| esac | |
| - name: Determine Docker Hub manifest tag (runners) | |
| id: dockerhub_runners_check | |
| run: | | |
| RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" | |
| N8N_VERSION="${{ needs.determine-build-context.outputs.n8n_version }}" | |
| # Convert Docker Hub username to lowercase | |
| DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]') | |
| DOCKER_BASE="${DOCKER_USERNAME_LOWER}/runners" | |
| # Determine if Docker Hub manifest is needed and construct the tag | |
| case "$RELEASE_TYPE" in | |
| "stable") | |
| { | |
| echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:${N8N_VERSION}" | |
| echo "CREATE_DOCKERHUB_MANIFEST=true" | |
| } >> "$GITHUB_OUTPUT" | |
| ;; | |
| "nightly") | |
| { | |
| echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:nightly" | |
| echo "CREATE_DOCKERHUB_MANIFEST=true" | |
| } >> "$GITHUB_OUTPUT" | |
| ;; | |
| "dev") | |
| if [[ "$N8N_VERSION" != pr-* ]]; then | |
| { | |
| echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:dev" | |
| echo "CREATE_DOCKERHUB_MANIFEST=true" | |
| } >> "$GITHUB_OUTPUT" | |
| else | |
| echo "CREATE_DOCKERHUB_MANIFEST=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| ;; | |
| *) | |
| echo "CREATE_DOCKERHUB_MANIFEST=false" >> "$GITHUB_OUTPUT" | |
| ;; | |
| esac | |
| - name: Convert Docker Hub username to lowercase | |
| if: steps.dockerhub_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' || | |
| steps.dockerhub_runners_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' | |
| id: docker_username_manifest | |
| run: | | |
| DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]') | |
| echo "username=${DOCKER_USERNAME_LOWER}" >> "$GITHUB_OUTPUT" | |
| - name: Login to Docker Hub | |
| if: steps.dockerhub_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' || | |
| steps.dockerhub_runners_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
| with: | |
| username: ${{ steps.docker_username_manifest.outputs.username }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Create GHCR multi-arch manifest | |
| if: needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag != '' | |
| run: | | |
| MANIFEST_TAG="${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}" | |
| RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" | |
| echo "Creating GHCR manifest: $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 | |
| - name: Create GHCR multi-arch manifest (runners) | |
| if: needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag != '' | |
| run: | | |
| MANIFEST_TAG="${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}" | |
| RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" | |
| echo "Creating GHCR runners manifest: $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 | |
| - name: Create Docker Hub multi-arch manifest | |
| if: steps.dockerhub_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' | |
| run: | | |
| MANIFEST_TAG="${{ steps.dockerhub_check.outputs.DOCKER_MANIFEST_TAG }}" | |
| echo "Creating Docker Hub manifest: $MANIFEST_TAG" | |
| docker buildx imagetools create \ | |
| --tag $MANIFEST_TAG \ | |
| ${MANIFEST_TAG}-amd64 \ | |
| ${MANIFEST_TAG}-arm64 | |
| - name: Create Docker Hub multi-arch manifest (runners) | |
| if: steps.dockerhub_runners_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' | |
| run: | | |
| MANIFEST_TAG="${{ steps.dockerhub_runners_check.outputs.DOCKER_MANIFEST_TAG }}" | |
| echo "Creating Docker Hub manifest: $MANIFEST_TAG" | |
| docker buildx imagetools create \ | |
| --tag $MANIFEST_TAG \ | |
| ${MANIFEST_TAG}-amd64 \ | |
| ${MANIFEST_TAG}-arm64 | |
| 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 | |
| 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') | |
| 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] | |
| if: | | |
| success() && | |
| (needs.determine-build-context.outputs.release_type == 'stable' || | |
| needs.determine-build-context.outputs.release_type == 'nightly') | |
| 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: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 | |
| with: | |
| status: ${{ needs.build-and-push-docker.result }} | |
| channel: '#team-catalysts' | |
| webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| message: Nightly Docker build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| deploy-to-kubernetes: | |
| name: Deploy to Kubernetes | |
| needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest] | |
| runs-on: ubuntu-latest | |
| # Only deploy on push to master (not on schedule, PR, or workflow_dispatch) | |
| if: | | |
| success() && | |
| github.event_name == 'push' && | |
| github.ref == 'refs/heads/master' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| - name: Update kubeconfig | |
| run: | | |
| aws eks update-kubeconfig --name ${{ secrets.EKS_CLUSTER_NAME }} --region ${{ secrets.AWS_REGION }} | |
| - name: Inject secrets into manifests | |
| run: | | |
| cd n8n-hosting/kubernetes | |
| # Replace placeholders in n8n-secret.yaml | |
| sed -i "s|N8N_ENCRYPTION_KEY: \"PLACEHOLDER_REPLACED_BY_CI\"|N8N_ENCRYPTION_KEY: \"${{ secrets.N8N_ENCRYPTION_KEY }}\"|g" n8n-secret.yaml | |
| sed -i "s|N8N_RUNNERS_AUTH_TOKEN: \"PLACEHOLDER_REPLACED_BY_CI\"|N8N_RUNNERS_AUTH_TOKEN: \"${{ secrets.N8N_RUNNERS_AUTH_TOKEN }}\"|g" n8n-secret.yaml | |
| # Replace placeholders in postgres-secret.yaml | |
| sed -i "s|POSTGRES_PASSWORD: PLACEHOLDER_REPLACED_BY_CI|POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}|g" postgres-secret.yaml | |
| sed -i "s|POSTGRES_NON_ROOT_PASSWORD: PLACEHOLDER_REPLACED_BY_CI|POSTGRES_NON_ROOT_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}|g" postgres-secret.yaml | |
| echo "✅ Secrets injected successfully" | |
| - name: Update image tags to use custom images | |
| run: | | |
| cd n8n-hosting/kubernetes | |
| # Convert repository owner to lowercase for Docker compatibility | |
| REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| # Update to use your custom images from GHCR (using 'latest' tag for production) | |
| sed -i "s|image: docker.n8n.io/n8nio/n8n:.*|image: ghcr.io/${REPO_OWNER_LOWER}/n8n:latest|g" \ | |
| n8n-deployment-queue-mode.yaml n8n-worker-deployment.yaml | |
| # Update runner images (match the actual image name in the file) | |
| sed -i "s|image: ghcr.io/n8n-io/n8n-task-runner-javascript:.*|image: ghcr.io/${REPO_OWNER_LOWER}/runners:latest|g" \ | |
| n8n-runner-deployment.yaml | |
| sed -i "s|image: ghcr.io/n8n-io/n8n-task-runner-python:.*|image: ghcr.io/${REPO_OWNER_LOWER}/runners:latest|g" \ | |
| n8n-runner-deployment.yaml | |
| echo "✅ Image references updated to custom images" | |
| - name: Check if namespace exists | |
| id: check_namespace | |
| run: | | |
| if kubectl get namespace n8n &> /dev/null; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "✅ Namespace 'n8n' already exists - will update existing deployment" | |
| # Check for problematic states that require fresh deployment | |
| echo "🔍 Checking deployment health..." | |
| kubectl get pods -n n8n 2>/dev/null || true | |
| NEEDS_RESET=false | |
| # Check for unbound PVCs | |
| UNBOUND=$(kubectl get pvc -n n8n -o json 2>/dev/null | jq -r '.items[] | select(.status.phase != "Bound") | .metadata.name' | wc -l) | |
| if [ "$UNBOUND" -gt 0 ]; then | |
| echo "❌ Found $UNBOUND unbound PVCs" | |
| NEEDS_RESET=true | |
| fi | |
| # Check for CrashLoopBackOff pods (e.g., encryption key mismatch) | |
| CRASHING=$(kubectl get pods -n n8n --field-selector=status.phase=Running -o json 2>/dev/null | jq -r '.items[].status.containerStatuses[]? | select(.state.waiting.reason == "CrashLoopBackOff") | .name' | wc -l) | |
| if [ "$CRASHING" -gt 0 ]; then | |
| echo "❌ Found $CRASHING pods in CrashLoopBackOff" | |
| kubectl logs -n n8n -l service=n8n-main --tail=20 || true | |
| NEEDS_RESET=true | |
| fi | |
| if [ "$NEEDS_RESET" = true ]; then | |
| echo "❌ Unhealthy deployment detected. Force deleting namespace for fresh start..." | |
| # Force delete the namespace | |
| kubectl delete namespace n8n --grace-period=0 --force || true | |
| # Wait for deletion (with timeout) | |
| echo "⏳ Waiting for namespace deletion..." | |
| kubectl wait --for=delete namespace/n8n --timeout=180s || true | |
| # Double check it's gone | |
| sleep 10 | |
| if kubectl get namespace n8n &> /dev/null; then | |
| echo "❌ Namespace still exists! Manual intervention required." | |
| exit 1 | |
| fi | |
| echo "✅ Namespace deleted. Setting flag for initial deployment." | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "📦 Namespace 'n8n' does not exist - will perform initial deployment" | |
| fi | |
| - name: Initial Deployment (if namespace doesn't exist) | |
| if: steps.check_namespace.outputs.exists == 'false' | |
| run: | | |
| cd n8n-hosting/kubernetes | |
| echo "🚀 Performing initial deployment..." | |
| kubectl apply -f namespace.yaml | |
| kubectl apply -f postgres-secret.yaml | |
| kubectl apply -f n8n-secret.yaml | |
| # Apply PVCs (they will bind when pods start consuming them due to WaitForFirstConsumer) | |
| echo "📦 Creating Persistent Volume Claims..." | |
| kubectl apply -f postgres-claim0-persistentvolumeclaim.yaml | |
| kubectl apply -f n8n-claim0-persistentvolumeclaim.yaml | |
| kubectl apply -f redis-claim0-persistentvolumeclaim.yaml | |
| echo "✅ PVCs created (will bind when pods start due to WaitForFirstConsumer mode)" | |
| kubectl get pvc -n n8n | |
| kubectl apply -f redis-deployment.yaml | |
| kubectl apply -f postgres-configmap.yaml | |
| kubectl apply -f postgres-deployment.yaml | |
| kubectl apply -f postgres-service.yaml | |
| echo "⏳ Waiting for PostgreSQL..." | |
| if ! kubectl wait --for=condition=ready pod -l service=postgres-n8n -n n8n --timeout=600s; then | |
| echo "❌ PostgreSQL failed to start. Debugging info:" | |
| kubectl get pods -n n8n | |
| kubectl describe pod -l service=postgres-n8n -n n8n | |
| kubectl logs -l service=postgres-n8n -n n8n --tail=50 | |
| exit 1 | |
| fi | |
| echo "⏳ Waiting for Redis..." | |
| kubectl wait --for=condition=ready pod -l service=redis -n n8n --timeout=300s | |
| kubectl apply -f n8n-deployment-queue-mode.yaml | |
| kubectl apply -f n8n-service.yaml | |
| kubectl apply -f n8n-main-service.yaml | |
| echo "⏳ Waiting for n8n main..." | |
| kubectl wait --for=condition=ready pod -l service=n8n-main -n n8n --timeout=300s | |
| kubectl apply -f n8n-worker-deployment.yaml | |
| kubectl apply -f n8n-runner-deployment.yaml | |
| echo "✅ Initial deployment complete!" | |
| - name: Update Existing Deployment (if namespace exists) | |
| if: steps.check_namespace.outputs.exists == 'true' | |
| run: | | |
| cd n8n-hosting/kubernetes | |
| echo "🔄 Updating existing deployment..." | |
| # Update secrets (in case they changed) | |
| kubectl apply -f n8n-secret.yaml | |
| kubectl apply -f postgres-secret.yaml | |
| # Update deployments (will pick up new image tags) | |
| kubectl apply -f n8n-deployment-queue-mode.yaml | |
| kubectl apply -f n8n-worker-deployment.yaml | |
| kubectl apply -f n8n-runner-deployment.yaml | |
| # Force restart to pull latest images (required when using :latest tag) | |
| echo "🔄 Forcing rollout restart to pull new images..." | |
| kubectl rollout restart deployment/n8n-main -n n8n | |
| kubectl rollout restart deployment/n8n-worker -n n8n | |
| kubectl rollout restart deployment/n8n-runner -n n8n | |
| echo "✅ Update applied and rollout restart triggered!" | |
| - name: Wait for rollout to complete | |
| run: | | |
| echo "⏳ Waiting for rollout to complete..." | |
| # Wait for n8n-main with debugging | |
| if ! kubectl rollout status deployment/n8n-main -n n8n --timeout=15m; then | |
| echo "❌ n8n-main deployment failed. Debugging info:" | |
| kubectl get pods -n n8n -l service=n8n-main | |
| kubectl describe pod -l service=n8n-main -n n8n | |
| kubectl logs -l service=n8n-main -n n8n --tail=100 --all-containers=true | |
| exit 1 | |
| fi | |
| # Wait for n8n-worker with debugging | |
| if ! kubectl rollout status deployment/n8n-worker -n n8n --timeout=10m; then | |
| echo "❌ n8n-worker deployment failed. Debugging info:" | |
| kubectl get pods -n n8n -l service=n8n-worker | |
| kubectl describe pod -l service=n8n-worker -n n8n | |
| kubectl logs -l service=n8n-worker -n n8n --tail=100 --all-containers=true | |
| exit 1 | |
| fi | |
| # Wait for n8n-runner with debugging | |
| if ! kubectl rollout status deployment/n8n-runner -n n8n --timeout=10m; then | |
| echo "❌ n8n-runner deployment failed. Debugging info:" | |
| kubectl get pods -n n8n -l service=n8n-runner | |
| kubectl describe pod -l service=n8n-runner -n n8n | |
| kubectl logs -l service=n8n-runner -n n8n --tail=100 --all-containers=true | |
| exit 1 | |
| fi | |
| echo "✅ All deployments rolled out successfully!" | |
| - name: Display deployment status | |
| run: | | |
| echo "==========================================" | |
| echo "🎉 n8n Deployment Complete!" | |
| echo "==========================================" | |
| echo "" | |
| echo "📊 Pod Status:" | |
| kubectl get pods -n n8n | |
| echo "" | |
| echo "🔗 Access URL:" | |
| kubectl get svc n8n -n n8n | |
| echo "" | |
| echo "Access n8n at the EXTERNAL-IP above on port 5678" | |
| echo "==========================================" |