From 92dab022da675f2fede090662728b370e49af78a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 13 May 2026 16:38:26 +1200 Subject: [PATCH 1/5] ci: Auto-accept API verifier changes in PRs Adds a new `verify api` workflow that runs the ApiApprovalTests on macOS (covers .NET / netstandard / iOS / MacCatalyst / Android TFMs) and Windows (covers the net48 / .NET Framework TFM), then runs scripts/accept-verifier-changes.ps1 over the resulting *.received.* files and pushes the accepted snapshots back to the PR branch. When API changes are accepted, the PR is also labelled `public API`. For PRs from forks (where we can't push back), the workflow fails with a hint telling the contributor how to accept the changes locally. Closes #5157 Co-Authored-By: Claude --- .github/workflows/verify-api.yml | 139 +++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 .github/workflows/verify-api.yml diff --git a/.github/workflows/verify-api.yml b/.github/workflows/verify-api.yml new file mode 100644 index 0000000000..475677e16a --- /dev/null +++ b/.github/workflows/verify-api.yml @@ -0,0 +1,139 @@ +name: verify api +on: + pull_request: + paths: + - 'src/**' + - 'test/**/ApiApprovalTests*' + - 'test/Sentry.Testing/ApiExtensions.cs' + - '.github/workflows/verify-api.yml' + +permissions: + contents: write + pull-requests: write + +jobs: + run-api-tests: + name: Run API Approval Tests (${{ matrix.rid }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + # macOS covers all non-Windows TFMs (net9.0, net10.0, netstandard, iOS, MacCatalyst, Android) + - os: macos-15 + rid: macos + slnf: Sentry-CI-Build-macOS.slnf + # Windows is required to produce the .NET Framework (net48 / Net4_8) verified files + - os: windows-latest + rid: win-x64 + slnf: Sentry-CI-Build-Windows.slnf + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: recursive + + - name: Remove unused applications + uses: ./.github/actions/freediskspace + + - name: Setup Environment + uses: ./.github/actions/environment + + - name: Restore sentry-native cache + id: cache-native + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: src/Sentry/Platforms/Native/sentry-native + key: sentry-native-${{ matrix.rid }}-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} + enableCrossOsArchive: true + + - name: Build sentry-native (cache miss) + if: steps.cache-native.outputs.cache-hit != 'true' + shell: pwsh + run: scripts/build-sentry-native.ps1 + + - name: Build Native Dependencies + uses: ./.github/actions/buildnative + + - name: Restore .NET Dependencies + run: | + dotnet workload restore + dotnet restore ${{ matrix.slnf }} --nologo + + - name: Build + run: dotnet build ${{ matrix.slnf }} -c Release --no-restore --nologo -v:minimal + + # API approval tests fail when the public API surface changes. We swallow the failure + # here and rely on the produced *.received.txt files to detect and accept the change. + - name: Run API Approval Tests + continue-on-error: true + run: dotnet test ${{ matrix.slnf }} -c Release --no-build --nologo --filter "FullyQualifiedName~ApiApprovalTests" + + - name: Upload Received API Files + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: api-verify-received-${{ matrix.rid }} + path: "**/*.received.*" + if-no-files-found: ignore + + accept-api-changes: + name: Accept and Commit API Changes + needs: run-api-tests + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download Received API Files + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: api-verify-received-* + merge-multiple: true + + - name: Accept Verifier Changes + shell: pwsh + run: pwsh ./scripts/accept-verifier-changes.ps1 + + - name: Detect API Changes + id: detect + shell: bash + run: | + if [[ -z "$(git status --porcelain)" ]]; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "No API verifier changes detected." + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "API verifier changes detected:" + git status --short + fi + + # For fork PRs we can't push back to the contributor's branch, so we fail the check + # instead — prompting the contributor to accept the changes locally. + - name: Commit Accepted API Changes + if: steps.detect.outputs.has_changes == 'true' + shell: bash + run: | + if [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then + echo "::error::Public API changes detected. Please run the following locally and push the result:" + echo "::error:: dotnet test && pwsh ./scripts/accept-verifier-changes.ps1" + exit 1 + fi + git config --global user.name 'Sentry Github Bot' + git config --global user.email 'bot+github-bot@sentry.io' + git fetch + git checkout "${GITHUB_HEAD_REF}" + git add -A + git commit -m "Accept API verifier changes" + git push --set-upstream origin "${GITHUB_HEAD_REF}" + + - name: Label Public API PR + if: steps.detect.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name == github.repository + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh label create "public API" --color "0075ca" --description "Modifies the public API surface" --repo "${{ github.repository }}" 2>/dev/null || true + gh pr edit "${{ github.event.pull_request.number }}" --add-label "public API" --repo "${{ github.repository }}" From dc602728b1856732594013aa4cc8374989dbcb0f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 20 May 2026 12:55:49 +1200 Subject: [PATCH 2/5] Refactor: Checkout in branch to simplify the logic later --- .github/workflows/verify-api.yml | 50 ++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/.github/workflows/verify-api.yml b/.github/workflows/verify-api.yml index 475677e16a..0ccba3fe48 100644 --- a/.github/workflows/verify-api.yml +++ b/.github/workflows/verify-api.yml @@ -76,17 +76,20 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: api-verify-received-${{ matrix.rid }} - path: "**/*.received.*" + path: "**/*.received.txt" if-no-files-found: ignore accept-api-changes: name: Accept and Commit API Changes needs: run-api-tests runs-on: ubuntu-22.04 + if: github.event.pull_request.head.repo.full_name == github.repository steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.head.ref }} - name: Download Received API Files uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -111,29 +114,52 @@ jobs: git status --short fi - # For fork PRs we can't push back to the contributor's branch, so we fail the check - # instead — prompting the contributor to accept the changes locally. - name: Commit Accepted API Changes if: steps.detect.outputs.has_changes == 'true' shell: bash run: | - if [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then - echo "::error::Public API changes detected. Please run the following locally and push the result:" - echo "::error:: dotnet test && pwsh ./scripts/accept-verifier-changes.ps1" - exit 1 - fi git config --global user.name 'Sentry Github Bot' git config --global user.email 'bot+github-bot@sentry.io' - git fetch - git checkout "${GITHUB_HEAD_REF}" git add -A git commit -m "Accept API verifier changes" - git push --set-upstream origin "${GITHUB_HEAD_REF}" + git push - name: Label Public API PR - if: steps.detect.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name == github.repository + if: steps.detect.outputs.has_changes == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh label create "public API" --color "0075ca" --description "Modifies the public API surface" --repo "${{ github.repository }}" 2>/dev/null || true gh pr edit "${{ github.event.pull_request.number }}" --add-label "public API" --repo "${{ github.repository }}" + + # Fork PRs can't be auto-committed since the bot can't push to a contributor's repo. + # Fail the check with guidance so the contributor accepts the changes locally. + report-fork-api-changes: + name: Report API Changes (Fork PR) + needs: run-api-tests + runs-on: ubuntu-22.04 + if: github.event.pull_request.head.repo.full_name != github.repository + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download Received API Files + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: api-verify-received-* + merge-multiple: true + + - name: Accept Verifier Changes + shell: pwsh + run: pwsh ./scripts/accept-verifier-changes.ps1 + + - name: Fail If API Changes Detected + shell: bash + run: | + if [[ -n "$(git status --porcelain)" ]]; then + echo "::error::Public API changes detected. Please run the following locally and push the result:" + echo "::error:: dotnet test && pwsh ./scripts/accept-verifier-changes.ps1" + exit 1 + fi + echo "No API verifier changes detected." From b0117451eaba68e2177ce77e8abad1e49782e9c3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 20 May 2026 13:16:08 +1200 Subject: [PATCH 3/5] Update verify-api.yml --- .github/workflows/verify-api.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/verify-api.yml b/.github/workflows/verify-api.yml index 0ccba3fe48..3e07b523ed 100644 --- a/.github/workflows/verify-api.yml +++ b/.github/workflows/verify-api.yml @@ -91,7 +91,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} + # No artifact is uploaded when the matrix produced no received files (clean PR). + # Tolerate the resulting "no matching artifacts" failure here. - name: Download Received API Files + continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: api-verify-received-* @@ -145,6 +148,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download Received API Files + continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: api-verify-received-* From 698a813768e9a816e34ad4ca93b8bab31e8e494b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 20 May 2026 17:02:48 +1200 Subject: [PATCH 4/5] ci: Scope workflow permissions per-job The workflow-level `contents: write` token was inherited by `run-api-tests`, which builds and runs untrusted PR code. Scope tokens per-job instead: - run-api-tests: contents: read - accept-api-changes: contents: write, pull-requests: write - report-fork-api-changes: contents: read Co-Authored-By: Claude --- .github/workflows/verify-api.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify-api.yml b/.github/workflows/verify-api.yml index 3e07b523ed..0d4dc373e2 100644 --- a/.github/workflows/verify-api.yml +++ b/.github/workflows/verify-api.yml @@ -7,14 +7,13 @@ on: - 'test/Sentry.Testing/ApiExtensions.cs' - '.github/workflows/verify-api.yml' -permissions: - contents: write - pull-requests: write - jobs: run-api-tests: name: Run API Approval Tests (${{ matrix.rid }}) runs-on: ${{ matrix.os }} + # This job builds and runs untrusted PR code — keep the token read-only. + permissions: + contents: read strategy: fail-fast: false @@ -84,6 +83,9 @@ jobs: needs: run-api-tests runs-on: ubuntu-22.04 if: github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: write + pull-requests: write steps: - name: Checkout @@ -142,6 +144,8 @@ jobs: needs: run-api-tests runs-on: ubuntu-22.04 if: github.event.pull_request.head.repo.full_name != github.repository + permissions: + contents: read steps: - name: Checkout From ed7001e5d468e1fc5d8cb5e7699d8859805e644f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 20 May 2026 19:09:45 +1200 Subject: [PATCH 5/5] ci: Remove continue-on-error from artifact download steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pattern branch of actions/download-artifact (no `name`/`artifact-ids`, only `pattern`) tolerates zero matches without erroring — see https://github.com/actions/download-artifact/blob/main/src/download-artifact.ts. So `continue-on-error: true` isn't needed for the clean-PR case and would otherwise mask genuine download failures. Co-Authored-By: Claude --- .github/workflows/verify-api.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify-api.yml b/.github/workflows/verify-api.yml index 0d4dc373e2..57d3863883 100644 --- a/.github/workflows/verify-api.yml +++ b/.github/workflows/verify-api.yml @@ -93,10 +93,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - # No artifact is uploaded when the matrix produced no received files (clean PR). - # Tolerate the resulting "no matching artifacts" failure here. + # When the matrix produces no received files (clean PR), no artifact is uploaded. + # download-artifact's pattern branch tolerates zero matches without erroring, so + # we don't need `continue-on-error` here — that would mask genuine download failures. - name: Download Received API Files - continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: api-verify-received-* @@ -152,7 +152,6 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download Received API Files - continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: api-verify-received-*