Skip to content

Commit 51b9566

Browse files
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.
1 parent 23c9beb commit 51b9566

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Enforce Single SDK Changes
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'docs/platforms/**'
7+
- 'platform-includes/**'
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
check-sdk-diff-scope:
15+
name: Check SDK diff scope
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
pull-requests: write
20+
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Set up bun
28+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
29+
30+
- name: Compute diff and evaluate
31+
id: evaluate
32+
run: |
33+
set +e
34+
SDK_LIST=$(git diff --numstat origin/${{ github.base_ref }}...HEAD | bun scripts/check-sdk-diff-scope.ts)
35+
EXIT=$?
36+
if [ $EXIT -eq 0 ]; then
37+
echo "violation=false" >> "$GITHUB_OUTPUT"
38+
elif [ -n "$SDK_LIST" ]; then
39+
echo "violation=true" >> "$GITHUB_OUTPUT"
40+
echo "sdk_list=$SDK_LIST" >> "$GITHUB_OUTPUT"
41+
else
42+
echo "error=true" >> "$GITHUB_OUTPUT"
43+
fi
44+
45+
- name: Post, update, or delete PR comment
46+
if: steps.evaluate.outputs.error != 'true'
47+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
48+
with:
49+
script: |
50+
const MARKER = '<!-- enforce-single-sdk-changes -->';
51+
const violation = '${{ steps.evaluate.outputs.violation }}' === 'true';
52+
53+
const {data: comments} = await github.rest.issues.listComments({
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
issue_number: context.issue.number,
57+
});
58+
const existing = comments.find(c => c.body.includes(MARKER));
59+
60+
if (violation) {
61+
const sdkList = ${{ toJSON(steps.evaluate.outputs.sdk_list) }};
62+
const body = `${MARKER}
63+
### 🚫 Non-trivial changes to multiple SDKs
64+
65+
This PR contains non-trivial changes to multiple SDKs: ${sdkList}.
66+
Changes to multiple SDKs should be submitted as separate PRs: **one PR per SDK**.
67+
68+
Please **split this PR** accordingly. Thank you in advance! 🙏`;
69+
70+
if (existing && existing.body !== body) {
71+
await github.rest.issues.updateComment({
72+
owner: context.repo.owner,
73+
repo: context.repo.repo,
74+
comment_id: existing.id,
75+
body,
76+
});
77+
} else if (!existing) {
78+
await github.rest.issues.createComment({
79+
owner: context.repo.owner,
80+
repo: context.repo.repo,
81+
issue_number: context.issue.number,
82+
body,
83+
});
84+
}
85+
} else if (existing) {
86+
await github.rest.issues.deleteComment({
87+
owner: context.repo.owner,
88+
repo: context.repo.repo,
89+
comment_id: existing.id,
90+
});
91+
}
92+
93+
- name: Fail on unexpected error
94+
if: steps.evaluate.outputs.error == 'true'
95+
run: |
96+
echo "::error::SDK diff check failed unexpectedly — the script produced no output. Check the job logs for details."
97+
exit 1
98+
99+
- name: Fail check
100+
if: steps.evaluate.outputs.violation == 'true'
101+
run: exit 1

scripts/check-sdk-diff-scope.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bun
2+
import {readFileSync} from 'fs';
3+
/**
4+
* Enforces the policy that a single PR should only contain non-trivial changes
5+
* to one SDK at a time. PRs spanning multiple SDKs are harder to review and
6+
* increase the risk of inconsistencies between platforms.
7+
*
8+
* Reads `git diff --numstat` from stdin (one `<added>\t<deleted>\t<path>` line
9+
* per changed file) and attributes each file to an SDK. If the total lines
10+
* changed for at least MIN_VIOLATING_SDKS SDKs each exceed LINE_THRESHOLD,
11+
* the script prints the violating SDK list and exits 1.
12+
*
13+
* Used in CI as:
14+
* git diff --numstat origin/$BASE...HEAD | bun scripts/check-sdk-diff-scope.ts
15+
*/
16+
17+
/** 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). */
18+
const LINE_THRESHOLD = 5;
19+
/** Minimum number of SDKs that must exceed LINE_THRESHOLD for the check to fail. */
20+
const MIN_VIOLATING_SDKS = 2;
21+
22+
/**
23+
* Extracts the SDK name from a changed file path, or returns null if the
24+
* file is not attributed to any SDK.
25+
*
26+
* Two path patterns are recognised:
27+
* - docs/platforms/<sdk>/... → sdk is the first path segment after platforms/
28+
* - platform-includes/.../<sdk>.<variant>.mdx → sdk is the first dot-segment of the filename
29+
* e.g. `javascript.angular.mdx` → `javascript`, `react-native.mdx` → `react-native`
30+
*/
31+
function sdkForPath(path: string): string | null {
32+
const segs = path.split("/");
33+
if (segs[0] === "docs" && segs[1] === "platforms") {
34+
return segs[2] ?? null;
35+
}
36+
if (segs[0] === "platform-includes") {
37+
return segs.at(-1)!.split(".")[0];
38+
}
39+
return null;
40+
}
41+
42+
// Accumulate total lines changed (added + deleted) per SDK.
43+
const sdkLines = new Map<string, number>();
44+
45+
const input = readFileSync(0, 'utf-8');
46+
for (const line of input.split("\n")) {
47+
const [added, deleted, path] = line.split("\t");
48+
if (!path || added === "-") continue; // binary file or empty line
49+
const sdk = sdkForPath(path.trim());
50+
if (sdk) {
51+
sdkLines.set(sdk, (sdkLines.get(sdk) ?? 0) + +added + +deleted);
52+
}
53+
}
54+
55+
// Collect SDKs that exceed the line threshold, ignoring trivial touches.
56+
const violating = [...sdkLines.entries()]
57+
.filter(([, n]) => n >= LINE_THRESHOLD)
58+
.map(([sdk]) => sdk)
59+
.sort();
60+
61+
if (violating.length >= MIN_VIOLATING_SDKS) {
62+
// Print the list for the CI step to capture and include in the PR comment.
63+
console.log(violating.map(s => `\`${s}\``).join(", "));
64+
process.exit(1);
65+
}

0 commit comments

Comments
 (0)