Skip to content

Commit d27e6f4

Browse files
authored
ci: add pr-size-labeler (#135)
* ci: add pr-size-labeler * leftover
1 parent b462b5a commit d27e6f4

File tree

5 files changed

+330
-1
lines changed

5 files changed

+330
-1
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Large PR Detected
2+
3+
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
4+
5+
### How to unblock this PR:
6+
7+
Add a section to your PR description with the following format:
8+
9+
```markdown
10+
## Large PR Justification
11+
12+
[Explain why this PR must be large, such as:]
13+
14+
- Generated code that cannot be split
15+
- Large refactoring that must be atomic
16+
- Multiple related changes that would break if separated
17+
- Migration or data transformation
18+
```
19+
20+
### Alternative:
21+
22+
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
23+
24+
See our [Contributing Guidelines](CONTRIBUTING_LINK) for more details on the pull request process.
25+
26+
---
27+
28+
_This review will be automatically dismissed once you add the justification section._
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
name: PR Size Labeler - Apply and Enforce
2+
3+
on:
4+
workflow_run:
5+
workflows: ["PR Size Labeler - Calculate"]
6+
types: [completed]
7+
8+
permissions:
9+
contents: read
10+
pull-requests: write
11+
12+
jobs:
13+
apply-size-label:
14+
name: Apply Size Label
15+
runs-on: ubuntu-latest
16+
if: github.event.workflow_run.conclusion == 'success'
17+
steps:
18+
- name: Download artifact
19+
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
20+
with:
21+
name: pr-size-label
22+
path: pr-size/
23+
github-token: ${{ secrets.GITHUB_TOKEN }}
24+
run-id: ${{ github.event.workflow_run.id }}
25+
26+
- name: Read PR number and size label
27+
id: read
28+
run: |
29+
PR_NUMBER=$(cat pr-size/pr-number.txt)
30+
SIZE_LABEL=$(cat pr-size/label.txt | tr -d '"')
31+
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
32+
echo "size_label=$SIZE_LABEL" >> $GITHUB_OUTPUT
33+
echo "PR #$PR_NUMBER should get label: $SIZE_LABEL"
34+
35+
- name: Remove old size labels
36+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
37+
env:
38+
PR_NUMBER: ${{ steps.read.outputs.pr_number }}
39+
with:
40+
script: |
41+
const prNumber = parseInt(process.env.PR_NUMBER);
42+
const sizeLabels = ['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL'];
43+
44+
const currentLabels = await github.rest.issues.listLabelsOnIssue({
45+
owner: context.repo.owner,
46+
repo: context.repo.repo,
47+
issue_number: prNumber
48+
});
49+
50+
for (const label of currentLabels.data) {
51+
if (sizeLabels.includes(label.name)) {
52+
console.log(`Removing old size label: ${label.name}`);
53+
await github.rest.issues.removeLabel({
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
issue_number: prNumber,
57+
name: label.name
58+
});
59+
}
60+
}
61+
62+
- name: Add new size label
63+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
64+
env:
65+
PR_NUMBER: ${{ steps.read.outputs.pr_number }}
66+
SIZE_LABEL: ${{ steps.read.outputs.size_label }}
67+
with:
68+
script: |
69+
const prNumber = parseInt(process.env.PR_NUMBER);
70+
const sizeLabel = process.env.SIZE_LABEL;
71+
72+
console.log(`Adding size label: ${sizeLabel} to PR #${prNumber}`);
73+
74+
await github.rest.issues.addLabels({
75+
owner: context.repo.owner,
76+
repo: context.repo.repo,
77+
issue_number: prNumber,
78+
labels: [sizeLabel]
79+
});
80+
81+
enforce-xl-justification:
82+
name: Enforce XL PR Justification
83+
runs-on: ubuntu-latest
84+
if: github.event.workflow_run.conclusion == 'success'
85+
steps:
86+
- name: Checkout repository
87+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
88+
89+
- name: Download artifact
90+
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
91+
with:
92+
name: pr-size-label
93+
path: pr-size/
94+
github-token: ${{ secrets.GITHUB_TOKEN }}
95+
run-id: ${{ github.event.workflow_run.id }}
96+
97+
- name: Read PR number and check for XL justification
98+
id: check
99+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
100+
with:
101+
script: |
102+
const fs = require('fs');
103+
104+
const prNumber = parseInt(fs.readFileSync('pr-size/pr-number.txt', 'utf8').trim());
105+
const sizeLabel = fs.readFileSync('pr-size/label.txt', 'utf8').trim().replace(/"/g, '');
106+
107+
console.log('PR Number:', prNumber);
108+
console.log('Size Label:', sizeLabel);
109+
110+
const pr = await github.rest.pulls.get({
111+
owner: context.repo.owner,
112+
repo: context.repo.repo,
113+
pull_number: prNumber
114+
});
115+
116+
const hasXLLabel = sizeLabel === 'size/XL';
117+
const prBody = pr.data.body || '';
118+
const hasJustification = /##\s*Large PR Justification/i.test(prBody);
119+
120+
console.log('Has XL label:', hasXLLabel);
121+
console.log('Has justification:', hasJustification);
122+
123+
return {
124+
prNumber: prNumber,
125+
hasXLLabel: hasXLLabel,
126+
hasJustification: hasJustification,
127+
needsEnforcement: hasXLLabel && !hasJustification,
128+
shouldDismiss: hasXLLabel && hasJustification
129+
};
130+
131+
- name: Request changes if no justification
132+
if: fromJSON(steps.check.outputs.result).needsEnforcement
133+
continue-on-error: true
134+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
135+
env:
136+
RESULT_JSON: ${{ steps.check.outputs.result }}
137+
with:
138+
script: |
139+
const result = JSON.parse(process.env.RESULT_JSON);
140+
const prNumber = result.prNumber;
141+
142+
// Check if we already have a review requesting changes
143+
const reviews = await github.rest.pulls.listReviews({
144+
owner: context.repo.owner,
145+
repo: context.repo.repo,
146+
pull_number: prNumber
147+
});
148+
149+
const botReview = reviews.data.find(review =>
150+
review.user.login === 'github-actions[bot]' &&
151+
review.state === 'CHANGES_REQUESTED'
152+
);
153+
154+
if (botReview) {
155+
console.log('Already requested changes in review:', botReview.id);
156+
return;
157+
}
158+
159+
// Read the message template from file
160+
const fs = require('fs');
161+
const template = fs.readFileSync('.github/workflows/pr-size-justification-template.md', 'utf8');
162+
const contributingLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/CONTRIBUTING.md#pull-request-process`;
163+
const message = template.replace('CONTRIBUTING_LINK', contributingLink);
164+
165+
// Request changes with explanation
166+
await github.rest.pulls.createReview({
167+
owner: context.repo.owner,
168+
repo: context.repo.repo,
169+
pull_number: prNumber,
170+
event: 'REQUEST_CHANGES',
171+
body: message
172+
});
173+
174+
console.log('Created review requesting changes for PR #' + prNumber);
175+
176+
- name: Dismiss review if justification added
177+
if: fromJSON(steps.check.outputs.result).shouldDismiss
178+
continue-on-error: true
179+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
180+
env:
181+
RESULT_JSON: ${{ steps.check.outputs.result }}
182+
with:
183+
script: |
184+
const result = JSON.parse(process.env.RESULT_JSON);
185+
const prNumber = result.prNumber;
186+
187+
// Find our previous review requesting changes
188+
const reviews = await github.rest.pulls.listReviews({
189+
owner: context.repo.owner,
190+
repo: context.repo.repo,
191+
pull_number: prNumber
192+
});
193+
194+
const botReview = reviews.data.find(review =>
195+
review.user.login === 'github-actions[bot]' &&
196+
review.state === 'CHANGES_REQUESTED'
197+
);
198+
199+
if (botReview) {
200+
await github.rest.pulls.dismissReview({
201+
owner: context.repo.owner,
202+
repo: context.repo.repo,
203+
pull_number: prNumber,
204+
review_id: botReview.id,
205+
message: 'Large PR justification has been provided. Thank you!'
206+
});
207+
console.log('Dismissed previous review:', botReview.id);
208+
209+
// Add a comment confirming unblock
210+
await github.rest.issues.createComment({
211+
owner: context.repo.owner,
212+
repo: context.repo.repo,
213+
issue_number: prNumber,
214+
body: '✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review.'
215+
});
216+
} else {
217+
console.log('No previous blocking review found to dismiss');
218+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: PR Size Labeler - Calculate
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
calculate-pr-size:
12+
name: Calculate PR Size
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Get PR details
16+
id: pr
17+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
18+
with:
19+
script: |
20+
const pr = await github.rest.pulls.get({
21+
owner: context.repo.owner,
22+
repo: context.repo.repo,
23+
pull_number: context.issue.number
24+
});
25+
26+
const additions = pr.data.additions;
27+
const deletions = pr.data.deletions;
28+
const totalChanges = additions + deletions;
29+
30+
console.log(`PR #${context.issue.number}: +${additions} -${deletions} (${totalChanges} total)`);
31+
32+
return {
33+
additions: additions,
34+
deletions: deletions,
35+
total: totalChanges
36+
};
37+
38+
- name: Determine size label
39+
id: size
40+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
41+
with:
42+
script: |
43+
const changes = ${{ steps.pr.outputs.result }};
44+
const total = changes.total;
45+
46+
let sizeLabel = '';
47+
48+
if (total < 100) {
49+
sizeLabel = 'size/XS';
50+
} else if (total < 300) {
51+
sizeLabel = 'size/S';
52+
} else if (total < 600) {
53+
sizeLabel = 'size/M';
54+
} else if (total < 1000) {
55+
sizeLabel = 'size/L';
56+
} else {
57+
sizeLabel = 'size/XL';
58+
}
59+
60+
console.log(`PR size: ${total} lines -> ${sizeLabel}`);
61+
return sizeLabel;
62+
63+
- name: Save size label to artifact
64+
run: |
65+
mkdir -p pr-size
66+
echo "${{ steps.size.outputs.result }}" > pr-size/label.txt
67+
echo "${{ github.event.pull_request.number }}" > pr-size/pr-number.txt
68+
69+
- name: Upload artifact
70+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
71+
with:
72+
name: pr-size-label
73+
path: pr-size/
74+
retention-days: 1

CONTRIBUTING.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ PRs to resolve existing issues are greatly appreciated, and issues labeled as ["
8585

8686
- Ensure that CI passes, if it fails, fix the failures.
8787

88+
- **Keep PRs small for efficient reviews**: For better review quality and faster turnaround, keep your PRs under **1000 lines of changes**. Smaller PRs are easier to review, less likely to introduce bugs, and get merged faster. Our [PR Size Labeler](./.github/workflows/pr-size-labeler.yml) automatically labels PRs based on size:
89+
90+
- `size/XS`: < 100 lines
91+
- `size/S`: 100-299 lines
92+
- `size/M`: 300-599 lines
93+
- `size/L`: 600-999 lines
94+
- `size/XL`: ≥ 1000 lines (requires justification)
95+
96+
If your PR exceeds 1000 lines, you'll be asked to provide a justification explaining why it cannot be split into smaller PRs.
97+
8898
- Every pull request requires a review from the core ToolHive team before merging.
8999

90100
- Once approved, all of your commits will be squashed into a single commit with your PR title.

src/app/catalog/[repoName]/[serverName]/[version]/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export default async function CatalogDetailPage({
1515
params,
1616
}: CatalogDetailPageProps) {
1717
const { repoName, serverName, version } = await params;
18-
1918
const { data: serverResponse, response } = await getServerDetails(
2019
`${repoName}/${serverName}`,
2120
version,

0 commit comments

Comments
 (0)