name: Build and Test

on: [push, pull_request]

env:
  python_default: 3.12

jobs:
  build-dpdk:
    strategy:
      matrix:
        runner: [ubuntu-24.04]
    env:
      dependencies: gcc libnuma-dev libxdp-dev ninja-build pkgconf
      CC: gcc
      DPDK_GIT: https://dpdk.org/git/dpdk-stable
      DPDK_VER: 23.11.6
    name: dpdk gcc
    outputs:
      dpdk_key: ${{ steps.gen_dpdk_key.outputs.key }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30

    steps:
    - name: checkout
      uses: actions/checkout@v6

    - name: update PATH
      run:  |
        echo "$HOME/bin"        >> $GITHUB_PATH
        echo "$HOME/.local/bin" >> $GITHUB_PATH

    - name: create ci signature file for the dpdk cache key
      # This will collect most of DPDK related lines, so hash will be different
      # if something changed in a way we're building DPDK including DPDK_VER.
      # This also allows us to use cache from any branch as long as version
      # and a way we're building DPDK stays the same.
      run:  |
        echo ${{ matrix.runner }} > dpdk-ci-signature
        cat .ci/dpdk-* >> dpdk-ci-signature
        grep -rwE 'DPDK_GIT|DPDK_VER' .github/ >> dpdk-ci-signature
        if [ "${DPDK_VER##refs/*/}" != "${DPDK_VER}" ]; then
            git ls-remote --heads $DPDK_GIT $DPDK_VER >> dpdk-ci-signature
        fi
        cat dpdk-ci-signature

    - name: generate ci DPDK key
      id: gen_dpdk_key
      env:
        ci_key: ${{ hashFiles('dpdk-ci-signature') }}
      run:  echo 'key=dpdk-${{ env.ci_key }}' >> $GITHUB_OUTPUT

    - name: cache
      id: dpdk_cache
      uses: actions/cache@v5
      with:
        path: dpdk-dir
        key:  ${{ steps.gen_dpdk_key.outputs.key }}

    - name: set up python
      if: steps.dpdk_cache.outputs.cache-hit != 'true'
      uses: actions/setup-python@v6
      with:
        python-version: ${{ env.python_default }}

    - name: update APT cache
      if: steps.dpdk_cache.outputs.cache-hit != 'true'
      run:  sudo apt update || true
    - name: install common dependencies
      if: steps.dpdk_cache.outputs.cache-hit != 'true'
      run:  sudo apt install -y ${{ env.dependencies }}

    - name: prepare
      if: steps.dpdk_cache.outputs.cache-hit != 'true'
      run:  ./.ci/dpdk-prepare.sh

    - name: build
      if: steps.dpdk_cache.outputs.cache-hit != 'true'
      run:  ./.ci/dpdk-build.sh

  build-libreswan:
    strategy:
      matrix:
        runner: [ubuntu-24.04]
    env:
      dependencies: build-essential fakeroot devscripts equivs
      libreswan_ver: v5.1
    name: libreswan
    outputs:
      libreswan_key: ${{ steps.gen_libreswan_key.outputs.key }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30

    steps:
    - name: Checkout Libreswan
      uses: actions/checkout@v6
      with:
        repository: libreswan/libreswan
        path: libreswan
        ref: ${{ env.libreswan_ver }}

    - name: generate cache key
      id: gen_libreswan_key
      run: echo 'key=libreswan-${{ env.libreswan_ver }}-${{ matrix.runner }}'
                >> $GITHUB_OUTPUT

    - name: cache
      id: libreswan_cache
      uses: actions/cache@v5
      with:
        path: libreswan-deb
        key:  ${{ steps.gen_libreswan_key.outputs.key }}

    - name: update APT cache
      if: steps.libreswan_cache.outputs.cache-hit != 'true'
      run: sudo apt update || true

    - name: install common dependencies
      if: steps.libreswan_cache.outputs.cache-hit != 'true'
      run: sudo apt install -y ${{ env.dependencies }}

    - name: install build dependencies
      if: steps.libreswan_cache.outputs.cache-hit != 'true'
      run: mk-build-deps --install --root-cmd sudo
                         libreswan/packaging/debian/control

    - name: build
      if: steps.libreswan_cache.outputs.cache-hit != 'true'
      run: cd libreswan && make deb

    - name: move the package to cache
      if: steps.libreswan_cache.outputs.cache-hit != 'true'
      run: mkdir -p libreswan-deb && mv libreswan_*.deb ./libreswan-deb

  build-linux:
    needs: [build-dpdk, build-libreswan]
    env:
      dependencies: |
        automake libtool gcc bc libjemalloc2 libjemalloc-dev libssl-dev \
        llvm-dev libnuma-dev selinux-policy-dev libxdp-dev lftp
      CC:          ${{ matrix.compiler }}
      DPDK:        ${{ matrix.dpdk }}
      DPDK_SHARED: ${{ matrix.dpdk_shared }}
      LIBS:        ${{ matrix.libs }}
      M32:         ${{ matrix.m32 }}
      OPTS:        ${{ matrix.opts }}
      SANITIZERS:  ${{ matrix.sanitizers }}
      STD:         ${{ matrix.std }}
      TESTSUITE:   ${{ matrix.testsuite }}
      TEST_RANGE:  ${{ matrix.test_range }}

    name: linux ${{ join(matrix.*, ' ') }}
    runs-on: ubuntu-24.04
    timeout-minutes: 30

    strategy:
      fail-fast: false
      matrix:
        include:
          - compiler:     gcc
            opts:         --disable-ssl
          - compiler:     clang
            opts:         --disable-ssl

          - compiler:     gcc
            std:          c99
          - compiler:     clang
            std:          c99

          - compiler:     gcc
            testsuite:    test
          - compiler:     clang
            sanitizers:   address,undefined
            testsuite:    test

          - compiler:     gcc
            testsuite:    test
            opts:         --enable-shared
          - compiler:     clang
            testsuite:    test
            opts:         --enable-shared

          - compiler:     gcc
            testsuite:    check
            dpdk:         dpdk
          - compiler:     clang
            testsuite:    check
            dpdk:         dpdk

          - compiler:     gcc
            testsuite:    test
            libs:         -ljemalloc
          - compiler:     clang
            testsuite:    test
            libs:         -ljemalloc

          - compiler:     gcc
            opts:         --enable-afxdp
          - compiler:     clang
            opts:         --enable-afxdp

          - compiler:     gcc
            dpdk:         dpdk
            opts:         --enable-shared
          - compiler:     clang
            dpdk:         dpdk
            opts:         --enable-shared

          - compiler:     gcc
            dpdk_shared:  dpdk-shared
          - compiler:     clang
            dpdk_shared:  dpdk-shared

          - compiler:     gcc
            dpdk_shared:  dpdk-shared
            opts:         --enable-shared
          - compiler:     clang
            dpdk_shared:  dpdk-shared
            opts:         --enable-shared

          - compiler:     gcc
            m32:          m32
            opts:         --disable-ssl

          - compiler:     gcc
            testsuite:    check-ovsdb-cluster

          - compiler:     gcc
            testsuite:    check-kernel
            test_range:   "-100"
          - compiler:     gcc
            testsuite:    check-kernel
            test_range:   "100-"

          - compiler:     clang
            sanitizers:   address,undefined
            testsuite:    check-kernel
            test_range:   "-100"
          - compiler:     clang
            sanitizers:   address,undefined
            testsuite:    check-kernel
            test_range:   "100-"

          - compiler:     gcc
            testsuite:    check-offloads
            test_range:   "-100"
          - compiler:     gcc
            testsuite:    check-offloads
            test_range:   "100-"

          - compiler:     gcc
            dpdk:         dpdk
            testsuite:    check-system-userspace

          - compiler:     clang
            sanitizers:   address,undefined
            dpdk:         dpdk
            testsuite:    check-system-userspace

          - compiler:     gcc
            dpdk:         dpdk
            testsuite:    check-system-tso

    steps:
    - name: checkout
      uses: actions/checkout@v6

    - name: update PATH
      run:  |
        echo "$HOME/bin"        >> $GITHUB_PATH
        echo "$HOME/.local/bin" >> $GITHUB_PATH

    - name: set up python
      uses: actions/setup-python@v6
      with:
        python-version: ${{ env.python_default }}

    - name: fix /etc/hosts
      # On multiple occasions GitHub added things to /etc/hosts that are not
      # a correct syntax for this file causing test failures:
      #   https://github.com/actions/runner-images/issues/3353
      #   https://github.com/actions/runner-images/issues/12192
      # Just clearing those out, if any.
      run: |
        set -x
        cp /etc/hosts ./hosts.bak
        sed -E -n \
          '/^[[:space:]]*(#.*|[0-9a-fA-F:.]+([[:space:]]+[a-zA-Z0-9.-]+)+|)$/p' \
          ./hosts.bak | sudo tee /etc/hosts
        diff -u ./hosts.bak /etc/hosts || true

    - name: DPDK cache
      if:   matrix.dpdk != '' || matrix.dpdk_shared != ''
      uses: actions/cache@v5
      with:
        path: dpdk-dir
        key:  ${{ needs.build-dpdk.outputs.dpdk_key }}

    - name: Libreswan cache
      uses: actions/cache@v5
      with:
        path: libreswan-deb
        key:  ${{ needs.build-libreswan.outputs.libreswan_key }}

    - name: update APT cache
      run:  sudo apt update || true
    - name: install common dependencies
      run:  sudo apt install -y ${{ env.dependencies }}
    - name: install Libreswan
      run:  sudo apt install -y ./libreswan-deb/libreswan_*.deb
    - name: install libunbound libunwind python3-unbound
      # GitHub Actions doesn't have 32-bit versions of these libraries.
      if:   matrix.m32 == ''
      run:  sudo apt install -y libunbound-dev libunwind-dev python3-unbound
    - name: install 32-bit libraries
      if:   matrix.m32 != ''
      run:  sudo apt install -y gcc-multilib

    - name: checkout sparse
      uses: actions/checkout@v6
      # Official mirror of the git.kernel.org/pub/scm/devel/sparse/sparse.git.
      with:
        repository: lucvoo/sparse
        path: sparse

    - name: prepare
      run:  ./.ci/linux-prepare.sh

    - name: build
      run:  ./.ci/linux-build.sh

    - name: copy logs on failure
      if: failure() || cancelled()
      run: |
        # upload-artifact throws exceptions if it tries to upload socket
        # files and we could have some socket files in testsuite.dir.
        # Also, upload-artifact doesn't work well enough with wildcards.
        # So, we're just archiving everything here to avoid any issues.
        mkdir logs
        cp config.log ./logs/
        cp -r ./*/_build/sub/tests/testsuite.* ./logs/ || true
        sudo cp -r ./tests/*testsuite.* ./logs/ || true
        sudo tar -czvf logs.tgz logs/

    - name: upload logs on failure
      if: failure() || cancelled()
      uses: actions/upload-artifact@v7
      with:
        name: logs-linux-${{ join(matrix.*, '-') }}
        path: logs.tgz

  build-clang-analyze-cache:
    needs: build-dpdk
    env:
      dependencies: |
        automake bc clang-tools libnuma-dev libunbound-dev libunwind-dev \
        libssl-dev libtool libxdp-dev llvm-dev
      CC:   clang
      DPDK: dpdk
      CLANG_ANALYZE: true
    name: clang-analyze-cache
    outputs:
      key: ${{ steps.cache_key.outputs.key }}
      outcome: ${{ steps.build_base.outcome }}
    runs-on: ubuntu-24.04
    timeout-minutes: 30

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

    - name: get base branch sha
      id: base_branch
      env:
        BASE_SHA: ${{ github.event.pull_request.base.sha }}
        EVENT_BEFORE: ${{ github.event.before }}
        FORCED_PUSH: ${{ github.event.forced }}
      run: |
        if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
          echo "sha=$BASE_SHA" >> $GITHUB_OUTPUT
        else
          if [ "$EVENT_BEFORE" = "0000000000000000000000000000000000000000" ] \
             || [ "$FORCED_PUSH" = true ]; then
            BASE_SHA=HEAD~1
            MIN_DISTANCE=1000
            git remote add upstream https://github.com/openvswitch/ovs.git
            git fetch upstream
            for upstream_head in $(git ls-remote --heads upstream main master dpdk-latest branch-2.17 branch-[3456789]* | cut -f 1); do
              CURR_BASE=$(git merge-base ${upstream_head} HEAD 2>/dev/null)
              if [ ${CURR_BASE} ]; then
                DISTANCE=$(git log --oneline ${CURR_BASE}..HEAD | wc -l);
                if test ${MIN_DISTANCE} -gt ${DISTANCE}; then
                  BASE_SHA=${CURR_BASE}
                  MIN_DISTANCE=${DISTANCE}
                fi
              fi
            done
            echo "sha=$BASE_SHA" >> $GITHUB_OUTPUT
          else
            echo "sha=$EVENT_BEFORE" >> $GITHUB_OUTPUT
          fi
        fi

    - name: checkout base branch
      env:
        BASE_SHA: ${{ steps.base_branch.outputs.sha }}
      run: |
        cp -r $(pwd)/. /tmp/base_ovs_main && mv /tmp/base_ovs_main ./
        cd $(pwd)/base_ovs_main
        git checkout ${BASE_SHA}

    - name: update PATH
      run: |
        echo "$HOME/bin"        >> $GITHUB_PATH
        echo "$HOME/.local/bin" >> $GITHUB_PATH

    - name: generate cache key
      id: cache_key
      run: |
        ver=$(${CC} -v 2>&1 | grep ' version ' | \
              sed 's/.*version \([0-9]*\.[0-9]*\.[0-9]*\).*/\1/g')
        echo "key=${CC}-${ver}-analyze-$(git -C base_ovs_main rev-parse HEAD)" \
          >> $GITHUB_OUTPUT

    - name: check for analyzer result cache
      id: clang_cache
      uses: actions/cache@v5
      with:
        path: base-clang-analyzer-results
        key:  ${{ steps.cache_key.outputs.key }}

    - name: set up python
      if: steps.clang_cache.outputs.cache-hit != 'true'
      uses: actions/setup-python@v6
      with:
        python-version: ${{ env.python_default }}

    - name: get cached dpdk-dir
      if: steps.clang_cache.outputs.cache-hit != 'true'
      uses: actions/cache/restore@v5
      with:
        path: dpdk-dir
        key:  ${{ needs.build-dpdk.outputs.dpdk_key }}

    - name: update APT cache
      if: steps.clang_cache.outputs.cache-hit != 'true'
      run:  sudo apt update || true

    - name: install common dependencies
      if: steps.clang_cache.outputs.cache-hit != 'true'
      run:  sudo apt install -y ${{ env.dependencies }}

    - name: prepare
      if: steps.clang_cache.outputs.cache-hit != 'true'
      run:  ./.ci/linux-prepare.sh

    - name: build base reference
      id: build_base
      if: steps.clang_cache.outputs.cache-hit != 'true'
      continue-on-error: true
      run:  ./.ci/linux-build.sh

  build-clang-analyze:
    needs: [build-dpdk, build-clang-analyze-cache]
    if: >
        needs.build-clang-analyze-cache.outputs.outcome == 'success' ||
        needs.build-clang-analyze-cache.outputs.outcome == 'skipped'
    env:
      dependencies: |
        automake bc clang-tools libnuma-dev libunbound-dev libunwind-dev \
        libssl-dev libtool libxdp-dev llvm-dev
      CC:   clang
      DPDK: dpdk
      CLANG_ANALYZE: true
    name: clang-analyze
    runs-on: ubuntu-24.04
    timeout-minutes: 30

    steps:
    - name: checkout
      uses: actions/checkout@v6

    - name: update PATH
      run: |
        echo "$HOME/bin"        >> $GITHUB_PATH
        echo "$HOME/.local/bin" >> $GITHUB_PATH

    - name: check for analyzer result cache
      uses: actions/cache/restore@v5
      with:
        path: base-clang-analyzer-results
        key:  ${{ needs.build-clang-analyze-cache.outputs.key }}

    - name: set up python
      uses: actions/setup-python@v6
      with:
        python-version: ${{ env.python_default }}

    - name: get cached dpdk-dir
      uses: actions/cache/restore@v5
      with:
        path: dpdk-dir
        key:  ${{ needs.build-dpdk.outputs.dpdk_key }}

    - name: update APT cache
      run:  sudo apt update || true

    - name: install common dependencies
      run:  sudo apt install -y ${{ env.dependencies }}

    - name: prepare
      run:  ./.ci/linux-prepare.sh

    - name: build
      run:  ./.ci/linux-build.sh

  build-osx:
    env:
      CC:    clang
      OPTS:  --disable-ssl

    name:    osx clang --disable-ssl
    runs-on: macos-latest
    timeout-minutes: 30

    strategy:
      fail-fast: false

    steps:
    - name: checkout
      uses: actions/checkout@v6
    - name: update PATH
      run:  |
        echo "$HOME/bin"        >> $GITHUB_PATH
        echo "$HOME/.local/bin" >> $GITHUB_PATH
    - name: set up python
      uses: actions/setup-python@v6
      with:
        python-version: ${{ env.python_default }}
    - name: install dependencies
      run:  brew install automake libtool
    - name: prepare
      run:  ./.ci/osx-prepare.sh
    - name: build
      run:  ./.ci/osx-build.sh
    - name: upload logs on failure
      if: failure()
      uses: actions/upload-artifact@v7
      with:
        name: logs-osx-clang---disable-ssl
        path: config.log

  build-linux-deb:
    env:
      deb_dependencies: |
        linux-headers-$(uname -r) build-essential fakeroot devscripts equivs
      DEB_PACKAGE: yes
      DPDK:        ${{ matrix.dpdk }}

    name: linux deb ${{ matrix.dpdk }} dpdk
    runs-on: ubuntu-24.04
    timeout-minutes: 30

    strategy:
      fail-fast: false
      matrix:
        include:
          - dpdk: no

    steps:
    - name: checkout
      uses: actions/checkout@v6

    - name: update PATH
      run:  |
        echo "$HOME/bin"        >> $GITHUB_PATH
        echo "$HOME/.local/bin" >> $GITHUB_PATH

    - name: update APT cache
      run:  sudo apt update || true
    - name: install dependencies for debian packages
      run:  sudo apt install -y ${{ env.deb_dependencies }}
    - name: install dpdk-dev
      if:   matrix.dpdk != 'no'
      run:  sudo apt install -y libdpdk-dev

    - name: prepare
      run:  ./.ci/linux-prepare.sh

    - name: build
      run:  ./.ci/linux-build.sh

    - name: upload deb packages
      uses: actions/upload-artifact@v7
      with:
        name: deb-packages-${{ matrix.dpdk }}-dpdk
        path: '/home/runner/work/ovs/*.deb'

  build-linux-rpm:
    name: linux rpm fedora
    runs-on: ubuntu-latest
    container: fedora:43
    timeout-minutes: 30

    strategy:
      fail-fast: false

    steps:
    - name: checkout
      uses: actions/checkout@v6
    - name: install dependencies
      run: |
        dnf install -y rpm-build dnf-plugins-core
        sed -e 's/@VERSION@/0.0.1/' rhel/openvswitch-fedora.spec.in \
            > /tmp/ovs.spec
        dnf builddep --exclude='*.i686' -y /tmp/ovs.spec
        rm -f /tmp/ovs.spec

    - name: configure
      run:  ./boot.sh && ./configure

    - name: build
      run:  make rpm-fedora

    - name: install
      run:  dnf install -y rpm/rpmbuild/RPMS/*/*.rpm

    - name: upload rpm packages
      uses: actions/upload-artifact@v7
      with:
        name: rpm-packages
        path: |
          rpm/rpmbuild/SRPMS/*.rpm
          rpm/rpmbuild/RPMS/*/*.rpm
