Skip to content

Commit 0bef493

Browse files
authored
Merge branch 'main' into patch-11
2 parents d193ff4 + 90aad4b commit 0bef493

37 files changed

Lines changed: 4237 additions & 419 deletions
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Issue Similarity Check
2+
3+
on:
4+
issues:
5+
types: [opened]
6+
7+
permissions:
8+
issues: write
9+
10+
jobs:
11+
similarity-check:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/github-script@v7
15+
with:
16+
script: |
17+
const issue = context.payload.issue;
18+
const author = issue.user.login;
19+
const currentBody = issue.body || '';
20+
21+
// Don't check the maintainer
22+
if (author === 'Him-an-shi') return;
23+
24+
// Skip short issues (can't meaningfully compare)
25+
if (currentBody.length < 200) return;
26+
27+
// Get author's other open issues
28+
const { data: authorIssues } = await github.rest.issues.listForRepo({
29+
owner: context.repo.owner,
30+
repo: context.repo.repo,
31+
creator: author,
32+
state: 'open',
33+
per_page: 30,
34+
});
35+
36+
// Simple word-set similarity (Jaccard index)
37+
const tokenize = (text) => {
38+
return new Set(
39+
text.toLowerCase()
40+
.replace(/[^a-z0-9\s]/g, ' ')
41+
.split(/\s+/)
42+
.filter(w => w.length > 4)
43+
);
44+
};
45+
46+
const jaccard = (a, b) => {
47+
const intersection = [...a].filter(x => b.has(x));
48+
const union = new Set([...a, ...b]);
49+
return union.size > 0 ? intersection.length / union.size : 0;
50+
};
51+
52+
const currentTokens = tokenize(currentBody);
53+
let highSimilarityCount = 0;
54+
55+
for (const other of authorIssues) {
56+
if (other.number === issue.number) continue;
57+
const otherTokens = tokenize(other.body || '');
58+
const sim = jaccard(currentTokens, otherTokens);
59+
if (sim > 0.55) {
60+
highSimilarityCount++;
61+
}
62+
}
63+
64+
// Flag if 3+ existing issues are highly similar to this one
65+
if (highSimilarityCount >= 3) {
66+
await github.rest.issues.createComment({
67+
owner: context.repo.owner,
68+
repo: context.repo.repo,
69+
issue_number: issue.number,
70+
body: `This issue has high textual similarity to ${highSimilarityCount} other open issues from the same author. This may indicate a templated bulk submission. @Him-an-shi please review if these should be consolidated into an umbrella issue.`
71+
});
72+
await github.rest.issues.addLabels({
73+
owner: context.repo.owner,
74+
repo: context.repo.repo,
75+
issue_number: issue.number,
76+
labels: ['potential-bulk-submission']
77+
});
78+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: PR Quality Check
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, synchronize]
6+
7+
permissions:
8+
issues: write
9+
pull-requests: write
10+
contents: read
11+
12+
jobs:
13+
quality-gate:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
ref: ${{ github.event.pull_request.head.sha }}
19+
20+
- name: Check PR quality signals
21+
uses: actions/github-script@v7
22+
with:
23+
script: |
24+
const pr = context.payload.pull_request;
25+
const author = pr.user.login;
26+
27+
// Don't check the maintainer
28+
if (author === 'Him-an-shi') return;
29+
30+
const { data: files } = await github.rest.pulls.listFiles({
31+
owner: context.repo.owner,
32+
repo: context.repo.repo,
33+
pull_number: pr.number,
34+
});
35+
36+
const warnings = [];
37+
38+
// 1. Flag: Very low line count (trivial PR)
39+
const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
40+
if (totalChanges < 5) {
41+
warnings.push(`Trivial change (${totalChanges} lines total). Consider if this warrants a standalone PR or could be bundled with related work.`);
42+
}
43+
44+
// 2. Flag: Test-only PR with low assertion density
45+
const testFiles = files.filter(f => f.filename.includes('test'));
46+
if (testFiles.length > 0 && testFiles.length === files.length) {
47+
// PR only touches test files - check assertion quality
48+
for (const tf of testFiles) {
49+
try {
50+
const { data: content } = await github.rest.repos.getContent({
51+
owner: context.repo.owner,
52+
repo: context.repo.repo,
53+
path: tf.filename,
54+
ref: pr.head.sha,
55+
});
56+
const decoded = Buffer.from(content.content, 'base64').toString();
57+
const assertCount = (decoded.match(/assert |assertEqual|assertTrue|assertFalse|assertRaises|assertIn|pytest\.raises/g) || []).length;
58+
const testCount = (decoded.match(/def test_/g) || []).length;
59+
60+
if (testCount > 0 && assertCount / testCount < 1.5) {
61+
warnings.push(`\`${tf.filename}\`: Low assertion density (${assertCount} asserts across ${testCount} tests). Each test should have multiple meaningful assertions.`);
62+
}
63+
} catch (e) {
64+
// File might be too large or binary - skip
65+
}
66+
}
67+
}
68+
69+
// 3. Flag: PR opened within 10 minutes of its linked issue
70+
const body = pr.body || '';
71+
const issueMatch = body.match(/#(\d+)/);
72+
if (issueMatch) {
73+
try {
74+
const { data: linkedIssue } = await github.rest.issues.get({
75+
owner: context.repo.owner,
76+
repo: context.repo.repo,
77+
issue_number: parseInt(issueMatch[1]),
78+
});
79+
const issueTime = new Date(linkedIssue.created_at).getTime();
80+
const prTime = new Date(pr.created_at).getTime();
81+
const diffMinutes = (prTime - issueTime) / (1000 * 60);
82+
83+
if (diffMinutes >= 0 && diffMinutes < 10) {
84+
warnings.push(`PR opened ${Math.round(diffMinutes)} minutes after linked issue #${issueMatch[1]}. Unusually fast turnaround may indicate pre-written/automated submission.`);
85+
}
86+
} catch (e) {
87+
// Issue not found - skip
88+
}
89+
}
90+
91+
if (warnings.length > 0) {
92+
await github.rest.issues.createComment({
93+
owner: context.repo.owner,
94+
repo: context.repo.repo,
95+
issue_number: pr.number,
96+
body: `**Quality check observations:**\n\n${warnings.map(w => '- ' + w).join('\n')}\n\n_These are informational signals, not blocking. Maintainer will review._`
97+
});
98+
}

.github/workflows/volume-guard.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Contribution Volume Guard
2+
3+
on:
4+
issues:
5+
types: [opened]
6+
pull_request_target:
7+
types: [opened]
8+
9+
permissions:
10+
issues: write
11+
pull-requests: write
12+
13+
jobs:
14+
volume-check:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/github-script@v7
18+
with:
19+
script: |
20+
const author = context.payload.issue?.user?.login || context.payload.pull_request?.user?.login;
21+
if (!author) return;
22+
23+
// Don't flag the maintainer
24+
if (author === 'Him-an-shi') return;
25+
26+
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
27+
28+
const { data: items } = await github.rest.issues.listForRepo({
29+
owner: context.repo.owner,
30+
repo: context.repo.repo,
31+
creator: author,
32+
since: since,
33+
state: 'all',
34+
per_page: 100,
35+
});
36+
37+
const recentCount = items.filter(i =>
38+
new Date(i.created_at) > new Date(since)
39+
).length;
40+
41+
if (recentCount > 5) {
42+
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
43+
await github.rest.issues.createComment({
44+
owner: context.repo.owner,
45+
repo: context.repo.repo,
46+
issue_number: issueNumber,
47+
body: `@Him-an-shi Heads up: **@${author}** has opened **${recentCount} items** in the last 24 hours. Flagging for review to check for potential bulk/automated submissions.`
48+
});
49+
await github.rest.issues.addLabels({
50+
owner: context.repo.owner,
51+
repo: context.repo.repo,
52+
issue_number: issueNumber,
53+
labels: ['needs-triage']
54+
});
55+
}

0 commit comments

Comments
 (0)