Skip to content

Commit 2c40441

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 2c40441

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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: Compute diff and evaluate
28+
id: evaluate
29+
run: |
30+
set +e
31+
SDK_LIST=$(git diff --numstat origin/${{ github.base_ref }}...HEAD | bun scripts/check-sdk-diff-scope.ts)
32+
if [ $? -ne 0 ]; then
33+
echo "violation=true" >> "$GITHUB_OUTPUT"
34+
echo "sdk_list=$SDK_LIST" >> "$GITHUB_OUTPUT"
35+
else
36+
echo "violation=false" >> "$GITHUB_OUTPUT"
37+
fi
38+
39+
- name: Post, update, or delete PR comment
40+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
41+
with:
42+
script: |
43+
const MARKER = '<!-- enforce-single-sdk-changes -->';
44+
const violation = '${{ steps.evaluate.outputs.violation }}' === 'true';
45+
46+
const {data: comments} = await github.rest.issues.listComments({
47+
owner: context.repo.owner,
48+
repo: context.repo.repo,
49+
issue_number: context.issue.number,
50+
});
51+
const existing = comments.find(c => c.body.includes(MARKER));
52+
53+
if (violation) {
54+
const sdkList = ${{ toJSON(steps.evaluate.outputs.sdk_list) }};
55+
const body = `${MARKER}
56+
### 🚫 Non-trivial changes to multiple SDKs
57+
58+
This PR contains non-trivial changes to multiple SDKs: ${sdkList}.
59+
Changes to multiple SDKs should be submitted as separate PRs: **one PR per SDK**.
60+
61+
Please **split this PR** accordingly. Thank you in advance! 🙏`;
62+
63+
if (existing && existing.body !== body) {
64+
await github.rest.issues.updateComment({
65+
owner: context.repo.owner,
66+
repo: context.repo.repo,
67+
comment_id: existing.id,
68+
body,
69+
});
70+
} else if (!existing) {
71+
await github.rest.issues.createComment({
72+
owner: context.repo.owner,
73+
repo: context.repo.repo,
74+
issue_number: context.issue.number,
75+
body,
76+
});
77+
}
78+
} else if (existing) {
79+
await github.rest.issues.deleteComment({
80+
owner: context.repo.owner,
81+
repo: context.repo.repo,
82+
comment_id: existing.id,
83+
});
84+
}
85+
86+
- name: Fail check
87+
if: steps.evaluate.outputs.violation == 'true'
88+
run: exit 1

scripts/check-sdk-diff-scope.ts

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

0 commit comments

Comments
 (0)