diff --git a/.github/workflows/coverage-on-pr.yml b/.github/workflows/coverage-on-pr.yml new file mode 100644 index 000000000..03260cacc --- /dev/null +++ b/.github/workflows/coverage-on-pr.yml @@ -0,0 +1,60 @@ +name: Report coverage on PR +on: + workflow_run: + workflows: ['Pipeline: Test, Lint, Build'] + types: [completed] +jobs: + comment: + name: Comment coverage report + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + pull-requests: write + env: + COVERAGE_COMMENT: 'true' + steps: + # Only the config, from the base branch: this job holds a write token, so + # it must never check out the fork. + - name: Check out the octocov config + uses: actions/checkout@v7 + with: + sparse-checkout: .octocov.yml + sparse-checkout-cone-mode: false + persist-credentials: false + + # Into a subdirectory. A pull_request run executes the fork's own copy of + # pipeline.yml, so every file in here is attacker-controlled. + - uses: actions/download-artifact@v8 + with: + name: octocov-pr + path: untrusted + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Verify the artifact and take the coverage profile + id: pr + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + number=$(head -c 20 untrusted/pr_number | tr -d '[:space:]') + case "$number" in ''|*[!0-9]*) + echo "::error::artifact pr_number is not a number"; exit 1;; + esac + sha=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$number" --jq .head.sha) + if [ "$sha" != "$HEAD_SHA" ]; then + echo "::error::artifact claims PR #$number, but its head $sha is not $HEAD_SHA"; exit 1 + fi + cp untrusted/coverage.out coverage.out + echo "number=$number" >> "$GITHUB_OUTPUT" + + - uses: k1LoW/octocov-action@v1 + env: + # A workflow_run job looks like a push to the default branch. Point + # octocov back at the pull request and at the run that produced it. + GITHUB_PULL_REQUEST_NUMBER: ${{ steps.pr.outputs.number }} + OCTOCOV_GITHUB_REF: refs/pull/${{ steps.pr.outputs.number }}/merge + OCTOCOV_GITHUB_SHA: ${{ github.event.workflow_run.head_sha }} + OCTOCOV_GITHUB_RUN_ID: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 7625cf410..aa8e29e49 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -193,8 +193,9 @@ jobs: needs: [go, go-plugins] permissions: contents: read - pull-requests: write actions: write + env: + COVERAGE_COMMENT: 'false' steps: - uses: actions/checkout@v7 @@ -212,6 +213,20 @@ jobs: - uses: k1LoW/octocov-action@v1 + - name: Save the PR number for the comment workflow + if: github.event_name == 'pull_request' + run: echo "${{ github.event.pull_request.number }}" > pr_number + + - name: Upload the merged profile for the comment workflow + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: octocov-pr + path: | + coverage.out + pr_number + if-no-files-found: error + go-windows: name: Test Go code (Windows) runs-on: windows-2022 diff --git a/.octocov.yml b/.octocov.yml index 397b5b364..52d4ff9bf 100644 --- a/.octocov.yml +++ b/.octocov.yml @@ -8,6 +8,9 @@ coverage: paths: - coverage.out codeToTestRatio: + # Needs the pull request's own source, which the comment workflow must not + # check out: it holds a write token. + if: env.COVERAGE_COMMENT != 'true' code: - '**/*.go' - '!**/*_test.go' @@ -23,7 +26,9 @@ diff: datastores: - artifact://${GITHUB_REPOSITORY} comment: - if: is_pull_request + # Only the 'Report coverage on PR' workflow sets this: a pull_request run from + # a fork gets a read-only token, so commenting from here 403s. + if: env.COVERAGE_COMMENT == 'true' updatePrevious: true summary: if: true