---
name: CI
on:
  push:
    branches:
      - main
    tags:
      - v*
  pull_request:
jobs:
  dependabot:
    name: 🤖 Check dependabot status
    runs-on: ubuntu-latest
    permissions: {}
    if: |
      !startsWith(github.event.head_commit.message, 'build: update Nix dependency hashes') ||
       github.ref_type == 'tag'
    steps:
      - name: Fetch dependabot metadata
        if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }}
        id: metadata
        uses: dependabot/fetch-metadata@v2.4.0
    outputs:
      package-ecosystem: ${{ steps.metadata.outputs.package-ecosystem }}

  build-linux:
    name: 🐧 Test on Linux
    runs-on: ubuntu-latest
    needs:
      - build-go
      - build-js
    permissions:
      contents: read
      actions: write
    strategy:
      matrix:
        tests:
          - regular
          - coverage
          - misc
        include:
          - tests: misc
            binary: ${{ github.ref_type == 'tag' }}
    env:
      CI_AKVORADO_FUNCTIONAL_TESTS: "true"
      CI_AKVORADO_EBPF: "true"
    steps:
      # Setup
      - uses: actions/checkout@v6
        with:
          fetch-depth: ${{ matrix.binary && 0 || 1 }}
          persist-credentials: false
      - uses: docker/setup-buildx-action@v3
        if: matrix.tests != 'misc'
      - name: Docker Compose up
        if: matrix.tests != 'misc'
        run: docker compose -f docker/docker-compose-dev.yml up --wait --wait-timeout 60 --quiet-pull
      - name: Setup
        if: matrix.tests != 'misc'
        uses: ./.github/actions/setup
      - name: Install dependencies
        run: sudo apt-get install -qqy shared-mime-info curl zip

      # Regular tests
      - name: Go race tests
        if: matrix.tests == 'regular'
        run: sudo -E capsh --keep=1 --user=$USER --caps=cap_bpf+eip --addamb=cap_bpf -- -c "make test-race"
      - name: JS tests
        if: matrix.tests == 'regular'
        run: make test-js

      # Coverage tests
      - name: Coverage tests
        if: matrix.tests == 'coverage'
        run: sudo -E capsh --keep=1 --user=$USER --caps=cap_bpf+eip --addamb=cap_bpf -- -c "make test-coverage"
      - name: Upload coverage results
        if: matrix.tests == 'coverage'
        uses: actions/upload-artifact@v5
        with:
          name: linux-coverage
          if-no-files-found: error
          path: |
            test/go/profile.out
            test/js/cobertura-coverage.xml

      # Misc tests
      - name: Vector tests
        if: matrix.tests == 'misc'
        run: docker compose -f docker/docker-compose-dev.yml run --quiet --rm vector test

      # Binary build
      - name: Build
        if: matrix.binary
        run: |
          make TARGETARCH=amd64 && mv bin/akvorado bin/akvorado-amd64
          make TARGETARCH=arm64 && mv bin/akvorado bin/akvorado-arm64
      - name: Upload binary
        if: matrix.binary
        uses: actions/upload-artifact@v5
        with:
          name: binary
          path: bin/akvorado-*
          if-no-files-found: error

  build-nix:
    name: ❄️ Build on Nix
    runs-on: ubuntu-latest
    needs:
      - build-go
      - build-js
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: cachix/install-nix-action@v31
      - name: Update dependency hashes
        run: nix run .#update
      - name: Abort on release if not up-to-date
        if: github.ref_type == 'tag'
        run: git diff --stat --exit-code HEAD
      - name: Build
        run: nix build && ./result/bin/akvorado version

  build-macos:
    name: 🍏 Build and test on macOS
    runs-on: macos-14
    needs:
      - build-go
      - build-js
    permissions:
      contents: read
    steps:
      # Setup
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - name: Setup
        uses: ./.github/actions/setup

      # Build and test
      - name: Build
        run: make && ./bin/akvorado version
      - name: Tests
        run: make test-coverage-go
      - name: Upload coverage results
        uses: actions/upload-artifact@v5
        with:
          name: macos-coverage
          if-no-files-found: error
          path: |
            test/go/profile.out

  e2e:
    name: 🧪 End-to-end testing
    runs-on: ubuntu-latest
    needs:
      - build-go
      - build-js
    permissions:
      contents: read
    env:
      AKVORADO_COVERAGE_DIRECTORY: /tmp/akvorado-coverage
    steps:
      # Setup
      - uses: docker/setup-buildx-action@v3
      - uses: cachix/install-nix-action@v31
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - name: Setup
        uses: ./.github/actions/setup
      - name: Build Docker container
        run: make docker-dev-coverage
      - name: Docker Compose up
        timeout-minutes: 3
        run: |
          ./.github/e2e.sh compose-setup
          COMPOSE_PROFILES=demo,prometheus,loki docker compose up --wait --quiet-pull
      - name: Run tests
        timeout-minutes: 3
        run: |
          ./.github/e2e.sh tests
      - name: Docker Compose logs
        if: failure()
        run: |
          docker compose logs
      - name: Retrieve coverage files
        run: |
          docker compose stop
          ./.github/e2e.sh coverage
      - name: Upload coverage results
        uses: actions/upload-artifact@v5
        with:
          name: e2e-coverage
          if-no-files-found: error
          path: |
            ${{ env.AKVORADO_COVERAGE_DIRECTORY }}/e2e-coverage.out

  coverage:
    name: 🔍 Upload code coverage
    needs:
      - build-linux
      - build-macos
      - build-nix
      - e2e
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: actions/download-artifact@v6
        with:
          name: linux-coverage
          path: test/linux
      - uses: actions/download-artifact@v6
        with:
          name: macos-coverage
          path: test/macos
      - uses: actions/download-artifact@v6
        with:
          name: e2e-coverage
          path: test/e2e
      - name: Upload unittests coverage
        uses: codecov/codecov-action@v5
        with:
          token: &codecov-token bab8d6d9-e90c-4e37-b156-38a9a4c2108e  # not ideal, but limited risk
          files: ./test/linux/go/profile.out,./test/macos/profile.out,./test/linux/js/cobertura-coverage.xml
          flags: unittests
          disable_search: true
          fail_ci_if_error: true
      - name: Upload e2e coverage
        uses: codecov/codecov-action@v5
        with:
          token: *codecov-token
          files: ./test/e2e/e2e-coverage.out
          flags: e2e
          disable_search: true
          fail_ci_if_error: true

  build-go:
    name: 🔬 Test only Go
    runs-on: ubuntu-latest
    needs:
      - dependabot
    if: needs.dependabot.outputs.package-ecosystem != 'npm_and_yarn'
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - name: Setup
        id: setup
        uses: ./.github/actions/setup
      - name: Check go.mod was not modified
        run: |
          ! go mod edit -json | jq -r .Go | grep -vPx '1.\d+' || {
            echo "^^^^ Incorrect go directive in go.mod: use only \`minor.major'."
            exit 1
          }
      - name: Build
        run: make && ./bin/akvorado version
      - name: Save IANA files to cache
        if: steps.setup.outputs.iana-cache-hit != 'true'
        uses: actions/cache/save@v4
        with:
          path: |
            orchestrator/clickhouse/data/udp.csv
            orchestrator/clickhouse/data/tcp.csv
          key: ${{ steps.setup.outputs.iana-cache-key }}
      - name: Tests
        run: make test-go

  build-js:
    name: 🔬 Test only JS
    runs-on: ubuntu-latest
    needs:
      - dependabot
    permissions:
      contents: read
    strategy:
      matrix:
        node-version: [20, 22, 24]
        dependabot:
          - ${{ needs.dependabot.outputs.package-ecosystem }}
        exclude:
          - dependabot: go_modules
        include:
          - node-version: 22
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - name: Setup
        uses: ./.github/actions/setup
        with:
          setup-go: false
          node-version: ${{ matrix.node-version }}
      - name: Build and test JS frontend
        run: make console/data/frontend test-js

  licenses:
    name: ⚖️ Check licenses
    runs-on: ubuntu-latest
    permissions:
      contents: read
    needs:
      - build-go
      - build-js
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - name: Setup
        uses: ./.github/actions/setup
      - name: License check
        run: make licensecheck

  build-docker:
    name: 🐋 Build Docker images
    needs:
      - build-go
      - build-js
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' || github.ref_type == 'tag'
    permissions:
      contents: read
      packages: write
      attestations: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0        # We need the version
          persist-credentials: false
      - name: Get version
        id: version
        run: |
          echo version=$(make version) >> "$GITHUB_OUTPUT"
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3
      - uses: docker/metadata-action@v5
        id: meta
        env:
          DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
        with:
          images: |
            ghcr.io/${{ github.repository }}
          tags: |
            type=raw,value=unreleased
          labels: |
            org.opencontainers.image.title=Akvorado
            org.opencontainers.image.licenses=AGPL-3.0-only
          annotations: |
            org.opencontainers.image.title=Akvorado
            org.opencontainers.image.licenses=AGPL-3.0-only
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build image
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: docker/Dockerfile
          platforms: >-
            ${{
              github.ref_type == 'tag' &&
              'linux/amd64,linux/amd64/v3,linux/arm64,linux/arm/v7' ||
              'linux/amd64/v3,linux/arm64'
            }}
          push: true
          build-args: |
            VERSION=${{ steps.version.outputs.version }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          annotations: ${{ steps.meta.outputs.annotations }}
          provenance: mode=max
    outputs:
      package: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}

  tag-docker:
    name: 🐋 Tag Docker images
    needs:
      - build-docker
      - build-linux
      - build-macos
      - build-nix
      - e2e
      - licenses
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      attestations: write
    steps:
      - uses: docker/setup-buildx-action@v3
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: |
            ghcr.io/${{ github.repository }}
          tags: |
            type=schedule,pattern=main
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Tag image
        run: |
          echo "${{ steps.meta.outputs.tags }}" | xargs -n1 | while read tag; do
            docker buildx imagetools create ${{ needs.build-docker.outputs.package }} --tag ${tag}
          done

  release:
    name: 🚀 Publish release
    needs:
      - tag-docker
    runs-on: ubuntu-latest
    if: github.ref_type == 'tag'
    permissions:
      contents: write
      actions: read
    steps:
      # Changelog
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - name: Generate changelog
        run: make changelog.md

      # Get binary from build step
      - name: Download binary
        uses: actions/download-artifact@v6
        with:
          name: binary

      # Build tarball for docker compose
      - name: Build Docker Compose "quick start"
        run: |
          sed -i s,akvorado:main,akvorado:${GITHUB_REF_NAME#v}, docker/versions.yml
          tar zcvf docker-compose-quickstart.tar.gz \
            .env docker/* \
            config/*.yaml
      - name: Build Docker Compose "upgrade"
        run: |
          mv .env .env.dist
          mv docker/docker-compose-local.yml docker/docker-compose-local.yml.dist
          tar zcvf docker-compose-upgrade.tar.gz \
            .env.dist docker/*

      # Publish release
      - name: Publish release
        uses: softprops/action-gh-release@v2
        with:
          body_path: changelog.md
          draft: true
          prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
          fail_on_unmatched_files: true
          files: |
            akvorado-*
            docker-compose-quickstart.tar.gz
            docker-compose-upgrade.tar.gz
