From aabb8f98ed6705ee21927b420baf9ea3b78e92e9 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Fri, 12 Jun 2026 16:04:15 -0500 Subject: [PATCH 1/2] ci: publish fork-PR test reports via workflow_run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub forces GITHUB_TOKEN to read-only for pull_request runs from forks, so dorny/test-reporter's Checks API call (POST .../check-runs) returned 403 "Resource not accessible by integration" and failed every fork PR, even when all tests passed. Secrets are likewise absent on fork PRs, so the Azure OIDC login failed in the build-windows job. Split reporting out of the build: - ci.yml now runs with a read-only token and only uploads the .trx files as test-results- artifacts. Azure login and package signing are gated to refs/heads/main (publish-only, matching Push to MyGet). checks:write is dropped — the build no longer touches the Checks API. - test-report.yml is triggered by workflow_run when CI completes. It runs in the base-repository context with checks:write, downloads the test-results-* artifacts, and publishes one inline check per platform. This restores real test reports for fork PRs instead of swallowing them. Closes #4642 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 30 ++++++++++++-------------- .github/workflows/test-report.yml | 36 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/test-report.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2c1b8e47d..86a7268b1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,13 @@ env: DOTNET_NOLOGO: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true MINVERBUILDMETADATA: build.${{ github.run_id }}.${{ github.run_attempt}} +# This workflow runs untrusted code from fork PRs, so it keeps a read-only token +# and never needs secrets. 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. 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,17 +40,14 @@ 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 strategy: @@ -79,16 +79,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' From 8c86567a0e146316a0e5651550bde079b273e522 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Fri, 12 Jun 2026 16:30:54 -0500 Subject: [PATCH 2/2] ci: scope id-token:write to the build-windows job, clarify perms comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback on #4643: - The workflow-level header comment claimed it 'never needs secrets', which is inaccurate for the main-only signing/publishing steps. Reword to scope the no-secrets/read-only claim to PR (and fork) runs. - id-token:write was granted workflow-wide, so it was active on the Linux build job and any future job. OIDC is only used by the main-only Azure login that code-signs packages, which lives in build-windows. Move the grant to that job so the default is contents:read and the Linux job carries no OIDC. (Fork PRs can't mint OIDC tokens regardless — GitHub forces the token read-only — so the residual surface is trusted same-repo actors.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86a7268b1c..8c15e57cae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,12 +10,14 @@ env: DOTNET_NOLOGO: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true MINVERBUILDMETADATA: build.${{ github.run_id }}.${{ github.run_attempt}} -# This workflow runs untrusted code from fork PRs, so it keeps a read-only token -# and never needs secrets. 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. See issue #4642. +# 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 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -50,6 +52,12 @@ jobs: 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