name: PR Check

on: [pull_request]

# When a new revision is pushed to a PR, cancel all in-progress CI runs for that
# PR. See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  # Check what types of changes this PR contains
  check-changes:
    name: Check what files changed
    runs-on: ubuntu-24.04
    outputs:
      go: ${{ steps.changes.outputs.go }}
      wasm: ${{ steps.changes.outputs.wasm }}
      docs: ${{ steps.changes.outputs.docs }}
    steps:
    - name: Check for file changes
      id: changes
      run: |
        set -e

        # Default to running all checks
        echo "go=true" >> $GITHUB_OUTPUT
        echo "wasm=true" >> $GITHUB_OUTPUT
        echo "docs=true" >> $GITHUB_OUTPUT

        if ! curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
          "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
          | jq -r '.[].filename' > changed_files.txt; then
          echo "Error: Failed to fetch changed files from GitHub API"
          echo "Defaulting to running all checks (go=true, wasm=true, docs=true)"
          exit 0
        fi

        if [ ! -s changed_files.txt ]; then
          echo "Warning: No changed files found"
          echo "Defaulting to running all checks (go=true, wasm=true, docs=true)"
          exit 0
        fi

        echo "Changed files:"
        cat changed_files.txt

        # Check for Go-related changes
        go_patterns="^(.*\.go$|\
        .*\.yaml$|\
        .*\.yml$|\
        .*\.json$|\
        .*\.mod$|\
        .*\.sum$|\
        .*\.sh$|\
        ^Makefile$|\
        ^cmd/|\
        ^internal/|\
        ^v1/)"
        if ! grep -E "$go_patterns" changed_files.txt > /dev/null 2>&1; then
          echo "go=false" >> $GITHUB_OUTPUT
          echo "No Go files changed, skipping Go checks"
        else
          echo "Found Go file changes"
        fi

        # Check for WASM-related changes
        wasm_patterns="^(Makefile|\
        wasm/|\
        ast/|\
        internal/compiler/|\
        internal/planner/|\
        internal/wasm/|\
        test/wasm/|\
        test/cases/|\
        v1/ast/|\
        v1/test/cases/|\
        v1/test/wasm/|\
        v1/ir/)"
        if ! grep -E "$wasm_patterns" changed_files.txt > /dev/null 2>&1; then
          echo "wasm=false" >> $GITHUB_OUTPUT
          echo "No WASM-related changes detected, skipping WASM checks"
        else
          echo "Found WASM-related changes"
        fi

        # Check for docs changes (docs/, builtin_metadata.json, capabilities/*)
        docs_patterns="^(docs/|builtin_metadata\.json|capabilities/)"
        if ! grep -E "$docs_patterns" changed_files.txt > /dev/null 2>&1; then
          echo "docs=false" >> $GITHUB_OUTPUT
          echo "No docs-related changes detected, skipping docs checks"
        else
          echo "Found docs-related changes"
        fi

        echo "Final outputs:"
        echo "go=$(grep '^go=' $GITHUB_OUTPUT | tail -1 | cut -d'=' -f2)"
        echo "wasm=$(grep '^wasm=' $GITHUB_OUTPUT | tail -1 | cut -d'=' -f2)"
        echo "docs=$(grep '^docs=' $GITHUB_OUTPUT | tail -1 | cut -d'=' -f2)"

  # All jobs essentially re-create the `ci-release-test` make target, but are split
  # up for parallel runners for faster PR feedback and a nicer UX.
  generate:
    name: Generate Code
    runs-on: ubuntu-24.04
    needs: check-changes
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Generate
      run: make clean generate

    - name: Upload generated artifacts
      uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
      with:
        name: generated
        path: |
          internal/compiler/wasm/opa
          capabilities.json

  go-build:
    name: Go Build (${{ matrix.os }}${{ matrix.arch && format(' {0}', matrix.arch) || '' }}${{ matrix.go_tags }})
    runs-on: ${{ matrix.run }}
    needs: [generate, check-changes]
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    strategy:
      fail-fast: false
      matrix:
        include:
        - os: linux
          run: ubuntu-24.04
          targets: ci-go-ci-build-linux ci-go-ci-build-linux-static
          arch: amd64
        - os: linux
          run: ubuntu-24.04
          targets: ci-go-ci-build-linux-static
          arch: arm64
        - os: linux
          run: ubuntu-24.04
          targets: ci-go-ci-build-linux-static
          go_tags: GO_TAGS="-tags=opa_no_oci"
          variant_name: opa_no_ci
          arch: arm64
        - os: windows
          run: ubuntu-24.04
          targets: ci-go-ci-build-windows
          arch: amd64
        - os: darwin
          run: macos-13
          targets: ci-build-darwin
          arch: amd64
        - os: darwin
          run: macos-14
          targets: ci-build-darwin-arm64-static
          arch: arm64
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - id: go_version
      name: Read go version
      run: echo "go_version=$(cat .go-version)" >> $GITHUB_OUTPUT

    - name: Install Go (${{ steps.go_version.outputs.go_version }})
      uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
      with:
        go-version: ${{ steps.go_version.outputs.go_version }}
      if: matrix.os == 'darwin'

    - name: Download generated artifacts
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        name: generated

    - name: Build
      run: make ${{ matrix.go_tags }} ${{ matrix.targets }}
      env:
        GOARCH: ${{ matrix.arch }}
      timeout-minutes: 30

    - name: Upload binaries - No Go tags
      uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
      if: ${{ matrix.go_tags == '' }}
      with:
        name: binaries-${{ matrix.os }}-${{ matrix.arch }}
        path: _release

    - name: Upload binaries - Go tag variants
      uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
      if: ${{ matrix.go_tags != '' && matrix.variant_name != '' }}
      with:
        name: binaries-variant-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.variant_name }}
        path: _release

  go-test:
    name: Go Test (${{ matrix.os }})
    runs-on: ${{ matrix.run }}
    needs: [generate, check-changes]
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    strategy:
      fail-fast: false
      matrix:
        include:
        - os: linux
          run: ubuntu-24.04
        - os: darwin
          run: macos-14
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - id: go_version
      name: Read go version
      run: echo "go_version=$(cat .go-version)" >> $GITHUB_OUTPUT

    - name: Install Go (${{ steps.go_version.outputs.go_version }})
      uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
      with:
        go-version: ${{ steps.go_version.outputs.go_version }}

    - name: Download generated artifacts
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        name: generated

    - name: Unit Test Golang
      run: make test-coverage
      timeout-minutes: 30

  go-lint:
    name: Go Lint
    runs-on: ubuntu-24.04
    needs: check-changes
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Golang Style and Lint Check
      run: make check
      timeout-minutes: 30

  yaml-lint:
    name: YAML Lint
    runs-on: ubuntu-24.04
    needs: check-changes
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: YAML Style and Lint Check
      run: make check-yaml-tests
      timeout-minutes: 30
      env:
        YAML_LINT_FORMAT: github

  wasm:
    name: WASM
    runs-on: ubuntu-24.04
    needs: [generate, check-changes]
    if: ${{ needs.check-changes.outputs.wasm == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Download generated artifacts
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        name: generated

    - name: Build and Test Wasm
      run: make ci-wasm
      timeout-minutes: 15

    - name: Build and Test Wasm SDK
      run: make ci-go-wasm-sdk-e2e-test
      timeout-minutes: 30
      env:
        DOCKER_RUNNING: 0

  check-generated:
    name: Check Generated
    runs-on: ubuntu-24.04
    needs: [generate, check-changes]
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Download generated artifacts
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        name: generated

    - name: Check Working Copy
      run: make ci-check-working-copy
      timeout-minutes: 15
      env:
        DOCKER_RUNNING: 0

  race-detector:
    name: Go Race Detector
    runs-on: ubuntu-24.04
    needs: [generate, check-changes]
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Download generated artifacts
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        name: generated

    - name: Test with Race Detector
      run: make ci-go-race-detector
      env:
        DOCKER_RUNNING: 0

  smoke-test-docker-images:
    name: docker image smoke test
    runs-on: ubuntu-24.04
    needs: [go-build, check-changes]
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Set up QEMU
      uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
      with:
        platforms: arm64

    - name: Download release binaries
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        pattern: binaries-*
        merge-multiple: true
        path: _release

    - name: Test amd64 images
      run: make ci-image-smoke-test

    - name: Test arm64 images
      run: make ci-image-smoke-test
      env:
        GOARCH: arm64

  # Note(philipc): We only run the amd64 targets.
  smoke-test-binaries:
    runs-on: ${{ matrix.run }}
    needs: [go-build, check-changes]
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    strategy:
      matrix:
        include:
        - os: linux
          run: ubuntu-24.04
          exec: opa_linux_amd64
          arch: amd64
        - os: linux
          run: ubuntu-24.04
          exec: opa_linux_amd64_static
          arch: amd64
          wasm: disabled
        - os: darwin
          run: macos-13
          exec: opa_darwin_amd64
          arch: amd64
        - os: darwin
          run: macos-14
          exec: opa_darwin_arm64_static
          arch: arm64
          wasm: disabled
        - os: windows
          run: windows-latest
          exec: opa_windows_amd64.exe
          arch: amd64

    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Install Go
      uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
      with:
        go-version: stable

    - name: Download release binaries
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        name: binaries-${{ matrix.os }}-${{ matrix.arch }}
        path: _release

    - name: Prep tests
      run: go install github.com/rogpeppe/go-internal/cmd/testscript@latest

    - name: CLI E2E tests
      run: |
        matches=($BINARY_PATH_GLOB) # expand glob
        export OPA="$(pwd)/${matches[0]}"
        chmod +x "$OPA"
        find . -type f -name '*.txtar' -path '*/script/*' -print0 \
          | xargs -0 -I{} testscript -e OPA {}
      shell: bash
      env:
        BINARY_PATH_GLOB: _release/*/${{ matrix.exec }}

    # TODO(sr): port these tests below to testscript
    - name: Test binaries (Rego)
      run: make ci-binary-smoke-test-rego BINARY=${{ matrix.exec }}
    - name: Test binaries (Wasm)
      run: make ci-binary-smoke-test-wasm BINARY=${{ matrix.exec }}
      if: matrix.wasm != 'disabled'

  go-version-build:
    name: Go compat build/test
    needs: [generate, check-changes]
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-24.04, macos-14]
        version: ["1.21"]
    steps:
    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
    - name: Download generated artifacts
      uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
      with:
        name: generated
    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
      with:
        go-version: ${{ matrix.version }}
    - run: make build
      env:
        DOCKER_RUNNING: 0
    - run: make go-test
      env:
        DOCKER_RUNNING: 0

  # Run PR metadata against Rego policies
  rego-check-pr:
    name: Rego PR checks
    runs-on: ubuntu-24.04
    needs: check-changes
    if: ${{ needs.check-changes.outputs.go == 'true' }}
    steps:
    - name: Checkout code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Download OPA
      uses: open-policy-agent/setup-opa@34a30e8a924d1b03ce2cf7abe97250bbb1f332b5 # v2.2.0
      with:
        version: edge

    - name: Test policies
      run: opa test --schema build/policy/schema --bundle build/policy

    - name: Ensure proper formatting
      run: opa fmt --list --fail build/policy

    - name: Run file policy checks on changed files
      run: |
        curl --silent --fail --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -o files.json \
          https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files

        opa eval --bundle build/policy --format values --input files.json  --fail-defined 'data.files.deny[message]'
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    - name: Download Regal
      uses: StyraInc/setup-regal@33a142b1189004e0f14bf42b15972c67eecce776 #v1.0.0
      with:
        version: latest

    - name: Run Regal lint
      # Current configuration ensures anything but build/policy is ignored. While this could point Regal only at that
      # directory, this will serve as a reminder when more Rego policies are added, as they should be linted by default.
      run: regal lint --format github .

  docs-build:
    name: Build Docs
    runs-on: ubuntu-24.04
    needs: check-changes
    if: ${{ needs.check-changes.outputs.docs == 'true' }}
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Build docs
      run: make docs-install docs-build

  # This job is required to complete before merging, and is set as a branch
  # protection rule:
  # https://github.com/open-policy-agent/opa/settings/branch_protection_rules
  pr-check-summary:
    name: PR Check Summary
    runs-on: ubuntu-24.04
    needs: [
      check-changes,
      generate,
      go-build,
      go-test,
      go-lint,
      yaml-lint,
      wasm,
      check-generated,
      race-detector,
      smoke-test-docker-images,
      smoke-test-binaries,
      go-version-build,
      rego-check-pr,
      docs-build,
    ]
    if: always()
    steps:
    - name: Check out code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
    - name: Download OPA
      uses: open-policy-agent/setup-opa@34a30e8a924d1b03ce2cf7abe97250bbb1f332b5 # v2.2.0
      with:
        version: edge
    - name: Check job results
      run: |
        # Create the input file with all job results
        echo '${{ toJSON(needs) }}' > input.json

        # Find failed or cancelled jobs using OPA
        opa eval -d .github/workflows/pull-request.yaml \
        --input=input.json \
        '{job|some _, job in data.jobs["pr-check-summary"].needs} & {job | input[job].result in {"failure", "cancelled"}}' \
        --format=raw > failed_jobs.json

        # Check for failures and display a nice message
        if [ "$(cat failed_jobs.json)" != "[]" ]; then
          echo "The following required jobs did not complete successfully:"
          jq -r '.[]' failed_jobs.json | sed 's/^/- /'
          exit 1
        fi

        echo "All jobs completed successfully or were skipped"
