ci: gate invalid pull requests with CodeRabbit (#327)

* ci: gate invalid pull requests with CodeRabbit

* fix: harden CodeRabbit PR hygiene gate

* fix: make invalid PR label creation idempotent
This commit is contained in:
Danila Poyarkov 2026-06-07 00:36:47 +03:00 committed by GitHub
parent 33f53324f7
commit 280b51e666
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 264 additions and 7 deletions

22
.coderabbit.yaml Normal file
View file

@ -0,0 +1,22 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "en-US"
early_access: false
reviews:
profile: "assertive"
request_changes_workflow: true
high_level_summary: true
review_status: true
auto_review:
enabled: true
drafts: false
pre_merge_checks:
description:
mode: "error"
custom_checks:
- name: "PR Hygiene"
mode: "error"
instructions: "Fail if the PR title or description is not written in English, does not follow the repository PR template, contains leftover template comments or placeholders such as 'Fixes #', TODO, TBD, has no meaningful summary, has no concrete list of changes, or does not document validation. Pass only when the PR clearly explains what changed, why it changed, and how it was validated."
chat:
auto_reply: true

View file

@ -1,10 +1,17 @@
Fixes #
> 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
> Security vulnerability? Please 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.
---
### Summary
**Checklist:**
- [ ] `bun run check` passes (lint + typecheck)
- [ ] Tests added or updated
- [ ] CHANGELOG.md updated (if user-facing)
<!-- Explain what this PR changes and why. Remove this comment before opening. -->
### What changed
- Add a concise summary here.
### Validation
- [ ] `bun run check`
- [ ] Tests added or updated, or not needed because: explain here
- [ ] CHANGELOG.md updated, or not needed because: explain here

38
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,38 @@
# OpenPencil Copilot instructions
OpenPencil is a Bun workspace for a Vue 3 + CanvasKit design editor with Tauri desktop support.
## Pull request requirements
- Write PR titles and bodies in English.
- Follow `.github/PULL_REQUEST_TEMPLATE.md` exactly.
- Remove template comments and placeholders before opening a PR.
- Never leave dangling text like `Fixes #`, `TODO`, `TBD`, or unfinished checklist explanations.
- Summarize what changed, why it changed, and how it was validated.
- If a user-facing change is made, update `CHANGELOG.md`.
- Follow `CONTRIBUTING.md` and `AGENTS.md` for repository conventions.
## Validation
Use Bun, not npm or Node scripts unless the package explicitly requires Node.
Before proposing changes, run targeted checks when possible and document them in the PR body. For broad changes, run:
```sh
bun run check
```
Package publishing changes should also run:
```sh
bun scripts/smoke-packages.ts
```
## Code conventions
- No `any` unless there is a clear typed alternative problem and a justification.
- No non-null assertions; use guards.
- Use `crypto.getRandomValues()`, never `Math.random()`.
- Keep Vue components free of `<style>` blocks.
- Use existing dependencies and Reka UI components before hand-rolling UI.
- Preserve architecture boundaries from `AGENTS.md`.

140
.github/scripts/close-invalid-prs.ts vendored Normal file
View file

@ -0,0 +1,140 @@
import { readFile } from 'node:fs/promises'
const owner = process.env.GITHUB_REPOSITORY_OWNER
const repo = process.env.GITHUB_REPOSITORY?.split('/')[1]
const eventPath = process.env.GITHUB_EVENT_PATH
const token = process.env.GITHUB_TOKEN
if (!owner || !repo || !eventPath || !token) {
throw new Error('Missing required GitHub Actions environment')
}
interface GitHubEvent {
sender?: { login?: string }
review?: { state?: string; body?: string }
pull_request?: { number?: number }
issue?: { number?: number; pull_request?: unknown }
comment?: { body?: string }
}
interface PullRequestResponse {
state: string
author_association: string
}
class GitHubAPIError extends Error {
readonly status: number
constructor(message: string, status: number) {
super(message)
this.status = status
}
}
const event = JSON.parse(await readFile(eventPath, 'utf8')) as GitHubEvent
const sender = event.sender?.login ?? ''
const coderabbitAuthors = new Set(['coderabbitai[bot]', 'coderabbitai'])
if (!coderabbitAuthors.has(sender)) {
console.log(`Ignoring sender: ${sender}`)
process.exit(0)
}
let issueNumber: number | undefined
let text = ''
let shouldInspect = false
if (event.review && event.pull_request) {
issueNumber = event.pull_request.number
text = `${event.review.state ?? ''}\n${event.review.body ?? ''}`
shouldInspect = event.review.state === 'changes_requested'
} else if (event.comment && event.issue?.pull_request) {
issueNumber = event.issue.number
text = event.comment.body ?? ''
shouldInspect = true
}
if (!issueNumber || !shouldInspect) {
console.log('No actionable PR hygiene signal found.')
process.exit(0)
}
const hygieneFailed = text
.split('\n')
.some((line) => /\bPR Hygiene\b/i.test(line) && /(?:❌|\berror\b|\bfailed\b|\bfail\b)/i.test(line))
if (!hygieneFailed) {
console.log('CodeRabbit signal did not reference a failed PR Hygiene check.')
process.exit(0)
}
async function github<T>(path: string, options: RequestInit = {}): Promise<T | null> {
const response = await fetch(`https://api.github.com${path}`, {
...options,
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${token}`,
'X-GitHub-Api-Version': '2022-11-28',
...options.headers
}
})
if (!response.ok) {
const body = await response.text()
throw new GitHubAPIError(`${options.method ?? 'GET'} ${path} failed: ${response.status} ${body}`, response.status)
}
if (response.status === 204) return null
return response.json() as Promise<T>
}
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 invalid because CodeRabbit failed the PR Hygiene check. See `CONTRIBUTING.md` and the PR template before opening a new PR.'
})
})
await github<unknown>(`/repos/${owner}/${repo}/pulls/${issueNumber}`, {
method: 'PATCH',
body: JSON.stringify({ state: 'closed' })
})

42
.github/workflows/close-invalid-prs.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Close invalid PRs
on:
pull_request_review:
types: [submitted]
issue_comment:
types: [created]
concurrency:
group: close-invalid-prs-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: false
jobs:
# This job only reacts to trusted CodeRabbit review signals and then labels/closes PRs.
close-invalid:
name: Close PRs failed by PR Hygiene
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Check out trusted workflow scripts
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24
- name: Close PR when CodeRabbit fails PR Hygiene
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
if [ ! -f .github/scripts/close-invalid-prs.ts ]; then
echo "Trusted close-invalid helper is not present on the default branch yet."
exit 0
fi
node --experimental-strip-types .github/scripts/close-invalid-prs.ts

View file

@ -19,6 +19,14 @@ bun run tauri dev # Tauri desktop app with hot reload
APPLE_SIGNING_IDENTITY=- bun run tauri build -c '{"bundle": { "createUpdaterArtifacts": false }}'
```
## Pull requests
Pull requests must be written in English and must follow the PR template. The title and body should clearly explain what changed, why it changed, and how it was validated.
Do not submit placeholder PRs. Remove template comments before opening a PR. Do not leave dangling issue references such as `Fixes #`, `TODO`, `TBD`, or similar unfinished text.
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.
## Quality checks
Run all of these before submitting a PR: