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:
parent
33f53324f7
commit
280b51e666
22
.coderabbit.yaml
Normal file
22
.coderabbit.yaml
Normal 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
|
||||
21
.github/PULL_REQUEST_TEMPLATE.md
vendored
21
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -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
38
.github/copilot-instructions.md
vendored
Normal 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
140
.github/scripts/close-invalid-prs.ts
vendored
Normal 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
42
.github/workflows/close-invalid-prs.yml
vendored
Normal 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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue