diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 68087968e..b21c49bca 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -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 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6701fd544..332ba4b29 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 diff --git a/.github/scripts/close-low-effort-prs.ts b/.github/scripts/monitor-pr-hygiene.ts similarity index 58% rename from .github/scripts/close-low-effort-prs.ts rename to .github/scripts/monitor-pr-hygiene.ts index 7c195d776..0d84d1cd2 100644 --- a/.github/scripts/close-low-effort-prs.ts +++ b/.github/scripts/monitor-pr-hygiene.ts @@ -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(path: string, options: RequestInit = {}): Promise(path: string, options: RequestInit = {}): Promise(`/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(`/repos/${owner}/${repo}/labels/${encodeURIComponent(label)}`) -} catch (error) { - if (!(error instanceof GitHubAPIError) || error.status !== 404) throw error - try { - await github(`/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(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, { - method: 'POST', - body: JSON.stringify({ labels: [label] }) -}) - -await github(`/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(`/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') +) diff --git a/.github/workflows/close-low-effort-prs.yml b/.github/workflows/monitor-pr-hygiene.yml similarity index 52% rename from .github/workflows/close-low-effort-prs.yml rename to .github/workflows/monitor-pr-hygiene.yml index d65782dc2..a99aa5950 100644 --- a/.github/workflows/close-low-effort-prs.yml +++ b/.github/workflows/monitor-pr-hygiene.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a76af6d6..2281c87e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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