name: CI

on:  # We are very liberal in terms of triggering builds. This should be revisited if we start seeing a lot of queueing
  - push
  - pull_request

env:
  java_default_distribution: corretto
  java_default_version: 11

jobs:
  build-jars:
    runs-on: ubuntu-latest
    name: Build JARs
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4
      - name: Build JARs
        run: make jar
      - name: Upload JARs
        uses: actions/upload-artifact@v4
        with:
          name: async-profiler-jars
          path: build/jar/*
          if-no-files-found: error
  build-and-upload-binaries:
    strategy:
      matrix:
        include:
          - runson:
              display: linux-arm64
              name: ubuntu-24.04-arm
            image: "public.ecr.aws/async-profiler/asprof-builder-arm:latest"
          - runson:
              display: linux-x64
              name: ubuntu-latest
            image: public.ecr.aws/async-profiler/asprof-builder-x86:latest
          - runson:
              display: macos
              name: macos-15
    runs-on: ${{ matrix.runson.name }}
    container:
      image: ${{ matrix.image }}
      volumes: ${{ fromJSON(matrix.volumes || '[]') }}
    name: "Build and unit test (${{ matrix.runson.display }})"
    steps:
      - name: Run container setup
        run: "[ ! -f /root/setup.sh ] || /root/setup.sh"
      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: ${{ matrix.java-distribution || env.java_default_distribution }}
          java-version: ${{ matrix.java-version || env.java_default_version }}
      - name: Checkout sources
        uses: actions/checkout@v4
      - name: Build and unit test
        id: build
        run: |
          set -x
          HASH=${GITHUB_SHA:0:7}
          case "${{ matrix.runson.display }}" in
            macos*)
              brew install gcovr
              make COMMIT_TAG=$HASH FAT_BINARY=true release coverage -j
            ;;
            *)
              make COMMIT_TAG=$HASH CC=/usr/local/musl/bin/musl-gcc release coverage -j
              echo "debug_archive=$(find . -type f -name "async-profiler-*-debug*" -exec basename {} \;)" >> $GITHUB_OUTPUT
            ;;
          esac
          echo "archive=$(find . -type f -name "async-profiler-*" -not -name "*-debug*" -exec basename {} \;)" >> $GITHUB_OUTPUT
        shell: bash
        env:
          GITHUB_SHA: ${{ github.sha }}
      - name: Set artifact name
        id: set_artifact_name
        run: echo "artifact_name=async-profiler-${{ matrix.runson.display }}-${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
        shell: bash
        env:
          GITHUB_SHA: ${{ github.sha }}
      - name: Upload binaries
        uses: actions/upload-artifact@v4
        with:
          name: ${{ steps.set_artifact_name.outputs.artifact_name }}
          path: ${{ steps.build.outputs.archive }}
          if-no-files-found: error
      - name: Upload debug info
        uses: actions/upload-artifact@v4
        if: matrix.runson.display != 'macos'
        with:
          name: ${{ steps.set_artifact_name.outputs.artifact_name }}-debug
          path: ${{ steps.build.outputs.debug_archive }}
          if-no-files-found: error
      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: test-coverage-${{ matrix.runson.display }}
          path: build/test/coverage/
          if-no-files-found: error
  integration-tests:
    needs: build-and-upload-binaries
    strategy:
      matrix:
        runson:
          - display: linux-x64
            # Using "latest" here as the build and test will any ways run inside a container which we control
            name: ubuntu-latest
        java-version: [11, 17, 21, 24]
        java-distribution: [corretto]
        image: [public.ecr.aws/async-profiler/asprof-builder-x86:latest]
        include:
          - runson:
              display: macos-arm64
              name: macos-15
            java-version: 11
            java-distribution: corretto
            # Not using container for mac-os as we have images only for linux
            image: ""
            # ARM MacOS should take fat binaries built on ARM
            asprof-binaries-job: macos
          - runson:
              display: macos-x64
              name: macos-13
            java-version: 11
            java-distribution: corretto
            architecture: x64
            image: ""
            # x64 MacOS should take fat binaries built on ARM
            asprof-binaries-job: macos
          - runson:
              display: linux-arm64
              # There is no "latest" tag available (yet) as ARM runners are still in public preview
              name: ubuntu-24.04-arm
            java-version: 11
            java-distribution: corretto
            image: public.ecr.aws/async-profiler/asprof-builder-arm:latest
          - runson:
              display: alpine
              name: ubuntu-latest
            java-version: 11
            java-distribution: corretto
            asprof-binaries-job: linux-x64
            image: public.ecr.aws/async-profiler/asprof-builder-alpine:corretto-11
          - runson:
              display: amazonlinux2
              name: ubuntu-latest
            java-version: 11
            java-distribution: corretto
            image: public.ecr.aws/async-profiler/asprof-builder-amazonlinux:2
            # GHA provides Node.js by attaching a volume to the container. The container path is
            # '/__e/node20', and it's not writable unless we override it via 'container.volumes'.
            volumes: '["/tmp/node20:/__e/node20"]'
            asprof-binaries-job: linux-x64
          - runson:
              display: amazonlinux2023
              name: ubuntu-latest
            java-version: 11
            java-distribution: corretto
            image: public.ecr.aws/async-profiler/asprof-builder-amazonlinux:2023
            asprof-binaries-job: linux-x64
    runs-on: ${{ matrix.runson.name }}
    container:
      image: ${{ matrix.image }}
      options: --privileged
      volumes: ${{ fromJSON(matrix.volumes || '[]') }}
    name: "Integration test (${{ matrix.runson.display }}, ${{ matrix.java-distribution }} ${{ matrix.java-version }})"
    steps:
      - name: Run container setup
        run: "[ ! -f /root/setup.sh ] || /root/setup.sh"
      - name: Setup Java
        uses: actions/setup-java@v4
        # https://github.com/actions/setup-java/issues/678#issuecomment-2446279753
        if: matrix.runson.display != 'alpine'
        with:
          distribution: ${{ matrix.java-distribution }}
          java-version: ${{ matrix.java-version }}
          architecture: ${{ matrix.architecture }}
      - name: Checkout sources
        uses: actions/checkout@v4
      - name: Set variables
        id: set_variables
        run: |
          echo "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
          echo "artifact_name=async-profiler-${{ matrix.asprof-binaries-job || matrix.runson.display }}-${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
        shell: bash
        env:
          GITHUB_SHA: ${{ github.sha }}
      - name: Download async-profiler release artifact
        uses: actions/download-artifact@v4
        with:
          name: ${{ steps.set_variables.outputs.artifact_name }}
          path: async_profiler_release
      - name: Download async-profiler JAR artifacts
        uses: actions/download-artifact@v4
        with:
          name: async-profiler-jars
          path: jar_artifacts
      - name: Extract async-profiler artifact
        id: extract_artifact
        run: |
          release_archive=$(basename $(find async_profiler_release -type f -iname "async-profiler-*" ))
          case "${{ matrix.runson.name }}" in
            macos*)
              unzip async_profiler_release/$release_archive
            ;;
            *)
              tar xvf async_profiler_release/$release_archive
            ;;
          esac
          echo "jars_directory=jar_artifacts" >> $GITHUB_OUTPUT
          echo "release_directory=$(basename $(find . -type d -iname "async-profiler-*" ))" >> $GITHUB_OUTPUT
      - name: Run integration tests
        run: |
          mkdir -p build/jar
          cp ${{ steps.extract_artifact.outputs.jars_directory }}/* build/jar
          make build/test.jar
          cp -r ${{ steps.extract_artifact.outputs.release_directory }}/bin build
          cp -r ${{ steps.extract_artifact.outputs.release_directory }}/lib build
          make test-java -j
      - name: Upload integration test logs
        uses: actions/upload-artifact@v4
        # Always upload, especially after test failure
        if: always()
        with:
          name: integration-test-logs-${{ matrix.runson.display }}-${{ matrix.java-version }}-${{ steps.set_variables.outputs.short_sha }}
          path: |
            build/test/logs/
            hs_err*.log
  publish-only-on-push:
    if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    name: publish (nightly)
    runs-on: ubuntu-latest
    needs: [build-jars, integration-tests]
    steps:
      - name: Download async-profiler binaries and jars
        uses: actions/download-artifact@v4
        with:
          pattern: 'async-profiler-*'
          merge-multiple: 'true'
      - name: Delete previous release and publish new release
        uses: actions/github-script@v7
        with:
          result-encoding: string
          script: |
            const fs = require('fs').promises;
            const commonOptions = {
              owner: "async-profiler",
              repo: "async-profiler",
            };
            let previousRelease = undefined;
            try {
              previousRelease = await github.rest.repos.getReleaseByTag({
                ...commonOptions,
                tag: "nightly",
              });
            } catch (e) {
              console.log("No previous nightly release");
              // ignore, there was no previous nightly release
            }
            if (previousRelease !== undefined) {
              // delete previous release and nightly tag
              await github.rest.repos.deleteRelease({
                ...commonOptions,
                release_id: previousRelease.data.id,
              });
              await github.rest.git.deleteRef({...commonOptions, ref: "tags/nightly"});
            }
            // create draft release
            const newReleaseId = (await github.rest.repos.createRelease({
              ...commonOptions,
              tag_name: "nightly",
              target_commitish: "${{ github.sha }}",
              name: "Nightly builds",
              body: "Async-profiler binaries published automatically from the latest sources in `master` upon a successful build.",
              prerelease: true,
              draft: true,
            })).data.id;
            // upload binaries and jars to draft release
            for (const archiveName of await fs.readdir(process.cwd())) {
              await github.rest.repos.uploadReleaseAsset({
                ...commonOptions,
                release_id: newReleaseId,
                name: archiveName,
                data: await fs.readFile(archiveName),
              });
            }
            // publish release
            await github.rest.repos.updateRelease({
              ...commonOptions,
              release_id: newReleaseId,
              draft: false,
            });
