chore: add PR approval gate for new contributors

- Add APPROVED_CONTRIBUTORS file with existing contributors
- Add pr-gate.yml workflow to close PRs from unapproved contributors
- Add approve-contributor.yml workflow to approve via lgtm on issues
- Add CONTRIBUTING.md with guidelines
- Update README.md to point to CONTRIBUTING.md
This commit is contained in:
Mario Zechner 2026-01-18 12:26:45 +01:00
parent 632495338f
commit 3eded2c146
5 changed files with 321 additions and 64 deletions

84
.github/APPROVED_CONTRIBUTORS vendored Normal file
View file

@ -0,0 +1,84 @@
# GitHub handles of users approved to submit PRs
# One handle per line (without @)
# Add new contributors by commenting lgtm on their issue
aadishv
airtonix
aliou
aos
austinm911
banteg
ben-vargas
butelo
can1357
CarlosGtrz
cau1k
cmf
crcatala
Cursivez
cv
dannote
default-anton
dnouri
DronNick
enisdenjo
ferologics
fightbulc
ghoulr
gnattu
HACKE-RC
hewliyang
hjanuschka
iamd3vil
jblwilliams
joshp123
jsinge97
justram
kaofelix
kiliman
kim0
lockmeister
LukeFost
lukele
m-box-mr
marckrenn
markusylisiurunen
mcinteerj
melihmucuk
mitsuhiko
mrexodia
nathyong
nickseelert
nicobailon
ninlds
ogulcancelik
patrick-kidger
paulbettner
Perlence
pjtf93
prateekmedia
prathamdby
ribelo
richardgill
robinwander
ronyrus
roshanasingh4
scutifer
skuridin
steipete
svkozak
tallshort
theBucky
thomasmhr
tiagoefreitas
timolins
tmustier
tudoroancea
unexge
vaayne
VaclavSynacek
vsabavat
w-winter
Whamp
WismutHansen
XesGaDeus
yevhen

View file

@ -0,0 +1,101 @@
name: Approve Contributor
on:
issue_comment:
types: [created]
jobs:
approve:
if: !github.event.issue.pull_request
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
- name: Add contributor to approved list
id: update
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const core = require('@actions/core');
const issueAuthor = context.payload.issue.user.login;
const commenter = context.payload.comment.user.login;
const commentBody = context.payload.comment.body || '';
const approvedFile = '.github/APPROVED_CONTRIBUTORS';
if (!/^\s*lgtm\s*$/i.test(commentBody)) {
console.log('Comment does not match lgtm');
core.setOutput('status', 'skipped');
return;
}
try {
const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter
});
if (!['admin', 'write'].includes(permissionLevel.permission)) {
console.log(`${commenter} does not have write access`);
core.setOutput('status', 'skipped');
return;
}
} catch (error) {
console.log(`${commenter} does not have collaborator access`);
core.setOutput('status', 'skipped');
return;
}
let content = fs.readFileSync(approvedFile, 'utf8');
const approvedList = content
.split('\n')
.map(line => line.trim().toLowerCase())
.filter(line => line && !line.startsWith('#'));
if (approvedList.includes(issueAuthor.toLowerCase())) {
console.log(`${issueAuthor} is already approved`);
core.setOutput('status', 'already');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `@${issueAuthor} is already in the approved contributors list.`
});
return;
}
content = content.trimEnd() + '\n' + issueAuthor + '\n';
fs.writeFileSync(approvedFile, content);
console.log(`Added ${issueAuthor} to approved contributors`);
core.setOutput('status', 'added');
- name: Commit and push
if: steps.update.outputs.status == 'added'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .github/APPROVED_CONTRIBUTORS
git diff --staged --quiet || git commit -m "chore: approve contributor ${{ github.event.issue.user.login }}"
git push
- name: Comment on issue
if: steps.update.outputs.status == 'added'
uses: actions/github-script@v7
with:
script: |
const issueAuthor = context.payload.issue.user.login;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `@${issueAuthor} has been added to the approved contributors list. You can now submit PRs. Thanks for contributing!`
});

88
.github/workflows/pr-gate.yml vendored Normal file
View file

