name: CI

on:
  push:
    branches:
      - "master"
  pull_request:
    branches:
      - "*"
  merge_group:
    branches:
      - "master"

permissions:
  # Required to manage and delete caches.
  actions: write
  # Default permission for checking out code.
  contents: read

concurrency:
  # Cancel any previous workflows if they are from a PR or push.
  group: ${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash

env:
  # Accepts either a major image tag like "30" or a patch tag like "29.1".
  BITCOIN_VERSION: "31"
  
  # TRANCHES defines the number of tranches used in the itests.
  TRANCHES: 16

  # SMALL_TRANCHES defines the number of tranches used in the less stable itest
  # builds
  #
  # TODO(yy): remove this value and use TRANCHES.
  SMALL_TRANCHES: 8

  # If you change this please also update GO_VERSION in Makefile (then run
  # `make lint` to see where else it needs to be updated as well).
  GO_VERSION: 1.26.4

jobs:
  static-checks:
    name: Static Checks
    runs-on: ubuntu-latest
    steps:
      - name: Git checkout
        uses: actions/checkout@v5
        with:
          # Needed for some checks.
          fetch-depth: 0

      - name: Clean up runner space
        uses: ./.github/actions/cleanup-space

      - name: Setup Go ${{ env.GO_VERSION }}
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          use-build-cache: 'no'

      ########################
      # sample configuration check
      ########################
      - name: Check default values in sample-lnd.conf file
        run: make sample-conf-check

      ########################
      # Check code and RPC format
      ########################
      - name: Check code format
        run: make fmt-check

      - name: Check go modules tidiness 
        run: make tidy-module-check

      - name: Lint proto files
        run: make protolint

      ########################
      # SQLC code gen check 
      ########################
      - name: Docker image cache
        uses: satackey/action-docker-layer-caching@v0.0.11
        # Ignore the failure of a step and avoid terminating the job.
        continue-on-error: true

      - name: Check SQL models
        run: make sqlc-check

      ########################
      # RPC and mobile compilation check
      ########################
      - name: Check RPC format
        run: make rpc-check

      - name: Check JSON/WASM stub compilation
        run: make rpc-js-compile

      - name: Check mobile RPC bindings
        run: make mobile-rpc

      - name: Check mobile specific code
        run: go build --tags="mobile" ./mobile

  ########################
  # check commits
  ########################
  check-commits:
    if: github.event_name == 'pull_request'
    name: Check commits
    runs-on: ubuntu-latest
    steps:
      - name: git checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Clean up runner space
        uses: ./.github/actions/cleanup-space

      - name: setup go ${{ env.GO_VERSION }}
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          # Use the same cache from unit test job to save time.
          key-prefix: unit-test

      - name: fetch and rebase on ${{ github.base_ref }}
        uses: ./.github/actions/rebase

      - name: check commits
        run: scripts/check-each-commit.sh upstream/${{ github.base_ref }}

  ########################
  # lint code
  ########################
  lint:
    name: Lint code
    runs-on: ubuntu-latest
    steps:
      - name: git checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Clean up runner space
        uses: ./.github/actions/cleanup-space

      - name: setup go ${{ env.GO_VERSION }}
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          # Use the same cache from unit test job to save time.
          key-prefix: unit-test

      - name: lint
        run: GOGC=50 make lint

  ########################
  # cross compilation
  ########################
  cross-compile:
    name: Cross compilation
    runs-on: ubuntu-latest
    strategy:
      fail-fast: true
      matrix:
        # Please keep this list in sync with make/release_flags.mk!
        include:
          - name: i386
            sys: freebsd-386 linux-386 windows-386
          - name: amd64
            sys: darwin-amd64 freebsd-amd64 linux-amd64 netbsd-amd64 openbsd-amd64 windows-amd64
          - name: arm
            sys: darwin-arm64 freebsd-arm linux-armv6 linux-armv7 linux-arm64 windows-arm64
    steps:
      - name: Git checkout
        uses: actions/checkout@v5

      - name: Clean up runner space
        uses: ./.github/actions/cleanup-space

      - name: Setup go ${{ env.GO_VERSION }}
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          key-prefix: cross-compile
          use-build-cache: 'no'

      - name: Build release for all architectures
        run: make release sys="${{ matrix.sys }}"

  ########################
  # run unit tests
  ########################
  unit-test:
    name: Run unit tests
    runs-on: ubuntu-latest
    strategy:
      # Allow other tests in the matrix to continue if one fails.
      fail-fast: false
      matrix:
        unit_type:
          - unit-cover
          - unit tags="kvdb_etcd"
          - unit tags="kvdb_postgres"
          - unit tags="kvdb_sqlite"
          - unit tags="test_db_sqlite"
          - unit tags="test_db_postgres"
          - unit-race
          - unit-race tags="test_db_sqlite"
          - unit-race tags="test_db_postgres"
          - unit-module

    steps:
      - name: Git checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Clean up runner space
        uses: ./.github/actions/cleanup-space

      - name: Fetch and rebase on ${{ github.base_ref }}
        if: github.event_name == 'pull_request'
        uses: ./.github/actions/rebase

      - name: Git checkout fuzzing seeds
        uses: actions/checkout@v5
        with:
          repository: lightninglabs/lnd-fuzz
          path: lnd-fuzz

      - name: Rsync fuzzing seeds
        run: rsync -a --ignore-existing lnd-fuzz/ ./

      - name: Setup go ${{ env.GO_VERSION }}
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          key-prefix: unit-test

      - name: Install bitcoind
        run: ./scripts/install_bitcoind.sh $BITCOIN_VERSION

      - name: Run ${{ matrix.unit_type }}
        run: make ${{ matrix.unit_type }}

      - name: Clean coverage
        run: grep -Ev '(\.pb\.go|\.pb\.json\.go|\.pb\.gw\.go)' coverage.txt > coverage-norpc.txt
        if: matrix.unit_type == 'unit-cover'

      - name: Send coverage
        uses: coverallsapp/github-action@v2
        if: matrix.unit_type == 'unit-cover'
        continue-on-error: true
        with:
          file: coverage-norpc.txt
          flag-name: 'unit'
          format: 'golang'
          parallel: true


  ########################
  # run integration tests with TRANCHES
  ########################
  basic-integration-test:
    name: Run basic itests
    runs-on: ubuntu-latest
    strategy:
      # Allow other tests in the matrix to continue if one fails.
      fail-fast: false
      matrix:
        include:
          - name: btcd
            args: backend=btcd cover=1
          - name: bitcoind
            args: backend=bitcoind cover=1
          - name: bitcoind-miner
            args: backend=bitcoind minerbackend=bitcoind cover=1
          - name: bitcoind-notxindex
            args: backend="bitcoind notxindex"
          - name: neutrino
            args: backend=neutrino cover=1
    steps:
      - name: Git checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Check for no-itest label
        id: check-label
        uses: ./.github/actions/check-label
        with:
          label: 'no-itest'
          skip-message: "Tests auto-passed due to 'no-itest' label"

      - name: Clean up runner space
        if: steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/cleanup-space

      - name: Fetch and rebase on ${{ github.base_ref }}
        if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/rebase

      - name: Setup go ${{ env.GO_VERSION }}
        if: steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          key-prefix: integration-test

      - name: Install bitcoind
        if: steps.check-label.outputs.skip != 'true'
        run: ./scripts/install_bitcoind.sh $BITCOIN_VERSION

      - name: Run ${{ matrix.name }}
        if: steps.check-label.outputs.skip != 'true'
        run: make itest-parallel tranches=${{ env.TRANCHES }} ${{ matrix.args }} shuffleseed=${{ github.run_id }}${{ strategy.job-index }}

      - name: Clean coverage
        run: grep -Ev '(\.pb\.go|\.pb\.json\.go|\.pb\.gw\.go)' coverage.txt > coverage-norpc.txt
        if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}

      - name: Send coverage
        if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}
        continue-on-error: true
        uses: coverallsapp/github-action@v2
        with:
          file: coverage-norpc.txt
          flag-name: 'itest-${{ matrix.name }}'
          format: 'golang'
          parallel: true

      - name: Zip log files on failure
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        timeout-minutes: 5 # timeout after 5 minute
        run: 7z a logs-itest-${{ matrix.name }}.zip itest/**/*.log itest/postgres.log

      - name: Upload log files on failure
        uses: actions/upload-artifact@v4
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        with:
          name: logs-itest-${{ matrix.name }}
          path: logs-itest-${{ matrix.name }}.zip
          retention-days: 5

  ########################
  # run integration tests with SMALL_TRANCHES
  ########################
  integration-test:
    name: Run itests
    runs-on: ubuntu-latest
    strategy:
      # Allow other tests in the matrix to continue if one fails.
      fail-fast: false
      matrix:
        include:
          - name: bitcoind-rpcpolling
            args: backend="bitcoind rpcpolling"
          - name: bitcoind-etcd
            args: backend=bitcoind dbbackend=etcd
          - name: bitcoind-sqlite
            args: backend=bitcoind dbbackend=sqlite
          - name: bitcoind-sqlite-nativesql
            args: backend=bitcoind dbbackend=sqlite nativesql=true
          - name: bitcoind-sqlite=nativesql-experiment
            args: backend=bitcoind dbbackend=sqlite nativesql=true tags=test_native_sql
          - name: bitcoind-postgres
            args: backend=bitcoind dbbackend=postgres
          - name: bitcoind-postgres-nativesql
            args: backend=bitcoind dbbackend=postgres nativesql=true
          - name: bitcoind-postgres-nativesql-experiment
            args: backend=bitcoind dbbackend=postgres nativesql=true tags=test_native_sql
    steps:
      - name: Git checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Check for no-itest label
        id: check-label
        uses: ./.github/actions/check-label
        with:
          label: 'no-itest'
          skip-message: "Tests auto-passed due to 'no-itest' label"

      - name: Clean up runner space
        if: steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/cleanup-space

      - name: Fetch and rebase on ${{ github.base_ref }}
        if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/rebase

      - name: Setup go ${{ env.GO_VERSION }}
        if: steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          key-prefix: integration-test

      - name: Install bitcoind
        if: steps.check-label.outputs.skip != 'true'
        run: ./scripts/install_bitcoind.sh $BITCOIN_VERSION

      - name: Run ${{ matrix.name }}
        if: steps.check-label.outputs.skip != 'true'
        run: make itest-parallel tranches=${{ env.SMALL_TRANCHES }} ${{ matrix.args }} shuffleseed=${{ github.run_id }}${{ strategy.job-index }}

      - name: Clean coverage
        run: grep -Ev '(\.pb\.go|\.pb\.json\.go|\.pb\.gw\.go)' coverage.txt > coverage-norpc.txt
        if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}

      - name: Send coverage
        if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}
        continue-on-error: true
        uses: coverallsapp/github-action@v2
        with:
          file: coverage-norpc.txt
          flag-name: 'itest-${{ matrix.name }}'
          format: 'golang'
          parallel: true

      - name: Zip log files on failure
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        timeout-minutes: 5 # timeout after 5 minute
        run: 7z a logs-itest-${{ matrix.name }}.zip itest/**/*.log

      - name: Upload log files on failure
        uses: actions/upload-artifact@v4
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        with:
          name: logs-itest-${{ matrix.name }}
          path: logs-itest-${{ matrix.name }}.zip
          retention-days: 5


  ########################
  # run windows integration test
  ########################
  windows-integration-test:
    name: Run windows itest
    runs-on: windows-latest
    steps:
      - name: Git checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Check for no-itest label
        id: check-label
        uses: ./.github/actions/check-label
        with:
          label: 'no-itest'
          skip-message: "Tests auto-passed due to 'no-itest' label"

      - name: Fetch and rebase on ${{ github.base_ref }}
        if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/rebase

      - name: Setup go ${{ env.GO_VERSION }}
        if: steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          key-prefix: integration-test

      - name: Run itest
        if: steps.check-label.outputs.skip != 'true'
        run: make itest-parallel tranches=${{ env.SMALL_TRANCHES }} windows=1 shuffleseed=${{ github.run_id }}
        
      - name: Kill any remaining lnd processes
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        shell: powershell
        run: taskkill /IM lnd-itest.exe /T /F

      - name: Zip log files on failure
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        timeout-minutes: 5 # timeout after 5 minute
        run: 7z a logs-itest-windows.zip itest/**/*.log

      - name: Upload log files on failure
        uses: actions/upload-artifact@v4
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        with:
          name: logs-itest-windows
          path: logs-itest-windows.zip
          retention-days: 5

  ########################
  # run macOS integration test
  ########################
  macos-integration-test:
    name: Run macOS itest
    runs-on: macos-14
    steps:
      - name: Git checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Check for no-itest label
        id: check-label
        uses: ./.github/actions/check-label
        with:
          label: 'no-itest'
          skip-message: "Tests auto-passed due to 'no-itest' label"

      - name: Fetch and rebase on ${{ github.base_ref }}
        if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/rebase

      - name: Setup go ${{ env.GO_VERSION }}
        if: steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/setup-go
        with:
          go-version: '${{ env.GO_VERSION }}'
          key-prefix: integration-test

      - name: Run itest
        if: steps.check-label.outputs.skip != 'true'
        run: make itest-parallel tranches=${{ env.SMALL_TRANCHES }} shuffleseed=${{ github.run_id }}

      - name: Zip log files on failure
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        timeout-minutes: 5 # timeout after 5 minute
        run: 7z a logs-itest-macos.zip itest/**/*.log

      - name: Upload log files on failure
        uses: actions/upload-artifact@v4
        if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
        with:
          name: logs-itest-macos
          path: logs-itest-macos.zip
          retention-days: 5

  ########################
  # check pinned dependencies
  ########################
  dep-pin:
    name: Check pinned dependencies
    runs-on: ubuntu-latest
    strategy:
      # Allow other tests in the matrix to continue if one fails.
      fail-fast: false
      matrix:
        pinned_dep:
          - google.golang.org/grpc v1.79.3
          - github.com/golang/protobuf v1.5.4

    steps:
      - name: Git checkout
        uses: actions/checkout@v5

      - name: Clean up runner space
        uses: ./.github/actions/cleanup-space

      - name: Ensure dependencies at correct version
        run: if ! grep -q "${{ matrix.pinned_dep }}" go.mod; then echo dependency ${{ matrix.pinned_dep }} should not be altered ; exit 1 ; fi

  ########################
  # check PR updates release notes
  ########################
  milestone-check:
    name: Check release notes updated
    runs-on: ubuntu-latest
    steps:
      - name: Git checkout
        uses: actions/checkout@v5

      - name: Check for no-changelog label
        id: check-label
        uses: ./.github/actions/check-label
        with:
          label: 'no-changelog'
          skip-message: "Changelog check auto-passed due to 'no-changelog' label"

      - name: Clean up runner space
        if: steps.check-label.outputs.skip != 'true'
        uses: ./.github/actions/cleanup-space

      - name: Release notes check
        if: steps.check-label.outputs.skip != 'true'
        run: scripts/check-release-notes.sh

  ########################
  # Backwards Compatibility Test
  ########################
  backwards-compatibility-test:
    name: Backwards compatibility test
    runs-on: ubuntu-latest
    steps:
      - name: Git checkout
        uses: actions/checkout@v5

      - name: 🐳 Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: 🛡️ Backwards compatibility test
        run: make backwards-compat-test

      - name: 📋 Upload node logs on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: bw-compat-logs
          path: scripts/bw-compatibility-test/logs/
          retention-days: 7

  #########################################
  # Auto Cache Cleanup on Pull Requests
  #########################################
  auto-cleanup-cache:
    name: Cache Cleanup
    runs-on: ubuntu-latest

    # This condition checks for pull requests from authors with write access.
    if: >-
      contains('OWNER, MEMBER, COLLABORATOR', github.event.pull_request.author_association)

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - name: Delete caches older than 12 hours
        continue-on-error: true
        env:
          # GITHUB_TOKEN is required for the gh CLI.
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "Finding caches not used in the last 12 hours..."

          # Get the current time and the cutoff time (12 hours ago) in Unix
          # timestamp format.
          cutoff_timestamp=$(date -d "12 hours ago" +%s)

          # Use gh and jq to parse caches. Delete any cache last accessed
          # before the cutoff time.
          gh cache list --json key,lastAccessedAt | jq -r '.[] |
          select(.lastAccessedAt != null) | "\(.lastAccessedAt) \(.key)"' |
          while read -r last_accessed_at key; do
            last_accessed_timestamp=$(date -d "$last_accessed_at" +%s)

            if (( last_accessed_timestamp < cutoff_timestamp )); then
              echo "Deleting old cache. Key: $key, Last Used: $last_accessed_at"
              gh cache delete "$key"
            fi
          done

  # Notify about the completion of all coverage collecting jobs.
  finish:
    name: Send coverage report
    if: ${{ !cancelled() }}
    needs: [unit-test, basic-integration-test]
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Send coverage
        uses: coverallsapp/github-action@v2
        continue-on-error: true
        with:
          parallel-finished: true
