ci: stop auto-closing PR hygiene failures

This commit is contained in:
Danila Poyarkov 2026-06-07 08:51:37 +03:00
parent f78a3f8663
commit 3d6ade211c
5 changed files with 39 additions and 77 deletions

View file

@ -15,17 +15,17 @@ reviews:
mode: "error"
requirements: "Apply CONTRIBUTING.md → Pull requests → PR title. Fail titles that are non-English, vague, placeholder-like, or do not identify the actual change."
description:
mode: "error"
mode: "warning"
custom_checks:
- name: "PR Hygiene: English"
mode: "error"
instructions: "Use CONTRIBUTING.md → Pull requests as the source of truth. Fail when the PR title or body is not primarily English, except for code identifiers, file paths, logs, error messages, and short quoted examples."
- name: "PR Hygiene: Template"
mode: "error"
instructions: "Use CONTRIBUTING.md → Pull requests as the source of truth. Fail when the PR body contains unfilled template placeholders, dangling issue references, TODO/TBD text, empty headings, or template comments."
mode: "warning"
instructions: "Use CONTRIBUTING.md → Pull requests as the source of truth. Warn when the PR body contains unfilled template placeholders, dangling issue references, TODO/TBD text, empty headings, or template comments. Do not treat missing exact headings as a failure when the author otherwise explains the change."
- name: "PR Hygiene: Substance"
mode: "error"
instructions: "Use CONTRIBUTING.md → Pull requests as the source of truth. Fail when the PR body does not explain what changed, why it changed, and how it was validated."
mode: "warning"
instructions: "Use CONTRIBUTING.md → Pull requests as the source of truth. Warn when the PR body does not explain what changed, why it changed, or how it was validated. Do not call a focused PR low-effort solely because it needs template cleanup."
chat:
auto_reply: true

View file

@ -1,6 +1,6 @@
> Security vulnerability? Do not open a public PR. Report it privately through GitHub Security Advisories: https://github.com/open-pencil/open-pencil/security/advisories/new
Before opening a PR, read `CONTRIBUTING.md` and `AGENTS.md`. PRs that ignore the template, use placeholder text, are not written in English, or do not explain validation may be closed as invalid.
Before opening a PR, read `CONTRIBUTING.md` and `AGENTS.md`. PRs should explain the intent, meaningful changes, and validation. Placeholder, non-English, unrelated, or otherwise unreviewable PRs may be closed by maintainers.
### Summary

View file

