name: 'CI: Pull Requests (Build, Test, Lint)'

on:
  pull_request:
  merge_group:

concurrency:
  group: ci-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.ref }}
  cancel-in-progress: true

jobs:
  install-and-build:
    name: Install & Build
    runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
    env:
      NODE_OPTIONS: '--max-old-space-size=6144'
      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
      QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
      QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
      QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
    outputs:
      ci: ${{ fromJSON(steps.ci-filter.outputs.results).ci == true }}
      runtime: ${{ fromJSON(steps.ci-filter.outputs.results).runtime == true }}
      unit: ${{ fromJSON(steps.ci-filter.outputs.results).unit == true }}
      e2e: ${{ fromJSON(steps.ci-filter.outputs.results).e2e == true }}
      dev_server_smoke: ${{ fromJSON(steps.ci-filter.outputs.results)['dev-server-smoke'] == true }}
      workflows: ${{ fromJSON(steps.ci-filter.outputs.results).workflows == true }}
      workflow_scripts: ${{ fromJSON(steps.ci-filter.outputs.results)['workflow-scripts'] == true }}
      db: ${{ fromJSON(steps.ci-filter.outputs.results).db == true }}
      performance: ${{ fromJSON(steps.ci-filter.outputs.results).performance == true }}
      e2e_performance: ${{ fromJSON(steps.ci-filter.outputs.results)['e2e-performance'] == true }}
      instance_ai_workflow_eval: ${{ fromJSON(steps.ci-filter.outputs.results)['instance-ai-workflow-eval'] == true }}
      commit_sha: ${{ steps.commit-sha.outputs.sha }}
      merge_base: ${{ steps.ci-filter.outputs.merge-base }}
      matrix: ${{ steps.generate-matrix.outputs.matrix }}
      skip_tests: ${{ steps.generate-matrix.outputs.skip-tests }}
      affected_packages: ${{ steps.affected-packages.outputs.list }}
      changed_files: ${{ steps.ci-filter.outputs.changed-files }}
    steps:
      - uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
        with:
          # Use merge_group SHA when in merge queue, otherwise PR merge ref
          ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || format('refs/pull/{0}/merge', github.event.pull_request.number) }}

      - name: Capture commit SHA for cache consistency
        id: commit-sha
        run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

      - name: Check for relevant changes
        uses: ./.github/actions/ci-filter
        id: ci-filter
        with:
          mode: filter
          filters: |
            ci:
              **
              !packages/@n8n/task-runner-python/**
              !.github/**
              !**/*.md
              !**/LICENSE
              !**/CHANGELOG.md
            runtime:
              **
              !packages/@n8n/task-runner-python/**
              !.github/**
              !**/*.md
              !**/LICENSE
              !**/CHANGELOG.md
              !**/*.test.ts
              !**/*.spec.ts
              !packages/testing/playwright/**
              !packages/frontend/@n8n/storybook/**
            unit:
              **
              !packages/@n8n/task-runner-python/**
              !packages/testing/playwright/**
              !.github/**
              !**/*.md
              !**/LICENSE
              !**/CHANGELOG.md
            e2e:
              .github/workflows/test-e2e-*.yml
              .github/workflows/prepare-docker-reusable.yml
              .github/actions/build-n8n-docker/**
              .github/actions/load-n8n-docker/**
              packages/testing/playwright/**
              packages/testing/containers/**
            dev-server-smoke:
              packages/frontend/editor-ui/vite.config.mts
              pnpm-workspace.yaml
              packages/@n8n/*/package.json
              packages/testing/playwright/tests/dev-server-smoke/**
              packages/testing/playwright/playwright.config.ts
              packages/testing/playwright/playwright-projects.ts
              packages/testing/playwright/package.json
              .github/workflows/test-dev-server-smoke-reusable.yml
            workflows: .github/**
            workflow-scripts: .github/scripts/**
            performance:
              packages/testing/performance/**
              packages/workflow/src/**
              packages/@n8n/expression-runtime/src/**
              .github/workflows/test-bench-reusable.yml
            e2e-performance:
              packages/testing/playwright/tests/performance/**
              packages/testing/playwright/utils/performance-helper.ts
              packages/testing/containers/**
              .github/workflows/test-e2e-performance-reusable.yml
            instance-ai-workflow-eval:
              packages/@n8n/instance-ai/src/**
              packages/@n8n/instance-ai/evaluations/**
              packages/cli/src/modules/instance-ai/**
              packages/core/src/execution-engine/eval-mock-helpers.ts
              .github/workflows/test-evals-discovery.yml
            db:
              packages/cli/src/databases/**
              packages/cli/src/modules/*/database/**
              packages/cli/src/modules/**/*.entity.ts
              packages/cli/src/modules/**/*.repository.ts
              packages/cli/test/integration/**
              packages/cli/test/migration/**
              packages/cli/test/shared/db/**
              packages/@n8n/db/**
              packages/cli/**/__tests__/**
              packages/testing/containers/services/postgres.ts
              .github/workflows/test-db-reusable.yml

      - name: Setup and Build
        if: fromJSON(steps.ci-filter.outputs.results).ci || fromJSON(steps.ci-filter.outputs.results).e2e
        uses: ./.github/actions/setup-nodejs
        with:
          build-command: ${{ fromJSON(steps.ci-filter.outputs.results).ci && 'pnpm build' || 'pnpm turbo run build --filter=@n8n/playwright-janitor' }}

      - name: Run format check
        if: fromJSON(steps.ci-filter.outputs.results).ci
        run: pnpm format:check

      - name: Generate shard matrix
        id: generate-matrix
        if: fromJSON(steps.ci-filter.outputs.results).ci || fromJSON(steps.ci-filter.outputs.results).e2e
        env:
          CHANGED_FILES: ${{ steps.ci-filter.outputs.changed-files }}
          MERGE_BASE: ${{ steps.ci-filter.outputs.merge-base }}
        run: |
          FILES_CSV=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//')
          MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 16 --orchestrate --impact "--files=$FILES_CSV" "--base=$MERGE_BASE")
          echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
          echo "skip-tests=$(node -e "process.stdout.write(JSON.parse(process.argv[1])[0]?.skip === true ? 'true' : 'false')" "$MATRIX")" >> "$GITHUB_OUTPUT"

      - name: Compute affected packages
        id: affected-packages
        if: fromJSON(steps.ci-filter.outputs.results).unit
        env:
          CHANGED_FILES: ${{ steps.ci-filter.outputs.changed-files }}
        run: |
          PACKAGES=$(node packages/testing/janitor/dist/cli.js affected-packages | tr '\n' ' ' | sed 's/ *$//')
          echo "Affected packages: $PACKAGES"
          echo "list=$PACKAGES" >> "$GITHUB_OUTPUT"

  unit-test:
    name: Unit tests
    if: needs.install-and-build.outputs.unit == 'true'
    uses: ./.github/workflows/test-unit-reusable.yml
    needs: install-and-build
    with:
      ref: ${{ needs.install-and-build.outputs.commit_sha }}
      collectCoverage: ${{ github.event_name != 'merge_group' }}
      affectedPackages: ${{ needs.install-and-build.outputs.affected_packages }}
      changedFiles: ${{ needs.install-and-build.outputs.changed_files }}
    secrets: inherit

  typecheck:
    name: Typecheck
    if: needs.install-and-build.outputs.ci == 'true'
    runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
    needs: install-and-build
    steps:
      - uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
        with:
          ref: ${{ needs.install-and-build.outputs.commit_sha }}

      - name: Setup Node.js
        uses: ./.github/actions/setup-nodejs
        with:
          build-command: pnpm typecheck

  lint:
    name: Lint
    if: needs.install-and-build.outputs.ci == 'true'
    uses: ./.github/workflows/test-linting-reusable.yml
    needs: install-and-build
    with:
      ref: ${{ needs.install-and-build.outputs.commit_sha }}

  check-packaging:
    name: Check packaging
    if: needs.install-and-build.outputs.ci == 'true'
    runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
    needs: install-and-build
    steps:
      - uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
        with:
          ref: ${{ needs.install-and-build.outputs.commit_sha }}

      - name: Setup Node.js
        uses: ./.github/actions/setup-nodejs

      - name: Check packaging
        shell: bash
        run: |
          pnpm -r pack --dry-run

  # Seeds the SHA-keyed Docker image cache once so that downstream e2e jobs
  # (each of which invokes prepare-docker internally) short-circuit to a
  # cache hit instead of racing to rebuild.
  prepare-docker:
    name: Prepare Docker
    needs: install-and-build
    if: >-
      github.repository == 'n8n-io/n8n' &&
      github.event_name != 'merge_group' &&
      (needs.install-and-build.outputs.runtime == 'true'
        || needs.install-and-build.outputs.e2e == 'true'
        || needs.install-and-build.outputs.e2e_performance == 'true'
        || contains(github.event.pull_request.labels.*.name, 'force-e2e'))
    uses: ./.github/workflows/prepare-docker-reusable.yml
    with:
      branch: ${{ needs.install-and-build.outputs.commit_sha }}
    secrets: inherit

  # Internal-only 1-spec fail-fast sanity check on sqlite.
  sqlite-sanity:
    name: 'SQLite: Sanity Check'
    needs: [install-and-build, prepare-docker]
    if: >-
      needs.prepare-docker.result == 'success' &&
      (needs.install-and-build.outputs.runtime == 'true'
        || needs.install-and-build.outputs.e2e == 'true'
        || contains(github.event.pull_request.labels.*.name, 'force-e2e')) &&
      github.repository == 'n8n-io/n8n' &&
      github.event_name != 'merge_group' &&
      github.event.pull_request.head.repo.fork != true
    uses: ./.github/workflows/test-e2e-reusable.yml
    with:
      branch: ${{ needs.install-and-build.outputs.commit_sha }}
      test-mode: docker-artifact
      test-command: pnpm --filter=n8n-playwright test:container:sqlite:e2e tests/e2e/building-blocks/workflow-entry-points.spec.ts
      workers: '1'
      artifact-prefix: sanity
    secrets: inherit

  # Full e2e run. Internal PRs run multi-main (postgres + redis + caddy + 2 mains + 1 worker).
  # Fork PRs run sqlite-only and skip @licensed tests (no enterprise license secrets on forks).
  e2e:
    name: E2E
    needs: [install-and-build, prepare-docker]
    if: >-
      needs.prepare-docker.result == 'success' &&
      (needs.install-and-build.outputs.runtime == 'true'
        || needs.install-and-build.outputs.e2e == 'true'
        || contains(github.event.pull_request.labels.*.name, 'force-e2e')) &&
      needs.install-and-build.outputs.skip_tests != 'true' &&
      github.event_name != 'merge_group'
    uses: ./.github/workflows/test-e2e-reusable.yml
    with:
      branch: ${{ needs.install-and-build.outputs.commit_sha }}
      test-mode: docker-artifact
      test-command: ${{ github.event.pull_request.head.repo.fork == true && 'pnpm --filter=n8n-playwright test:container:sqlite:e2e --grep-invert=@licensed' || 'pnpm --filter=n8n-playwright test:container:multi-main:e2e' }}
      workers: '1'
      pre-generated-matrix: ${{ needs.install-and-build.outputs.matrix }}
      artifact-prefix: e2e
    secrets: inherit

  # Boots the editor-ui against the Vite dev server and fails on any console
  # or page error during load. Catches regressions in dev-mode module
  # resolution (missing Vite alias, broken workspace package interop) that
  # the production-bundle e2e job bundles around.
  dev-server-smoke:
    name: Dev-server boot smoke
    needs: install-and-build
    if: needs.install-and-build.outputs.dev_server_smoke == 'true' && github.event_name != 'merge_group'
    uses: ./.github/workflows/test-dev-server-smoke-reusable.yml
    with:
      ref: ${{ needs.install-and-build.outputs.commit_sha }}
    secrets: inherit

  db-tests:
    name: DB Tests
    needs: install-and-build
    if: needs.install-and-build.outputs.db == 'true'
    uses: ./.github/workflows/test-db-reusable.yml
    with:
      ref: ${{ needs.install-and-build.outputs.commit_sha }}

  performance:
    name: Performance
    needs: install-and-build
    if: needs.install-and-build.outputs.performance == 'true' && github.event_name != 'merge_group'
    uses: ./.github/workflows/test-bench-reusable.yml
    with:
      ref: ${{ needs.install-and-build.outputs.commit_sha }}

  e2e-performance:
    name: E2E Performance
    needs: [install-and-build, prepare-docker]
    # Performance is internal-only (license secrets required, not available on forks).
    if: >-
      needs.prepare-docker.result == 'success' &&
      needs.install-and-build.outputs.e2e_performance == 'true' &&
      github.event.pull_request.head.repo.fork != true
    uses: ./.github/workflows/test-e2e-performance-reusable.yml
    secrets: inherit

  security-checks:
    name: Security Checks
    needs: install-and-build
    if: needs.install-and-build.outputs.workflows == 'true'
    uses: ./.github/workflows/sec-ci-reusable.yml
    with:
      ref: ${{ needs.install-and-build.outputs.commit_sha }}
    secrets: inherit

  workflow-scripts:
    name: Workflow scripts
    needs: install-and-build
    if: needs.install-and-build.outputs.workflow_scripts == 'true'
    uses: ./.github/workflows/test-workflow-scripts-reusable.yml
    with:
      ref: ${{ needs.install-and-build.outputs.commit_sha }}
    secrets: inherit

  # In-process discovery eval — asserts the orchestrator reaches for browser/computer-use
  # tools at OAuth/screenshot moments. Lightweight (no Docker), runs in parallel with the
  # heavy workflow eval. Non-blocking initially; promote to required after stability.
  instance-ai-discovery-evals:
    name: Instance AI Discovery Evals
    needs: install-and-build
    if: >-
      !cancelled() &&
      needs.install-and-build.result == 'success' &&
      needs.install-and-build.outputs.instance_ai_workflow_eval == 'true' &&
      github.repository == 'n8n-io/n8n' &&
      (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork)
    uses: ./.github/workflows/test-evals-discovery.yml
    with:
      branch: ${{ needs.install-and-build.outputs.commit_sha }}
    secrets: inherit

  # This job is required by GitHub branch protection rules.
  # PRs cannot be merged unless this job passes.
  required-checks:
    name: Required Checks
    needs:
      [
        install-and-build,
        unit-test,
        typecheck,
        lint,
        check-packaging,
        sqlite-sanity,
        e2e,
        dev-server-smoke,
        db-tests,
        performance,
        security-checks,
        workflow-scripts,
      ]
    if: always()
    runs-on: ubuntu-slim
    steps:
      - uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
        with:
          sparse-checkout: .github/actions/ci-filter
          sparse-checkout-cone-mode: false
      - name: Validate required checks
        uses: ./.github/actions/ci-filter
        with:
          mode: validate
          job-results: ${{ toJSON(needs) }}

  # Posts a QA metrics comparison comment on the PR.
  # Runs after all checks so any job can emit metrics before this reports.
  post-qa-metrics-comment:
    name: QA Metrics
    needs: [required-checks, e2e-performance]
    if: always()
    uses: ./.github/workflows/util-qa-metrics-comment-reusable.yml
    with:
      metrics: memory-heap-used-baseline,memory-rss-baseline,instance-ai-heap-used-baseline,instance-ai-rss-baseline,agents-heap-used-baseline,agents-rss-baseline,docker-image-size-n8n,docker-image-size-runners
    secrets: inherit
