Skip to content

build(deps): bump actions/github-script from 7 to 9 #44

build(deps): bump actions/github-script from 7 to 9

build(deps): bump actions/github-script from 7 to 9 #44

name: PR Quality Check
on:
pull_request_target:
types: [opened, synchronize]
permissions:
issues: write
pull-requests: write
contents: read
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Check PR quality signals
uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
const author = pr.user.login;
// Don't check the maintainer
if (author === 'Him-an-shi') return;
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const warnings = [];
// 1. Flag: Very low line count (trivial PR)
const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
if (totalChanges < 5) {
warnings.push(`Trivial change (${totalChanges} lines total). Consider if this warrants a standalone PR or could be bundled with related work.`);
}
// 2. Flag: Test-only PR with low assertion density
const testFiles = files.filter(f => f.filename.includes('test'));
if (testFiles.length > 0 && testFiles.length === files.length) {
// PR only touches test files - check assertion quality
for (const tf of testFiles) {
try {
const { data: content } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: tf.filename,
ref: pr.head.sha,
});
const decoded = Buffer.from(content.content, 'base64').toString();
const assertCount = (decoded.match(/assert |assertEqual|assertTrue|assertFalse|assertRaises|assertIn|pytest\.raises/g) || []).length;
const testCount = (decoded.match(/def test_/g) || []).length;
if (testCount > 0 && assertCount / testCount < 1.5) {
warnings.push(`\`${tf.filename}\`: Low assertion density (${assertCount} asserts across ${testCount} tests). Each test should have multiple meaningful assertions.`);
}
} catch (e) {
// File might be too large or binary - skip
}
}
}
// 3. Flag: PR opened within 10 minutes of its linked issue
const body = pr.body || '';
const issueMatch = body.match(/#(\d+)/);
if (issueMatch) {
try {
const { data: linkedIssue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(issueMatch[1]),
});
const issueTime = new Date(linkedIssue.created_at).getTime();
const prTime = new Date(pr.created_at).getTime();
const diffMinutes = (prTime - issueTime) / (1000 * 60);
if (diffMinutes >= 0 && diffMinutes < 10) {
warnings.push(`PR opened ${Math.round(diffMinutes)} minutes after linked issue #${issueMatch[1]}. Unusually fast turnaround may indicate pre-written/automated submission.`);
}
} catch (e) {
// Issue not found - skip
}
}
if (warnings.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `**Quality check observations:**\n\n${warnings.map(w => '- ' + w).join('\n')}\n\n_These are informational signals, not blocking. Maintainer will review._`
});
}