# Copyright (c) 2023-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/license/mit.

name: CI
on:
  # See: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request.
  pull_request:
  # See: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push.
  push:
    branches:
      - '**'
    tags-ignore:
      - '**'

concurrency:
  group: ${{ github.event_name != 'pull_request' && github.run_id || github.ref }}
  cancel-in-progress: true

env:
  CI_FAILFAST_TEST_LEAVE_DANGLING: 1  # GHA does not care about dangling processes and setting this variable avoids killing the CI script itself on error
  REPO_USE_WARP_RUNNERS: 'bitcoin/bitcoin' # Use warp runners for this repo, instead of falling back to the slow GHA runners

defaults:
  run:
    # Enforce fail-fast behavior for all platforms.
    # See: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
    shell: bash

jobs:
  runners:
    name: '[meta] determine runners'
    runs-on: ubuntu-slim
    outputs:
      provider: ${{ steps.runners.outputs.provider }}
    steps:
      - &ANNOTATION_PR_NUMBER
        name: Annotate with pull request number
        # This annotation is machine-readable and can be used to assign a check
        # run to its corresponding pull request. Running in all check runs is
        # required, because check re-runs discard the annotations of other
        # tasks in the test suite.
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            echo "::notice title=debug_pull_request_number_str::${{ github.event.number }}"
          fi
      - id: runners
        run: |
          if [[ "${REPO_USE_WARP_RUNNERS}" == "${{ github.repository }}" ]]; then
            echo "provider=warp" >> "$GITHUB_OUTPUT"
            echo "::notice title=Runner Selection::Using Warp Runners"
          else
            echo "provider=gha" >> "$GITHUB_OUTPUT"
            echo "::notice title=Runner Selection::Using GitHub-hosted runners"
          fi

  test-each-commit:
    name: 'test ancestor commits'
    needs: runners
    runs-on: ${{ needs.runners.outputs.provider == 'warp' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }}
    env:
      TEST_RUNNER_PORT_MIN: "14000"  # Use a larger port range to avoid colliding with other CI services.
    if: github.event_name == 'pull_request' && github.event.pull_request.commits != 1
    timeout-minutes: 360  # Use maximum time, see https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes.
    steps:
      - name: Determine fetch depth
        run: echo "FETCH_DEPTH=$((${{ github.event.pull_request.commits }} + 2))" >> "$GITHUB_ENV"
      - *ANNOTATION_PR_NUMBER
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: ${{ env.FETCH_DEPTH }}
      - name: Determine commit range
        run: |
          # Checkout HEAD~ and find the test base commit.
          # Checkout HEAD~ because it would be wasteful to rerun
          # tests on the PR head commit that are already run
          # by other jobs.
          git checkout HEAD~
          # Moreover, pull requests that contain a merge commit
          # are generally draft pull requests that merge in other
          # pull requests, so only check the relevant commits
          # after the last merge commit. A merge commit could
          # also be a subtree merge commit, which may be
          # worthwhile to check. However, it is rare that the
          # subtree merge commit is not the top commit (which
          # would be skipped anyway by this task, because it is
          # run by all other tasks). Also, `git rebase --exec`
          # does not work on merge commits, so if this was
          # important to check, the logic would have to be
          # rewritten.
          #
          # Figure out test base commit by listing ancestors of
          # HEAD, excluding ancestors of the most recent merge
          # commit, ordering them from oldest to newest, and
          # taking the first one.
          #
          # In the command below, the ^@ suffix is used to refer
          # to all parents of the merge commit as described in:
          # https://git-scm.com/docs/git-rev-parse#_other_rev_parent_shorthand_notations
          # and the ^ prefix is used to exclude these parents
          # and all their ancestors from the rev-list output
          # as described in:
          # https://git-scm.com/docs/git-rev-list
          MERGE_BASE=$(git rev-list -n1 --merges HEAD)
          EXCLUDE_MERGE_BASE_ANCESTORS=
          # MERGE_BASE can be empty due to limited fetch-depth
          if test -n "$MERGE_BASE"; then
            EXCLUDE_MERGE_BASE_ANCESTORS=^${MERGE_BASE}^@
          fi
          echo "TEST_BASE=$(git rev-list -n${{ github.event.pull_request.commits }} --reverse HEAD $EXCLUDE_MERGE_BASE_ANCESTORS | head -1)" >> "$GITHUB_ENV"
      - run: |
          git fetch origin "${GITHUB_BASE_REF}"
          git config user.email "ci@example.com"
          git config user.name "CI"
      - run: |
          sudo apt-get update
          sudo apt-get install clang mold ccache build-essential cmake ninja-build pkgconf python3-zmq libevent-dev libboost-dev libsqlite3-dev systemtap-sdt-dev libzmq3-dev qt6-base-dev qt6-tools-dev qt6-l10n-tools libqrencode-dev capnproto libcapnp-dev -y
          sudo pip3 install --break-system-packages pycapnp
      - name: Compile and run tests
        run: |
          # Run tests on commits after the last merge commit and before the PR head commit
          git rebase --exec "git merge --no-commit origin/${GITHUB_BASE_REF} && python3 ./.github/ci-test-each-commit-exec.py && git reset --hard" ${{ env.TEST_BASE }}

  macos-native-arm64:
    name: ${{ matrix.job-name }}
    # Use any image to support the xcode-select below, but hardcode version to avoid silent upgrades (and breaks).
    # See: https://github.com/actions/runner-images#available-images.
    runs-on: macos-15

    # When a contributor maintains a fork of the repo, any pull request they make
    # to their own fork, or to the main repository, will trigger two CI runs:
    # one for the branch push and one for the pull request.
    # This can be avoided by setting SKIP_BRANCH_PUSH=true as a custom env variable
    # in Github repository settings.
    if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}

    timeout-minutes: 120

    strategy:
      fail-fast: false
      matrix:
        job-type: [standard, fuzz]
        include:
          - job-type: standard
            file-env: './ci/test/00_setup_env_mac_native.sh'
            job-name: 'macOS native'
          - job-type: fuzz
            file-env: './ci/test/00_setup_env_mac_native_fuzz.sh'
            job-name: 'macOS native, fuzz'

    env:
      DANGER_RUN_CI_ON_HOST: 1
      BASE_ROOT_DIR: ${{ github.workspace }}/repo_archive

    steps:
      - *ANNOTATION_PR_NUMBER

      - &CHECKOUT
        name: Checkout
        uses: actions/checkout@v6
        with:
          # Ensure the latest merged pull request state is used, even on re-runs.
          ref: &CHECKOUT_REF_TMPL ${{ github.event_name == 'pull_request' && github.ref || '' }}

      - name: Clang version
        run: |
          # Use the latest Xcode supported by the version of macOS denoted in
          # doc/release-notes-empty-template.md and providing at least the
          # minimum clang version denoted in doc/dependencies.md.
          # See: https://developer.apple.com/documentation/xcode-release-notes/xcode-16_2-release-notes
          sudo xcode-select --switch /Applications/Xcode_16.2.app
          clang --version

      - name: Install Homebrew packages
        env:
          HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
        run: |
          # A workaround for "The `brew link` step did not complete successfully" error.
          brew install --quiet python@3 || brew link --overwrite python@3
          brew install --quiet coreutils ninja pkgconf ccache boost libevent zeromq qt@6 qrencode capnp

      - name: Set Ccache directory
        run: echo "CCACHE_DIR=${RUNNER_TEMP}/ccache_dir" >> "$GITHUB_ENV"

      - name: Restore Ccache cache
        id: ccache-cache
        uses: actions/cache/restore@v5
        with:
          path: ${{ env.CCACHE_DIR }}
          key: ${{ github.job }}-${{ matrix.job-type }}-ccache-${{ github.run_id }}
          restore-keys: ${{ github.job }}-${{ matrix.job-type }}-ccache-

      - name: Create git archive
        run: |
          git log -1
          git archive --format=tar --prefix=repo_archive/ --output=repo.tar HEAD
          tar -xf repo.tar

      - name: CI script
        run: |
          cd repo_archive
          ./ci/test_run_all.sh
        env:
          FILE_ENV: ${{ matrix.file-env }}

      - name: Save Ccache cache
        uses: actions/cache/save@v5
        if: github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch && steps.ccache-cache.outputs.cache-hit != 'true'
        with:
          path: ${{ env.CCACHE_DIR }}
          # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#update-a-cache
          key: ${{ steps.ccache-cache.outputs.cache-primary-key }}

  windows-native-dll:
    name: ${{ matrix.job-name }}
    runs-on: windows-2025-vs2026

    if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}

    env:
      PYTHONUTF8: 1
      TEST_RUNNER_TIMEOUT_FACTOR: 40

    strategy:
      fail-fast: false
      matrix:
        job-type: [standard, fuzz]
        include:
          - job-type: standard
            job-name: 'Windows native, VS'
          - job-type: fuzz
            job-name: 'Windows native, fuzz, VS'

    steps:
      - *ANNOTATION_PR_NUMBER

      - *CHECKOUT

      - &IMPORT_VS_ENV
        name: Import Visual Studio env vars
        run: py -3 .github/ci-windows.py "standard" github_import_vs_env

      - name: Get tool information
        run: |
          set -o errexit -o pipefail -o xtrace -o nounset

          cmake -version | tee cmake_version
          echo '---'
          msbuild.exe -version | tee msbuild_version
          echo "${VCToolsVersion-}" | tee toolset_version
          py -3 --version
          bash --version

      - name: Restore vcpkg downloads cache
        id: vcpkg-downloads-cache
        uses: actions/cache/restore@v5
        with:
          path: |
            ~/AppData/Local/vcpkg/downloads/*
            !~/AppData/Local/vcpkg/downloads/tools
          key: ${{ github.job }}-vcpkg-downloads-${{ hashFiles('vcpkg.json') }}

      - name: Restore vcpkg binary cache
        uses: actions/cache/restore@v5
        id: vcpkg-binary-cache
        with:
          path: ~/AppData/Local/vcpkg/archives
          key: ${{ github.job }}-vcpkg-binary-release-${{ hashFiles('cmake_version', 'msbuild_version', 'toolset_version', 'vcpkg.json') }}

      - name: Generate build system
        run: |
          py -3 .github/ci-windows.py ${{ matrix.job-type }} generate

      - name: Save vcpkg binary cache
        uses: actions/cache/save@v5
        if: github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch && steps.vcpkg-binary-cache.outputs.cache-hit != 'true' && matrix.job-type == 'standard'
        with:
          path: ~/AppData/Local/vcpkg/archives
          key: ${{ steps.vcpkg-binary-cache.outputs.cache-primary-key }}

      - name: Save vcpkg downloads cache
        uses: actions/cache/save@v5
        # Only save cache from the 'standard' job, as it includes the necessary downloads for other jobs in the matrix. If the matrix is modified, this may need amending.
        if: github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch && steps.vcpkg-binary-cache.outputs.cache-hit != 'true' && steps.vcpkg-downloads-cache.outputs.cache-hit != 'true' && matrix.job-type == 'standard'
        with:
          # Cache the tools once as archives, but not redundantly in extracted form.
          path: |
            ~/AppData/Local/vcpkg/downloads/*
            !~/AppData/Local/vcpkg/downloads/tools
          key: ${{ steps.vcpkg-downloads-cache.outputs.cache-primary-key }}

      - name: Build
        run: |
          py -3 .github/ci-windows.py ${{ matrix.job-type }} build

      - name: Check executable manifests
        run: |
          py -3 .github/ci-windows.py ${{ matrix.job-type }} check_manifests

      - name: Prepare tests
        run: |
          py -3 .github/ci-windows.py ${{ matrix.job-type }} prepare_tests

      - name: Run tests
        env:
          TEST_RUNNER_EXTRA: "--timeout-factor=${{ env.TEST_RUNNER_TIMEOUT_FACTOR }} ${{ case(github.event_name == 'pull_request', '', '--extended') }}"
        run: |
          py -3 .github/ci-windows.py ${{ matrix.job-type }} run_tests

  record-frozen-commit:
    # Record frozen commit, so that the native tests on cross-builds can run on
    # the exact same commit id of the build.
    name: '[meta] record frozen commit'
    runs-on: ubuntu-slim
    outputs:
      commit: ${{ steps.record-commit.outputs.commit }}
    steps:
      - *ANNOTATION_PR_NUMBER
      - *CHECKOUT
      - name: Record commit
        id: record-commit
        run: echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

  windows-cross:
    name: 'Windows-cross to x86_64, ${{ matrix.crt }}'
    needs: [runners, record-frozen-commit]
    runs-on: ${{ needs.runners.outputs.provider == 'warp' && 'warp-ubuntu-latest-x64-4x' || 'ubuntu-latest' }}
    if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}

    strategy:
      fail-fast: false
      matrix:
        crt: [msvcrt, ucrt]
        include:
          - crt: msvcrt
            file-env: './ci/test/00_setup_env_win64_msvcrt.sh'
            artifact-name: 'x86_64-w64-mingw32-executables'
          - crt: ucrt
            file-env: './ci/test/00_setup_env_win64.sh'
            artifact-name: 'x86_64-w64-mingw32ucrt-executables'

    env:
      FILE_ENV: ${{ matrix.file-env }}
      DANGER_CI_ON_HOST_FOLDERS: 1

    steps:
      - *ANNOTATION_PR_NUMBER

      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ needs.record-frozen-commit.outputs.commit }}

      - name: Configure environment
        uses: ./.github/actions/configure-environment

      - name: Restore caches
        id: restore-cache
        uses: ./.github/actions/cache/restore
        with:
          provider: ${{ needs.runners.outputs.provider }}

      - name: Configure Docker
        uses: ./.github/actions/configure-docker
        with:
          provider: ${{ needs.runners.outputs.provider }}

      - name: CI script
        run: ./ci/test_run_all.sh

      - name: Save caches
        uses: ./.github/actions/cache/save
        with:
          provider: ${{ needs.runners.outputs.provider }}

      - name: Upload built executables
        uses: actions/upload-artifact@v7
        with:
          name: ${{ matrix.artifact-name }}-${{ github.run_id }}
          path: |
            ${{ env.BASE_BUILD_DIR }}/bin/*.dll
            ${{ env.BASE_BUILD_DIR }}/bin/*.exe
            ${{ env.BASE_BUILD_DIR }}/src/secp256k1/bin/*.exe
            ${{ env.BASE_BUILD_DIR }}/src/univalue/*.exe
            ${{ env.BASE_BUILD_DIR }}/test/config.ini

  windows-native-test:
    name: 'Windows, ${{ matrix.crt }}, test cross-built'
    runs-on: windows-2022
    needs: [windows-cross, record-frozen-commit]

    strategy:
      fail-fast: false
      matrix:
        crt: [msvcrt, ucrt]
        include:
          - crt: msvcrt
            artifact-name: 'x86_64-w64-mingw32-executables'
          - crt: ucrt
            artifact-name: 'x86_64-w64-mingw32ucrt-executables'

    env:
      PYTHONUTF8: 1
      TEST_RUNNER_TIMEOUT_FACTOR: 40

    steps:
      - *ANNOTATION_PR_NUMBER

      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ needs.record-frozen-commit.outputs.commit }}

      - name: Download built executables
        uses: actions/download-artifact@v8
        with:
          name: ${{ matrix.artifact-name }}-${{ github.run_id }}

      - name: Run bitcoind.exe
        run: py -3 .github/ci-windows-cross.py print_version

      - *IMPORT_VS_ENV

      - name: Check executable manifests
        run: py -3 .github/ci-windows-cross.py check_manifests

      - name: Prepare Windows test environment
        run: py -3 .github/ci-windows-cross.py prepare_tests

      - name: Run unit tests
        run: py -3 .github/ci-windows-cross.py run_unit_tests

      - name: Run functional tests
        env:
          TEST_RUNNER_EXTRA: "--timeout-factor=${{ env.TEST_RUNNER_TIMEOUT_FACTOR }} ${{ case(github.event_name == 'pull_request', '', '--extended') }}"
        run: |
          py -3 .github/ci-windows-cross.py run_functional_tests

  ci-matrix:
    name: ${{ matrix.name }}
    needs: runners
    runs-on: ${{ needs.runners.outputs.provider == 'warp' && matrix.warp-runner || matrix.fallback-runner }}
    if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}
    timeout-minutes: ${{ matrix.timeout-minutes }}

    env:
      DANGER_CI_ON_HOST_FOLDERS: 1
      FILE_ENV: ${{ matrix.file-env }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - name: 'iwyu'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_iwyu.sh'

          - name: '32 bit ARM'
            warp-runner: 'ubuntu-24.04-arm' # Warp's Arm runners don't support 32-bit mode currently
            fallback-runner: 'ubuntu-24.04-arm'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_arm.sh'
            provider: 'gha'

          - name: 'ASan + LSan + UBSan + integer'
            warp-runner: 'warp-ubuntu-2404-x64-8x' # has to match container in ci/test/00_setup_env_native_asan.sh for tracing tools
            fallback-runner: 'ubuntu-24.04'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_asan.sh'

          - name: 'macOS-cross to arm64'
            warp-runner: 'warp-ubuntu-latest-x64-4x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_mac_cross.sh'

          - name: 'macOS-cross to x86_64'
            warp-runner: 'warp-ubuntu-latest-x64-4x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_mac_cross_intel.sh'

          - name: 'FreeBSD Cross'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_freebsd_cross.sh'

          - name: 'No wallet'
            warp-runner: 'warp-ubuntu-latest-x64-4x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_nowallet.sh'

          - name: 'i686, no IPC'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_i686_no_ipc.sh'

          - name: 'fuzzer,address,undefined,integer'
            warp-runner: 'warp-ubuntu-latest-x64-16x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 240
            file-env: './ci/test/00_setup_env_native_fuzz.sh'

          - name: 'previous releases'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_previous_releases.sh'

          - name: 'Alpine (musl)'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_alpine_musl.sh'

          - name: 'tidy'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_tidy.sh'

          - name: 'TSan'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_tsan.sh'

          - name: 'MSan, fuzz'
            warp-runner: 'warp-ubuntu-latest-x64-8x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 150
            file-env: './ci/test/00_setup_env_native_fuzz_with_msan.sh'

          - name: 'MSan'
            warp-runner: 'warp-ubuntu-latest-x64-16x'
            fallback-runner: 'ubuntu-latest'
            timeout-minutes: 120
            file-env: './ci/test/00_setup_env_native_msan.sh'

    steps:
      - *ANNOTATION_PR_NUMBER

      - *CHECKOUT

      - name: Configure environment
        uses: ./.github/actions/configure-environment

      - name: Restore caches
        id: restore-cache
        uses: ./.github/actions/cache/restore
        with:
          provider: ${{ matrix.provider || needs.runners.outputs.provider }}

      - name: Configure Docker
        uses: ./.github/actions/configure-docker
        with:
          provider: ${{ matrix.provider || needs.runners.outputs.provider }}

      - name: Clear unnecessary files
        if: ${{ needs.runners.outputs.provider == 'gha' && true || false }} # Only needed on GHA runners
        uses: ./.github/actions/clear-files

      - name: Enable bpfcc script
        if: ${{ env.CONTAINER_NAME == 'ci_native_asan' }}
        # In the image build step, no external environment variables are available,
        # so any settings will need to be written to the settings env file:
        run: sed -i "s|\${INSTALL_BCC_TRACING_TOOLS}|true|g" ./ci/test/00_setup_env_native_asan.sh

      - name: Set mmap_rnd_bits
        if: ${{ env.CONTAINER_NAME == 'ci_native_tsan' || env.CONTAINER_NAME == 'ci_native_msan' || env.CONTAINER_NAME == 'ci_native_fuzz_msan' }}
        # Prevents crashes due to high ASLR entropy
        run: sudo sysctl -w vm.mmap_rnd_bits=28

      - name: CI script
        run: ./ci/test_run_all.sh

      - name: Save caches
        uses: ./.github/actions/cache/save
        with:
          provider: ${{ matrix.provider || needs.runners.outputs.provider }}

  lint:
    name: 'lint'
    needs: runners
    runs-on: ${{ needs.runners.outputs.provider == 'warp' && 'warp-ubuntu-latest-x64-2x' || 'ubuntu-latest' }}
    if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}
    timeout-minutes: 20
    env:
      CONTAINER_NAME: "bitcoin-linter"
    steps:
      - *ANNOTATION_PR_NUMBER

      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: *CHECKOUT_REF_TMPL
          fetch-depth: 0

      - name: Configure Docker
        uses: ./.github/actions/configure-docker
        with:
          provider: ${{ needs.runners.outputs.provider }}

      - name: CI script
        run: |
          git worktree add ../lint-worktree HEAD
          ../lint-worktree/ci/lint.py
