name: Build

on:
  push:
    branches:
      - main
      - next
      - maint/**

  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review

  merge_group:

  workflow_dispatch:
    inputs:
      dryRun:
        description: 'Dry-Run'
        default: 'true'
        required: false

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
  cancel-in-progress: true

env:
  # are we on a release branch?
  DO_REALEASE: ${{ github.ref_name == github.event.repository.default_branch || github.ref_name == 'next' || startsWith(github.ref_name, 'maint/')}}
  NODE_VERSION: 22
  PDM_VERSION: 2.25.6 # renovate: datasource=pypi depName=pdm
  DRY_RUN: true
  TEST_LEGACY_DECRYPTION: true
  SPARSE_CHECKOUT: |-
    .github/actions/
    data/
    patches/
    tools/
    package.json
    pnpm-lock.yaml

jobs:
  setup:
    runs-on: ubuntu-latest

    outputs:
      os-matrix: ${{ steps.os-matrix.outputs.os-matrix }}
      os-matrix-is-full: ${{ steps.os-matrix-is-full.outputs.os-matrix-is-full }}
      os-matrix-prefetch: ${{ steps.os-matrix-prefetch.outputs.matrix }}
      test-shard-matrix: ${{ steps.schedule-test-shards.outputs.test-shard-matrix }}
      test-matrix-empty: ${{ steps.schedule-test-shards.outputs.test-matrix-empty }}

    steps:
      - name: Calculate `os-matrix-is-full` output
        id: os-matrix-is-full
        env:
          IS_FULL: >-
            ${{
              (
                github.event_name != 'pull_request' ||
                contains(github.event.pull_request.labels.*.name, 'ci:fulltest')
              ) && 'true' || ''
            }}
        run: |
          echo 'OS_MATRIX_IS_FULL=${{ env.IS_FULL }}' >> "$GITHUB_ENV"
          echo 'os-matrix-is-full=${{ env.IS_FULL }}' >> "$GITHUB_OUTPUT"

      - name: Calculate `os-matrix` output
        id: os-matrix
        env:
          OS_ALL: '["ubuntu-latest", "macos-latest", "windows-latest"]'
          OS_LINUX_ONLY: '["ubuntu-latest"]'
        run: |
          echo 'os-matrix=${{
            env.OS_MATRIX_IS_FULL && env.OS_ALL || env.OS_LINUX_ONLY
          }}' >> "$GITHUB_OUTPUT"

      - name: Detect changed files
        if: ${{ github.event_name == 'pull_request' }}
        id: changed-files
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.event.repository.full_name }}
          PR_URL: >-
            https://api.github.com/repos/{owner}/{repo}/compare/${{
              github.event.pull_request.base.sha
            }}...${{
              github.event.pull_request.head.sha
            }}
          JQ_FILTER: >-
            "changed-files=" + ([.files[].filename] | tostring)
        run: gh api ${{ env.PR_URL }} | jq -rc '${{ env.JQ_FILTER }}' >> "$GITHUB_OUTPUT"

      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          filter: blob:none # we don't need all blobs
          sparse-checkout: ${{ env.SPARSE_CHECKOUT }}
          show-progress: false

      - name: Calculate matrix for `node_modules` prefetch
        uses: ./.github/actions/calculate-prefetch-matrix
        id: os-matrix-prefetch
        with:
          repo: ${{ github.event.repository.full_name }}
          token: ${{ github.token }}
          node-version: ${{ env.NODE_VERSION }}

      - name: Prefetch test modules for `ubuntu-latest`
        id: setup-node
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ env.NODE_VERSION }}
          os: ${{ runner.os }}
          save-cache: true

      - name: Schedule test shards
        id: schedule-test-shards
        env:
          ALL_PLATFORMS: ${{ env.OS_MATRIX_IS_FULL }}
          FILTER_SHARDS: ${{ github.event.pull_request.draft && 'true' || '' }}
          CHANGED_FILES: ${{ steps.changed-files.outputs.changed-files }}
        run: |
          echo "$(pnpm -s schedule-test-shards)" >> "$GITHUB_OUTPUT"

  setup-build:
    runs-on: ubuntu-latest

    outputs:
      node-version: ${{ steps.setup-outputs.outputs.node-version }}

    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          filter: blob:none # we don't need all blobs
          sparse-checkout: ${{ env.SPARSE_CHECKOUT }}
          show-progress: false

      - name: Setup outputs
        id: setup-outputs
        run: |
          echo "node-version=${{ env.NODE_VERSION }}" >> "$GITHUB_OUTPUT"

      - name: Prefetch build modules for `ubuntu-latest`
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ env.NODE_VERSION }}
          os: ${{ runner.os }}
          save-cache: true

  prefetch:
    needs: [setup]

    # We can't check `needs.setup.outputs.os-matrix-is-full` here,
    # as it will lead to further complications that aren't solvable
    # with current GitHub Actions feature set.
    #
    # Although this job sometimes may act as short-lived `no-op`,
    # it's actually the best option available.
    #
    # However, in draft mode we can skip this step.
    if: |
      !(github.event.pull_request.draft == true &&
        needs.setup.outputs.test-matrix-empty == 'true')

    strategy:
      matrix:
        os: ${{ fromJSON(needs.setup.outputs.os-matrix-prefetch) }}

    runs-on: ${{ matrix.os }}

    timeout-minutes: 10

    steps:
      - name: Checkout code
        if: needs.setup.outputs.os-matrix-is-full && runner.os != 'Linux'
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          filter: blob:none # we don't need all blobs
          sparse-checkout: ${{ env.SPARSE_CHECKOUT }}
          show-progress: false

      - name: Setup Node.js
        if: needs.setup.outputs.os-matrix-is-full && runner.os != 'Linux'
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ env.NODE_VERSION }}
          os: ${{ runner.os }}
          save-cache: true

  lint-eslint:
    needs:
      - setup-build
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Restore eslint cache
        uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
        with:
          path: .cache/eslint
          # we need to add the hash because eslint cache doesn't detect plugin changes
          key: eslint-cache-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}
          restore-keys: eslint-cache-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}-

      - name: Lint
        run: pnpm eslint-ci

      - name: Save eslint cache
        if: github.event_name == 'push'
        uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
        with:
          path: .cache/eslint
          key: eslint-cache-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}-${{ github.run_id }}-${{ github.run_attempt }}

  lint-prettier:
    needs:
      - setup-build
    runs-on: ubuntu-latest
    timeout-minutes: 7

    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Restore prettier cache
        uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
        with:
          path: .cache/prettier
          # we need to add the hash because prettier cache doesn't detect plugin changes
          key: prettier-cache-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}
          restore-keys: prettier-cache-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}-

      - name: Lint
        run: |
          pnpm prettier --write --cache-location .cache/prettier
          git diff --quiet || {
            echo "[ERROR] Please apply the changes prettier suggests:"
            git diff --color=always
            exit 1
          }

      - name: Save prettier cache
        if: github.event_name == 'push'
        uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
        with:
          path: .cache/prettier
          key: prettier-cache-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}-${{ github.run_id }}-${{ github.run_attempt }}

  lint-docs:
    needs:
      - setup-build
    runs-on: ubuntu-latest
    timeout-minutes: 7

    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Lint markdown
        uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0

      - name: Lint fenced code blocks
        run: pnpm doc-fence-check

      - name: Lint documentation
        run: pnpm lint-documentation

      - name: Markdown lint
        run: pnpm markdown-lint

  lint-other:
    needs:
      - setup-build
    runs-on: ubuntu-latest
    timeout-minutes: 7

    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Type check
        run: pnpm type-check

      - name: Lint project file structure
        run: pnpm ls-lint

      - name: Check git version
        run: pnpm git-check

      - name: Test schema
        run: pnpm test-schema

      - name: Lint other
        run: pnpm lint-other

  test:
    needs: [setup, prefetch]

    if: |
      !(github.event.pull_request.draft == true &&
        needs.setup.outputs.test-matrix-empty == 'true')

    name: ${{ matrix.name }}

    runs-on: ${{ matrix.os }}
    timeout-minutes: ${{ matrix.runner-timeout-minutes }}

    strategy:
      matrix:
        include: ${{ fromJSON(needs.setup.outputs.test-shard-matrix) }}

    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ env.NODE_VERSION }}
          os: ${{ runner.os }}

      - name: Cache vitest
        uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
        with:
          path: .cache/vitest
          key: |
            vitest-cache-${{
              runner.os
            }}-${{
              env.NODE_VERSION
            }}-${{
              hashFiles('pnpm-lock.yaml')
            }}-${{
              matrix.cache-key
            }}

      - name: Unit tests
        shell: bash
        run: |
          for shard in ${{ matrix.shards }};
          do
            TEST_SHARD="$shard" pnpm vitest \
              --test-timeout ${{ matrix.test-timeout-milliseconds }} \
              --coverage ${{ matrix.coverage }}
          done

      - name: Move coverage files
        if: success() && github.event.pull_request.draft != true && matrix.coverage
        run: |
          mkdir -p ./coverage/lcov
          mkdir -p ./coverage/json
          for shard in ${{ matrix.shards }};
          do
            mv ./coverage/shard/$shard/lcov.info ./coverage/lcov/$shard.lcov
            mv ./coverage/shard/$shard/coverage-final.json ./coverage/json/$shard.json
          done

      - name: Save coverage artifacts
        if: success() && github.event.pull_request.draft != true && matrix.coverage
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: ${{ matrix.upload-artifact-name }}
          path: |
            ./coverage/lcov
            ./coverage/json

  codecov:
    needs: [test]
    runs-on: ubuntu-latest
    timeout-minutes: 3
    if: success() && github.event_name != 'merge_group' && github.event.pull_request.draft != true
    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          filter: blob:none # we don't need all blobs
          show-progress: false

      - name: Download coverage reports
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
        with:
          pattern: coverage-*
          path: coverage
          merge-multiple: true

      - name: Codecov
        uses: codecov/codecov-action@39a2af19d997be74586469d4062e173ecae614f6 # v5.4.3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          directory: coverage/lcov
          fail_ci_if_error: github.event_name != 'pull_request'
          verbose: true

  coverage-threshold:
    needs:
      - test
      - setup-build
    runs-on: ubuntu-latest
    timeout-minutes: 3
    if: success() && github.event.pull_request.draft != true
    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          filter: blob:none # we don't need all blobs
          sparse-checkout: ${{ env.SPARSE_CHECKOUT }}
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Download coverage reports
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
        with:
          pattern: coverage-*
          path: coverage
          merge-multiple: true

      - name: Merge coverage reports
        run: pnpm nyc merge ./coverage/json ./coverage/nyc/coverage.json

      - name: Report coverage
        run: pnpm nyc report -t ./coverage/nyc --skip-full -r text -r text-summary

      - name: Check coverage threshold
        run: |
          pnpm nyc check-coverage -t ./coverage/nyc \
            --branches 98.16 \
            --functions 99.51 \
            --lines 98.22 \
            --statements 98.22

  # Catch-all required check for test matrix and coverage
  test-success:
    needs:
      - setup
      - test
      - codecov
      - coverage-threshold
    runs-on: ubuntu-latest
    timeout-minutes: 1
    if: always()
    steps:
      - name: Fail for failed or cancelled tests
        if: |
          needs.test.result == 'failure' ||
          needs.test.result == 'cancelled'
        run: exit 1

      - name: Fail for skipped tests when PR is ready for review
        if: |
          github.event_name == 'pull_request' &&
          github.event.pull_request.draft != true &&
          needs.test.result == 'skipped'
        run: exit 1

      - name: Fail for failed or cancelled codecov
        if: |
          needs.codecov.result == 'failure' ||
          needs.codecov.result == 'cancelled'
        run: exit 1

      - name: Fail for skipped codecov when PR is ready for review
        if: |
          github.event_name == 'pull_request' &&
          github.event.pull_request.draft != true &&
          needs.codecov.result == 'skipped'
        run: exit 1

      - name: Fail for failed or cancelled coverage-threshold
        if: |
          needs.coverage-threshold.result == 'failure' ||
          needs.coverage-threshold.result == 'cancelled'
        run: exit 1

      - name: Fail for skipped coverage-threshold when PR is ready for review
        if: |
          github.event_name == 'pull_request' &&
          github.event.pull_request.draft != true &&
          needs.coverage-threshold.result == 'skipped'
        run: exit 1

  build:
    needs:
      - setup-build
    runs-on: ubuntu-latest
    timeout-minutes: 15
    if: github.event.pull_request.draft != true
    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Build
        run: pnpm build

      - name: Upload dist
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: renovate-dist
          path: dist/

  build-docker:
    needs:
      - build
    runs-on: ubuntu-latest
    timeout-minutes: 15
    if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:fulltest')
    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Download dist
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
        with:
          name: renovate-dist
          path: dist/

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Build docker
        run: pnpm build:docker build --tries=3 --args '--load'
        env:
          LOG_LEVEL: debug

      - name: Test docker
        run: docker run -e LOG_LEVEL=debug --rm renovate/renovate --version

      - name: dry-run
        if: github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && env.DO_REALEASE != 'true')
        run: docker run -e LOG_LEVEL=debug -e RENOVATE_TOKEN --rm renovate/renovate --dry-run=lookup ${{ github.repository }}
        env:
          RENOVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  build-docs:
    needs:
      - lint-docs
      - setup-build
    runs-on: ubuntu-latest
    timeout-minutes: 5
    if: github.event.pull_request.draft != true
    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Setup PDM
        uses: pdm-project/setup-pdm@94a823180e06fcde4ad29308721954a521c96ed0 # v4.4
        with:
          python-version-file: .python-version
          version: ${{ env.PDM_VERSION }}
          cache: true

      - name: Install pdm dependencies
        run: pdm install

      - name: Build
        run: pnpm build:docs
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SKIP_GITHUB_ISSUES: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && 'true' || '' }}

      - name: Test docs
        run: pnpm test:docs

      - name: Upload
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: docs
          path: tmp/docs/

      - name: Build mkdocs
        run: pnpm mkdocs build --no-build

  test-e2e:
    needs: [build]
    runs-on: 'ubuntu-latest'
    timeout-minutes: 7

    if: github.event.pull_request.draft != true

    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          show-progress: false

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - name: Download dist
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
        with:
          name: renovate-dist
          path: dist/

      - name: Pack
        run: pnpm test-e2e:pack

      - name: Install dependencies
        run: pnpm test-e2e:install

      - name: E2E Test
        run: pnpm test-e2e:run

  release:
    needs:
      - setup-build
      - build-docker
      - lint-eslint
      - lint-prettier
      - lint-docs
      - lint-other
      - test-e2e
      - test-success
      - build-docs
      - codecov
      - coverage-threshold

    if: github.repository == 'renovatebot/renovate' && github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    timeout-minutes: 60
    permissions:
      contents: write
      issues: write
      pull-requests: write
      id-token: write
      packages: write

    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          fetch-depth: 0 # zero stands for full checkout, which is required for semantic-release
          filter: blob:none # we don't need all blobs, only the full tree
          show-progress: false

      - name: docker-config
        uses: containerbase/internal-tools@74041d37e882259b3f18de5bcce4956fae9f9ba0 # v3.10.75
        with:
          command: docker-config

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          node-version: ${{ needs.setup-build.outputs.node-version }}
          os: ${{ runner.os }}

      - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2

      - name: Docker registry login
        run: |
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin

      - name: Check dry run
        run: |
          if [[ "${{github.event_name}}" == "workflow_dispatch" && "${{ github.event.inputs.dryRun }}" != "true"  ]]; then
            echo "DRY_RUN=false" >> "$GITHUB_ENV"
          elif [[ "${{env.DO_REALEASE}}" == "true" ]]; then
            echo "DRY_RUN=false" >> "$GITHUB_ENV"
          fi

      - name: semantic-release
        run: |
          pnpm semantic-release --dry-run ${{env.DRY_RUN}}
        env:
          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # TODO: use action token?
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          LOG_LEVEL: debug