@ -0,0 +1,88 @@
name: PR Gate
on:
pull_request_target:
types: [opened]
jobs:
check-contributor:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Check if contributor is approved
uses: actions/github-script@v7
with:
script: |
const prAuthor = context.payload.pull_request.user.login;
const defaultBranch = context.payload.repository.default_branch;
// Skip bots
if (prAuthor.endsWith('[bot]') || prAuthor === 'dependabot[bot]') {
console.log(`Skipping bot: ${prAuthor}`);
return;
}
// Check if user is a collaborator (has write access)
try {
const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: prAuthor
});
if (['admin', 'write'].includes(permissionLevel.permission)) {
console.log(`${prAuthor} is a collaborator with ${permissionLevel.permission} access`);
return;
}
} catch (e) {
// User is not a collaborator, continue with check
}
// Fetch approved contributors list
const { data: fileContent } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/APPROVED_CONTRIBUTORS',
ref: defaultBranch
});
const content = Buffer.from(fileContent.content, 'base64').toString('utf8');
const approvedList = content
.split('\n')
.map(line => line.trim().toLowerCase())
.filter(line => line && !line.startsWith('#'));
if (approvedList.includes(prAuthor.toLowerCase())) {
console.log(`${prAuthor} is in the approved contributors list`);
return;
}
// Not approved - close PR with comment
console.log(`${prAuthor} is not approved, closing PR`);
const message = `Hi @${prAuthor}, thanks for your interest in contributing!
We ask new contributors to open an issue first before submitting a PR. This helps us discuss the approach and avoid wasted effort.
**Next steps:**
1. Open an issue describing what you want to change and why (keep it concise, write in your human voice, AI slop will be closed)
2. Once a maintainer approves with \`lgtm\`, you'll be added to the approved contributors list
3. Then you can submit your PR
This PR will be closed automatically. See https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md for more details.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: message
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
state: 'closed'
});

42
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,42 @@
# Contributing to pi
Thanks for wanting to contribute! This guide exists to save both of us time.
## The One Rule
**You must understand your code.** If you can't explain what your changes do and how they interact with the rest of the system, your PR will be closed.
Using AI to write code is fine. You can gain understanding by interrogating an agent with access to the codebase until you grasp all edge cases and effects of your changes. What's not fine is submitting agent-generated slop without that understanding.
If you use an agent, run it from the `pi-mono` root directory so it picks up `AGENTS.md` automatically. Your agent must follow the rules and guidelines in that file.
## First-Time Contributors
We use an approval gate for new contributors:
1. Open an issue describing what you want to change and why
2. Keep it concise (if it doesn't fit on one screen, it's too long)
3. Write in your own voice, at least for the intro
4. A maintainer will comment `lgtm` if approved
5. Once approved, you can submit PRs
This exists because AI makes it trivial to generate plausible-looking but low-quality contributions. The issue step lets us filter early.
## Before Submitting a PR
```bash
npm run check # must pass with no errors
./test.sh # must pass
```
Do not edit `CHANGELOG.md`. Changelog entries are added by maintainers.
If you're adding a new provider to `packages/ai`, see `AGENTS.md` for required tests.
## Philosophy
pi's core is minimal. If your feature doesn't belong in the core, it should be an extension. PRs that bloat the core will likely be rejected.
## Questions?
Open an issue or ask on [Discord](https://discord.gg/XdVrgtQAzd).

View file

@ -26,80 +26,22 @@ Tools for building AI agents and managing LLM deployments.
| **[@mariozechner/pi-web-ui](packages/web-ui)** | Web components for AI chat interfaces |
| **[@mariozechner/pi-pods](packages/pods)** | CLI for managing vLLM deployments on GPU pods |
## Development
## Contributing
### Setup
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [AGENTS.md](AGENTS.md) for project-specific rules (for both humans and agents).
## Development
```bash
npm install # Install all dependencies
npm run build # Build all packages
npm run check # Lint, format, and type check
./test.sh # Run tests (skips LLM-dependent tests without API keys)
./pi-test.sh # Run pi from sources (must be run from repo root)
```
> **Note:** `npm run check` requires `npm run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.
### CI
GitHub Actions runs on push to `main` and on pull requests. The workflow runs `npm run check` and `npm run test` for each package in parallel.
**Do not add LLM API keys as secrets to this repository.** Tests that require LLM access use `describe.skipIf()` to skip when API keys are missing. This is intentional:
- PRs from external contributors would have access to secrets in the CI environment
- Malicious PR code could exfiltrate API keys
- Tests that need LLM calls are skipped on CI and run locally by developers who have keys configured
If you need to run LLM-dependent tests, run them locally with your own API keys.
### Development
Start watch builds for all packages:
```bash
npm run dev
```
Then run with tsx:
```bash
cd packages/coding-agent && npx tsx src/cli.ts
cd packages/pods && npx tsx src/cli.ts
```
To run tests that don't require an LLM endpoint:
```bash
./test.sh
```
### Versioning (Lockstep)
**All packages MUST always have the same version number.** Use these commands to bump versions:
```bash
npm run version:patch # 0.7.5 -> 0.7.6
npm run version:minor # 0.7.5 -> 0.8.0
npm run version:major # 0.7.5 -> 1.0.0
```
These commands:
1. Update all package versions to the same number
2. Update inter-package dependency versions (e.g., `pi-agent` depends on `pi-ai@^0.7.7`)
3. Update `package-lock.json`
**Never manually edit version numbers.** The lockstep system ensures consistency across the monorepo.
### Publishing
```bash
npm run release:patch # Bug fixes
npm run release:minor # New features
npm run release:major # Breaking changes
```
This handles version bump, CHANGELOG updates, commit, tag, publish, and push.
**NPM Token Setup**: Requires a granular access token with "Bypass 2FA on publish" enabled.
- Go to https://www.npmjs.com/settings/badlogic/tokens/
- Create a new "Granular Access Token" with "Bypass 2FA on publish"
- Set the token: `npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN`
## License
MIT