@ -21,6 +21,8 @@ interface GitHubEvent {
interface PullRequestResponse {
state: string
author_association: string
title: string
user: { login: string }
}
class GitHubAPIError extends Error {
@ -73,23 +75,20 @@ function normalizedCheckName(value: string): string {
return value.replace(/[^a-z0-9]+/gi, ' ').trim().toLowerCase()
}
function isPRHygieneFailure(line: string): boolean {
function prHygieneFailureName(line: string): string | null {
const cells = tableCells(line)
const checkName = cells[0] ?? ''
const status = cells[1] ?? ''
if (checkName.toLowerCase().includes('[ignored]')) return false
if (!normalizedCheckName(checkName).startsWith('pr hygiene')) return false
if (checkName.toLowerCase().includes('[ignored]')) return null
if (!normalizedCheckName(checkName).startsWith('pr hygiene')) return null
if (!/❌/u.test(status) && !/\berror\b/i.test(status)) return null
return /❌/u.test(status) || /\berror\b/i.test(status)
return checkName
}
const hygieneFailed = text.split('\n').some(isPRHygieneFailure)
const hygieneFailures = text.split('\n').map(prHygieneFailureName).filter((name): name is string => Boolean(name))
if (hygieneFailed) {
console.log('Detected failed PR Hygiene check from CodeRabbit pre-merge table.')
}
if (!hygieneFailed) {
if (hygieneFailures.length === 0) {
console.log('CodeRabbit signal did not reference a failed PR Hygiene check.')
process.exit(0)
}
@ -107,7 +106,10 @@ async function github<T>(path: string, options: RequestInit = {}): Promise<T | n
if (!response.ok) {
const body = await response.text()
throw new GitHubAPIError(`${options.method ?? 'GET'} ${path} failed: ${response.status} ${body}`, response.status)
throw new GitHubAPIError(
`${options.method ?? 'GET'} ${path} failed: ${response.status} ${body}`,
response.status
)
}
if (response.status === 204) return null
@ -117,50 +119,11 @@ async function github<T>(path: string, options: RequestInit = {}): Promise<T | n
const pr = await github<PullRequestResponse>(`/repos/${owner}/${repo}/pulls/${issueNumber}`)
if (!pr) throw new Error(`PR #${issueNumber} returned no data`)
const trustedAssociations = new Set(['OWNER', 'MEMBER', 'COLLABORATOR'])
if (trustedAssociations.has(pr.author_association)) {
console.log(`Not closing trusted author association: ${pr.author_association}`)
process.exit(0)
}
if (pr.state !== 'open') {
console.log(`PR is already ${pr.state}.`)
process.exit(0)
}
const label = 'invalid'
try {
await github<unknown>(`/repos/${owner}/${repo}/labels/${encodeURIComponent(label)}`)
} catch (error) {
if (!(error instanceof GitHubAPIError) || error.status !== 404) throw error
try {
await github<unknown>(`/repos/${owner}/${repo}/labels`, {
method: 'POST',
body: JSON.stringify({
name: label,
color: 'd73a4a',
description: 'Does not meet contribution requirements'
})
})
} catch (createError) {
if (!(createError instanceof GitHubAPIError) || createError.status !== 422) throw createError
}
}
await github<unknown>(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
method: 'POST',
body: JSON.stringify({ labels: [label] })
})
await github<unknown>(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
method: 'POST',
body: JSON.stringify({
body: 'Closing this as a low-effort PR because CodeRabbit failed the PR Hygiene check. See `CONTRIBUTING.md` and the PR template before opening a new PR. If you are sure this was closed by mistake, please file an issue with a link to this PR and the relevant context.'
})
})
await github<unknown>(`/repos/${owner}/${repo}/pulls/${issueNumber}`, {
method: 'PATCH',
body: JSON.stringify({ state: 'closed' })
})
console.log(
[
`Detected CodeRabbit PR Hygiene failure on #${issueNumber}: ${hygieneFailures.join(', ')}`,
`Author: ${pr.user.login} (${pr.author_association})`,
`Title: ${pr.title}`,
'No automatic close/label/comment was applied. Treat this as maintainer review signal only.'
].join('\n')
)

View file

@ -1,4 +1,4 @@
name: Close low-effort PRs
name: Monitor PR hygiene
on:
pull_request_review:
@ -7,18 +7,17 @@ on:
types: [created, edited]
concurrency:
group: close-low-effort-prs-${{ github.event.pull_request.number || github.event.issue.number }}
group: monitor-pr-hygiene-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: false
jobs:
# This job only reacts to CodeRabbit PR Hygiene pre-merge failures and never runs contributor code.
close-low-effort:
name: Close low-effort PRs
# This job only logs CodeRabbit PR Hygiene pre-merge failures and never runs contributor code.
monitor-pr-hygiene:
name: Monitor PR hygiene
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
pull-requests: read
steps:
- name: Check out trusted workflow scripts
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
@ -31,12 +30,12 @@ jobs:
with:
node-version: 24
- name: Close low-effort PR when CodeRabbit fails PR Hygiene
- name: Log CodeRabbit PR Hygiene signal
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
if [ ! -f .github/scripts/close-low-effort-prs.ts ]; then
echo "Trusted close-low-effort helper is not present on the default branch yet."
if [ ! -f .github/scripts/monitor-pr-hygiene.ts ]; then
echo "Trusted PR hygiene monitor is not present on the default branch yet."
exit 0
fi
node --experimental-strip-types .github/scripts/close-low-effort-prs.ts
node --experimental-strip-types .github/scripts/monitor-pr-hygiene.ts

View file

@ -37,11 +37,11 @@ Pull requests must be reviewable without guessing the author's intent.
- Document validation, such as `bun run check`, targeted tests, docs-only review, or an explicit reason validation was not run.
- Keep the body primarily in English. Code identifiers, file paths, logs, error messages, and short quoted examples may use their original language.
### Invalid PRs
### Low-effort PRs
Do not submit placeholder PRs. Remove template comments before opening a PR. Do not leave dangling issue references such as `Fixes #`, `TODO`, `TBD`, empty headings, unfilled sections, or similar unfinished text.
CodeRabbit enforces these PR rules through PR Hygiene checks. Low-effort PRs from external contributors that ignore the template, omit validation, are not written in English, or otherwise do not follow these guidelines may be labeled `invalid` and closed automatically. If you are unsure how to fix something, please open a detailed issue instead of submitting a placeholder PR. If you are sure your PR was closed by mistake, please file an issue with the PR link and context.
CodeRabbit may flag PR Hygiene issues for maintainers to review. Missing template sections or validation details are normal review feedback; they are not, by themselves, a personal judgment on the contributor. Maintainers may close PRs manually when they are clearly low-effort, automated, not written in English, unrelated to the project, or impossible to review without substantial guesswork. If you are unsure how to fix something, please open a detailed issue instead of submitting a placeholder PR.
## Quality checks