From 51b95663786a7478b78ee98837d2e2d8742f2b4d Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 20 Apr 2026 17:45:16 +0200 Subject: [PATCH 1/3] ci(sdks): Warn against non-trivial changes to multiple SDKs Add a CI job which fails and posts a comment when non-trivial changes to multiple SDK docs pages are detected. We define non-trivial as affecting more than five lines. We do not intend to make this CI job required; rather, I hope PR authors and reviewers will heed the comments posted by this workflow. --- .../workflows/enforce-single-sdk-changes.yml | 101 ++++++++++++++++++ scripts/check-sdk-diff-scope.ts | 65 +++++++++++ 2 files changed, 166 insertions(+) create mode 100644 .github/workflows/enforce-single-sdk-changes.yml create mode 100644 scripts/check-sdk-diff-scope.ts diff --git a/.github/workflows/enforce-single-sdk-changes.yml b/.github/workflows/enforce-single-sdk-changes.yml new file mode 100644 index 0000000000000..5a69450f99a3c --- /dev/null +++ b/.github/workflows/enforce-single-sdk-changes.yml @@ -0,0 +1,101 @@ +name: Enforce Single SDK Changes + +on: + pull_request: + paths: + - 'docs/platforms/**' + - 'platform-includes/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + check-sdk-diff-scope: + name: Check SDK diff scope + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Set up bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + + - name: Compute diff and evaluate + id: evaluate + run: | + set +e + SDK_LIST=$(git diff --numstat origin/${{ github.base_ref }}...HEAD | bun scripts/check-sdk-diff-scope.ts) + EXIT=$? + if [ $EXIT -eq 0 ]; then + echo "violation=false" >> "$GITHUB_OUTPUT" + elif [ -n "$SDK_LIST" ]; then + echo "violation=true" >> "$GITHUB_OUTPUT" + echo "sdk_list=$SDK_LIST" >> "$GITHUB_OUTPUT" + else + echo "error=true" >> "$GITHUB_OUTPUT" + fi + + - name: Post, update, or delete PR comment + if: steps.evaluate.outputs.error != 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const MARKER = ''; + const violation = '${{ steps.evaluate.outputs.violation }}' === 'true'; + + const {data: comments} = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(MARKER)); + + if (violation) { + const sdkList = ${{ toJSON(steps.evaluate.outputs.sdk_list) }}; + const body = `${MARKER} + ### 🚫 Non-trivial changes to multiple SDKs + + This PR contains non-trivial changes to multiple SDKs: ${sdkList}. + Changes to multiple SDKs should be submitted as separate PRs: **one PR per SDK**. + + Please **split this PR** accordingly. Thank you in advance! 🙏`; + + if (existing && existing.body !== body) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else if (!existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + } else if (existing) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + }); + } + + - name: Fail on unexpected error + if: steps.evaluate.outputs.error == 'true' + run: | + echo "::error::SDK diff check failed unexpectedly — the script produced no output. Check the job logs for details." + exit 1 + + - name: Fail check + if: steps.evaluate.outputs.violation == 'true' + run: exit 1 diff --git a/scripts/check-sdk-diff-scope.ts b/scripts/check-sdk-diff-scope.ts new file mode 100644 index 0000000000000..08b153c2ef60a --- /dev/null +++ b/scripts/check-sdk-diff-scope.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env bun +import {readFileSync} from 'fs'; +/** + * Enforces the policy that a single PR should only contain non-trivial changes + * to one SDK at a time. PRs spanning multiple SDKs are harder to review and + * increase the risk of inconsistencies between platforms. + * + * Reads `git diff --numstat` from stdin (one `\t\t` line + * per changed file) and attributes each file to an SDK. If the total lines + * changed for at least MIN_VIOLATING_SDKS SDKs each exceed LINE_THRESHOLD, + * the script prints the violating SDK list and exits 1. + * + * Used in CI as: + * git diff --numstat origin/$BASE...HEAD | bun scripts/check-sdk-diff-scope.ts + */ + +/** SDKs with fewer total changed lines than this are ignored (e.g. a one-line typo fix that touches two SDKs should not block the PR). */ +const LINE_THRESHOLD = 5; +/** Minimum number of SDKs that must exceed LINE_THRESHOLD for the check to fail. */ +const MIN_VIOLATING_SDKS = 2; + +/** + * Extracts the SDK name from a changed file path, or returns null if the + * file is not attributed to any SDK. + * + * Two path patterns are recognised: + * - docs/platforms//... → sdk is the first path segment after platforms/ + * - platform-includes/.../..mdx → sdk is the first dot-segment of the filename + * e.g. `javascript.angular.mdx` → `javascript`, `react-native.mdx` → `react-native` + */ +function sdkForPath(path: string): string | null { + const segs = path.split("/"); + if (segs[0] === "docs" && segs[1] === "platforms") { + return segs[2] ?? null; + } + if (segs[0] === "platform-includes") { + return segs.at(-1)!.split(".")[0]; + } + return null; +} + +// Accumulate total lines changed (added + deleted) per SDK. +const sdkLines = new Map(); + +const input = readFileSync(0, 'utf-8'); +for (const line of input.split("\n")) { + const [added, deleted, path] = line.split("\t"); + if (!path || added === "-") continue; // binary file or empty line + const sdk = sdkForPath(path.trim()); + if (sdk) { + sdkLines.set(sdk, (sdkLines.get(sdk) ?? 0) + +added + +deleted); + } +} + +// Collect SDKs that exceed the line threshold, ignoring trivial touches. +const violating = [...sdkLines.entries()] + .filter(([, n]) => n >= LINE_THRESHOLD) + .map(([sdk]) => sdk) + .sort(); + +if (violating.length >= MIN_VIOLATING_SDKS) { + // Print the list for the CI step to capture and include in the PR comment. + console.log(violating.map(s => `\`${s}\``).join(", ")); + process.exit(1); +} From b5aa513ade02964a9135cfe855e0ac63c75210bd Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:14:03 +0000 Subject: [PATCH 2/3] [getsentry/action-github-commit] Auto commit --- scripts/check-sdk-diff-scope.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/check-sdk-diff-scope.ts b/scripts/check-sdk-diff-scope.ts index 08b153c2ef60a..7678bbaef6e5a 100644 --- a/scripts/check-sdk-diff-scope.ts +++ b/scripts/check-sdk-diff-scope.ts @@ -29,12 +29,12 @@ const MIN_VIOLATING_SDKS = 2; * e.g. `javascript.angular.mdx` → `javascript`, `react-native.mdx` → `react-native` */ function sdkForPath(path: string): string | null { - const segs = path.split("/"); - if (segs[0] === "docs" && segs[1] === "platforms") { + const segs = path.split('/'); + if (segs[0] === 'docs' && segs[1] === 'platforms') { return segs[2] ?? null; } - if (segs[0] === "platform-includes") { - return segs.at(-1)!.split(".")[0]; + if (segs[0] === 'platform-includes') { + return segs.at(-1)!.split('.')[0]; } return null; } @@ -43,9 +43,9 @@ function sdkForPath(path: string): string | null { const sdkLines = new Map(); const input = readFileSync(0, 'utf-8'); -for (const line of input.split("\n")) { - const [added, deleted, path] = line.split("\t"); - if (!path || added === "-") continue; // binary file or empty line +for (const line of input.split('\n')) { + const [added, deleted, path] = line.split('\t'); + if (!path || added === '-') continue; // binary file or empty line const sdk = sdkForPath(path.trim()); if (sdk) { sdkLines.set(sdk, (sdkLines.get(sdk) ?? 0) + +added + +deleted); @@ -60,6 +60,6 @@ const violating = [...sdkLines.entries()] if (violating.length >= MIN_VIOLATING_SDKS) { // Print the list for the CI step to capture and include in the PR comment. - console.log(violating.map(s => `\`${s}\``).join(", ")); + console.log(violating.map(s => `\`${s}\``).join(', ')); process.exit(1); } From 6d2cb4d303243d9388652fb3b0cbc6651efec0ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Apr 2026 16:25:48 +0000 Subject: [PATCH 3/3] fix(sdks): Use strict greater-than for line threshold comparison Change the filter condition from >= to > so that SDKs with exactly LINE_THRESHOLD (5) changed lines are not treated as non-trivial. This aligns the code with the documented behavior stating changes must 'exceed' the threshold ('more than five lines'). Co-Authored-By: Claude Co-authored-by: Daniel Szoke --- scripts/check-sdk-diff-scope.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-sdk-diff-scope.ts b/scripts/check-sdk-diff-scope.ts index 7678bbaef6e5a..99f1795df74c4 100644 --- a/scripts/check-sdk-diff-scope.ts +++ b/scripts/check-sdk-diff-scope.ts @@ -54,7 +54,7 @@ for (const line of input.split('\n')) { // Collect SDKs that exceed the line threshold, ignoring trivial touches. const violating = [...sdkLines.entries()] - .filter(([, n]) => n >= LINE_THRESHOLD) + .filter(([, n]) => n > LINE_THRESHOLD) .map(([sdk]) => sdk) .sort();