diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2c1b8e47d..8c15e57cae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,15 @@ env: DOTNET_NOLOGO: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true MINVERBUILDMETADATA: build.${{ github.run_id }}.${{ github.run_attempt}} +# On pull requests — especially from forks, where GitHub forces the token to +# read-only — this workflow needs no secrets and no write access: it only builds, +# tests, and uploads .trx artifacts. Test results are published by a separate +# workflow (test-report.yml, triggered via workflow_run) that runs in the +# base-repo context with checks:write. The only steps that touch secrets/OIDC are +# the main-only signing and publishing steps in build-windows, which is the sole +# job granted id-token: write (scoped at the job level below). See issue #4642. permissions: - id-token: write contents: read - checks: write concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -37,19 +42,22 @@ jobs: - name: Build and Test run: ./Build.ps1 shell: pwsh - - name: Report Test Results - uses: dorny/test-reporter@v1 - # Publishing the report needs checks:write, which GitHub forces to read-only - # for pull_request runs from forks. Don't let that fail the build; the test - # step itself already reflects pass/fail. - continue-on-error: true + - name: Upload Test Results + # Publishing happens in test-report.yml, which downloads this artifact. + uses: actions/upload-artifact@v4 if: success() || failure() with: - name: Test Results (Linux) + name: test-results-Linux path: artifacts/**/*.trx - reporter: dotnet-trx + if-no-files-found: ignore build-windows: needs: build + # id-token:write is required only for the main-only Azure OIDC login used to + # code-sign packages. Granting it here (not at the workflow level) keeps it + # off the Linux build job and the default for any future jobs. + permissions: + id-token: write + contents: read strategy: fail-fast: false runs-on: windows-latest @@ -79,16 +87,14 @@ jobs: - name: Build and Test run: ./Build.ps1 shell: pwsh - - name: Report Test Results - uses: dorny/test-reporter@v1 - # See note on the Linux job: report publishing is best-effort and must - # never fail the build (forked-PR tokens are read-only). - continue-on-error: true + - name: Upload Test Results + # Publishing happens in test-report.yml, which downloads this artifact. + uses: actions/upload-artifact@v4 if: success() || failure() with: - name: Test Results (Windows) + name: test-results-Windows path: artifacts/**/*.trx - reporter: dotnet-trx + if-no-files-found: ignore - name: Sign packages # Signing uses the Azure Key Vault login above; only runs on main. if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml new file mode 100644 index 0000000000..2b73b6a4be --- /dev/null +++ b/.github/workflows/test-report.yml @@ -0,0 +1,36 @@ +name: Test Report + +# Publishes test results for runs of the CI workflow, including pull requests +# from forks. The CI workflow itself runs with a read-only token (GitHub forces +# this for fork PRs) and only uploads the .trx files as artifacts. This workflow +# is triggered by workflow_run, so it runs in the *base repository* context with +# a writable token and can call the Checks API to publish inline reports. +# See issue #4642. + +on: + workflow_run: + workflows: [CI] + types: + - completed + +permissions: + contents: read + actions: read + checks: write + +jobs: + report: + runs-on: ubuntu-latest + steps: + - name: Publish Test Results + uses: dorny/test-reporter@v1 + with: + # Download every test-results-* artifact from the triggering CI run and + # publish one check per platform, e.g. "Test Results (Linux)". + artifact: /test-results-(.*)/ + name: Test Results ($1) + path: '**/*.trx' + reporter: dotnet-trx + # A build that fails before producing any .trx shouldn't make this + # report red; the CI run already reflects that failure. + fail-on-empty: 'false'