chore: replace OSS weekend with permanent contribution gate

This commit is contained in:
Mario Zechner
2026-04-14 23:16:11 +02:00
parent 8f66938c80
commit d62d22173a
14 changed files with 424 additions and 753 deletions

View File

@@ -1,147 +1,147 @@
# GitHub handles of users approved to submit PRs
# GitHub handles approved to bypass contribution auto-close
# Format: <username> <capability>
# capability:
# issue future issues stay open
# pr future issues and PRs stay open
# One handle per line (without @)
# Add new contributors by commenting lgtm on their issue
barapa
alasano
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
badlogictest
terrorobe
zedrdave
mrud
toorusr
andresaraujo
lightningRalf
williballenthin
masonc15
4h9fbZ
haoqixu
Graffioh
charles-cooper
emanuelst
juanibiapina
liby
pasky
odysseus0
giuseppeg
michaelpersonal
academo
PriNova
semtexzv
jasonish
markusn
SamFold
Soleone
virtuald
NateSmyth
7Sageer
MatthieuBizien
sumeet
marchellodev
vedang
lucemia
mcollina
lajarre
smithbm2316
drewburr
gordonhwc
deybhayden
tintinweb
asoules
zhahaoyu
in0vik
jtac
yzhg1983
smcllns
dmmulroy
zmberber
andresvi94
bjesuiter
sudosubin
dyyz1993
Mic92
pmateusz
wirjo
jay-aye-see-kay
barapa pr
alasano pr
aadishv pr
airtonix pr
aliou pr
aos pr
austinm911 pr
banteg pr
ben-vargas pr
butelo pr
can1357 pr
CarlosGtrz pr
cau1k pr
cmf pr
crcatala pr
Cursivez pr
cv pr
dannote pr
default-anton pr
dnouri pr
DronNick pr
enisdenjo pr
ferologics pr
fightbulc pr
ghoulr pr
gnattu pr
HACKE-RC pr
hewliyang pr
hjanuschka pr
iamd3vil pr
jblwilliams pr
joshp123 pr
jsinge97 pr
justram pr
kaofelix pr
kiliman pr
kim0 pr
lockmeister pr
LukeFost pr
lukele pr
m-box-mr pr
marckrenn pr
markusylisiurunen pr
mcinteerj pr
melihmucuk pr
mitsuhiko pr
mrexodia pr
nathyong pr
nickseelert pr
nicobailon pr
ninlds pr
ogulcancelik pr
patrick-kidger pr
paulbettner pr
Perlence pr
pjtf93 pr
prateekmedia pr
prathamdby pr
ribelo pr
richardgill pr
robinwander pr
ronyrus pr
roshanasingh4 pr
scutifer pr
skuridin pr
steipete pr
svkozak pr
tallshort pr
theBucky pr
thomasmhr pr
tiagoefreitas pr
timolins pr
tmustier pr
tudoroancea pr
unexge pr
vaayne pr
VaclavSynacek pr
vsabavat pr
w-winter pr
Whamp pr
WismutHansen pr
XesGaDeus pr
yevhen pr
badlogictest pr
terrorobe pr
zedrdave pr
mrud pr
toorusr pr
andresaraujo pr
lightningRalf pr
williballenthin pr
masonc15 pr
4h9fbZ pr
haoqixu pr
Graffioh pr
charles-cooper pr
emanuelst pr
juanibiapina pr
liby pr
pasky pr
odysseus0 pr
giuseppeg pr
michaelpersonal pr
academo pr
PriNova pr
semtexzv pr
jasonish pr
markusn pr
SamFold pr
Soleone pr
virtuald pr
NateSmyth pr
7Sageer pr
MatthieuBizien pr
sumeet pr
marchellodev pr
vedang pr
lucemia pr
mcollina pr
lajarre pr
smithbm2316 pr
drewburr pr
gordonhwc pr
deybhayden pr
tintinweb pr
asoules pr
zhahaoyu pr
in0vik pr
jtac pr
yzhg1983 pr
smcllns pr
dmmulroy pr
zmberber pr
andresvi94 pr
bjesuiter pr
sudosubin pr
dyyz1993 pr
Mic92 pr
pmateusz pr
wirjo pr
jay-aye-see-kay pr

View File

@@ -1,118 +0,0 @@
# 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
badlogictest
terrorobe
zedrdave
mrud
toorusr
andresaraujo
lightningRalf
williballenthin
masonc15
4h9fbZ
haoqixu
Graffioh
charles-cooper
emanuelst
juanibiapina
liby
pasky
odysseus0
giuseppeg
michaelpersonal
academo
PriNova
semtexzv
jasonish
markusn
SamFold
Soleone
virtuald
NateSmyth
7Sageer
MatthieuBizien
sumeet
marchellodev
vedang

View File

@@ -2,6 +2,15 @@ name: Bug Report
description: Report something that's broken
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
**Before you start:** Read [CONTRIBUTING.md](https://github.com/badlogic/pi-mono/blob/main/CONTRIBUTING.md).
New issues from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/badlogic/pi-mono/blob/main/CONTRIBUTING.md) will not be reopened or receive a reply.
Keep this short. If it doesn't fit on one screen, it's too long. Write in your own voice.
- type: textarea
id: description
attributes:

View File

@@ -6,7 +6,9 @@ body:
attributes:
value: |
**Before you start:** Read [CONTRIBUTING.md](https://github.com/badlogic/pi-mono/blob/main/CONTRIBUTING.md).
New issues from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/badlogic/pi-mono/blob/main/CONTRIBUTING.md) will not be reopened or receive a reply.
Keep this short. If it doesn't fit on one screen, it's too long. Write in your own voice.
- type: textarea

View File

@@ -1,10 +0,0 @@
{
"active": true,
"mode": "weekend",
"startsAt": "2026-04-13T20:58:48.644Z",
"startsAtText": "Monday, April 13, 2026",
"reopensOn": "2026-04-20",
"reopensOnText": "Monday, April 20, 2026",
"reason": null,
"discordUrl": "https://discord.com/invite/3cU7Bz4UPx"
}

View File

@@ -17,20 +17,26 @@ jobs:
with:
ref: ${{ github.event.repository.default_branch }}
- name: Add contributor to approved list
- name: Update contributor approval
id: update
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS';
const VALID_CAPABILITIES = new Set(['issue', 'pr']);
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';
const commentBody = (context.payload.comment.body || '').trim();
if (!/^\s*lgtm\b/i.test(commentBody)) {
console.log('Comment does not match lgtm');
let targetCapability;
if (/\blgtmi\b/i.test(commentBody)) {
targetCapability = 'issue';
} else if (/\blgtm\b/i.test(commentBody)) {
targetCapability = 'pr';
} else {
console.log('Comment does not match lgtm or lgtmi');
core.setOutput('status', 'skipped');
return;
}
@@ -39,46 +45,89 @@ jobs:
const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter
username: commenter,
});
if (!['admin', 'write'].includes(permissionLevel.permission)) {
if (!['admin', 'maintain', 'write'].includes(permissionLevel.permission)) {
console.log(`${commenter} does not have write access`);
core.setOutput('status', 'skipped');
return;
}
} catch (error) {
} catch {
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('#'));
function parseApprovedUsers(content) {
const lines = content.split('\n');
const entries = [];
const users = new Map();
if (approvedList.includes(issueAuthor.toLowerCase())) {
console.log(`${issueAuthor} is already approved`);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
entries.push({ type: 'other', line });
continue;
}
const parts = trimmed.split(/\s+/);
if (parts.length !== 2) {
console.log(`Skipping malformed line: ${line}`);
entries.push({ type: 'other', line });
continue;
}
const [username, capability] = parts;
const normalizedCapability = capability.toLowerCase();
if (!VALID_CAPABILITIES.has(normalizedCapability)) {
console.log(`Skipping line with invalid capability: ${line}`);
entries.push({ type: 'other', line });
continue;
}
const normalizedUser = username.toLowerCase();
const entry = { type: 'user', username, normalizedUser, capability: normalizedCapability };
entries.push(entry);
users.set(normalizedUser, entry);
}
return { entries, users };
}
function stringifyApprovedUsers(entries) {
return `${entries
.map((entry) => (entry.type === 'user' ? `${entry.username} ${entry.capability}` : entry.line))
.join('\n')
.replace(/\n+$/g, '')}\n`;
}
const content = fs.readFileSync(APPROVED_FILE, 'utf8');
const { entries, users } = parseApprovedUsers(content);
const normalizedAuthor = issueAuthor.toLowerCase();
const existingEntry = users.get(normalizedAuthor);
const existingCapability = existingEntry?.capability ?? null;
if (existingCapability === 'pr' || existingCapability === targetCapability) {
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.`
});
core.setOutput('capability', existingCapability);
console.log(`${issueAuthor} is already approved for ${existingCapability}`);
return;
}
content = content.trimEnd() + '\n' + issueAuthor + '\n';
fs.writeFileSync(approvedFile, content);
if (existingEntry) {
existingEntry.capability = targetCapability;
} else {
entries.push({ type: 'user', username: issueAuthor, normalizedUser: normalizedAuthor, capability: targetCapability });
}
console.log(`Added ${issueAuthor} to approved contributors`);
core.setOutput('status', 'added');
fs.writeFileSync(APPROVED_FILE, stringifyApprovedUsers(entries));
core.setOutput('status', existingCapability ? 'updated' : 'added');
core.setOutput('capability', targetCapability);
console.log(`Set ${issueAuthor} capability to ${targetCapability}`);
- name: Commit and push
if: steps.update.outputs.status == 'added'
if: steps.update.outputs.status == 'added' || steps.update.outputs.status == 'updated'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
@@ -87,14 +136,46 @@ jobs:
git push
- name: Comment on issue
if: steps.update.outputs.status == 'added'
if: steps.update.outputs.status == 'added' || steps.update.outputs.status == 'updated' || steps.update.outputs.status == 'already'
uses: actions/github-script@v7
with:
script: |
const issueAuthor = context.payload.issue.user.login;
const capability = '${{ steps.update.outputs.capability }}';
const defaultBranch = context.payload.repository.default_branch;
let body;
if ('${{ steps.update.outputs.status }}' === 'already') {
body = `@${issueAuthor} is already approved.`;
} else if (capability === 'issue') {
body = [
`@${issueAuthor} approved for issues. Your future issues will not be auto-closed. PRs still require \`lgtm\`.`,
'',
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
].join('\n');
} else {
body = [
`@${issueAuthor} approved for issues and PRs. Your future issues and PRs will not be auto-closed.`,
'',
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
].join('\n');
}
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!`
body,
});
- name: Close issue after PR approval
if: (steps.update.outputs.status == 'added' || steps.update.outputs.status == 'updated') && steps.update.outputs.capability == 'pr'
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed',
});

View File

@@ -1,20 +1,22 @@
name: OSS Weekend Issues
name: Issue Gate
on:
issues:
types: [opened]
jobs:
close-issues-during-weekend:
check-contributor:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Close new issues during OSS weekend
- name: Check issue author
uses: actions/github-script@v7
with:
script: |
const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS';
const VALID_CAPABILITIES = new Set(['issue', 'pr']);
const issueAuthor = context.payload.issue.user.login;
const defaultBranch = context.payload.repository.default_branch;
@@ -51,50 +53,55 @@ jobs:
return Buffer.from(fileContent.content, 'base64').toString('utf8');
}
function parseApprovedUsers(content) {
const users = new Map();
for (const rawLine of content.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const parts = line.split(/\s+/);
if (parts.length !== 2) {
console.log(`Skipping malformed line: ${rawLine}`);
continue;
}
const [username, capability] = parts;
const normalizedCapability = capability.toLowerCase();
if (!VALID_CAPABILITIES.has(normalizedCapability)) {
console.log(`Skipping line with invalid capability: ${rawLine}`);
continue;
}
users.set(username.toLowerCase(), normalizedCapability);
}
return users;
}
const permission = await getPermission(issueAuthor);
if (['admin', 'maintain', 'write'].includes(permission)) {
console.log(`${issueAuthor} is a collaborator with ${permission} access`);
return;
}
const approvedContent = await getTextFile('.github/APPROVED_CONTRIBUTORS');
const approvedList = approvedContent
.split('\n')
.map(line => line.trim().toLowerCase())
.filter(line => line && !line.startsWith('#'));
const isApprovedContributor = approvedList.includes(issueAuthor.toLowerCase());
const approvedContent = await getTextFile(APPROVED_FILE);
const approvedUsers = parseApprovedUsers(approvedContent);
const capability = approvedUsers.get(issueAuthor.toLowerCase());
let weekendState;
try {
weekendState = JSON.parse(await getTextFile('.github/oss-weekend.json'));
} catch (error) {
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
console.log('OSS weekend is not active');
return;
}
throw error;
}
if (!weekendState?.active) {
console.log('OSS weekend is not active');
if (capability === 'issue' || capability === 'pr') {
console.log(`${issueAuthor} is approved for ${capability}`);
return;
}
if (isApprovedContributor) {
console.log(`${issueAuthor} is in the approved contributors list`);
return;
}
const reopenDate = weekendState.reopensOnText || weekendState.reopensOn || 'after the weekend';
const discordUrl = weekendState.discordUrl || 'https://discord.com/invite/3cU7Bz4UPx';
const reason = typeof weekendState.reason === 'string' && weekendState.reason.trim() ? weekendState.reason.trim() : null;
const message = [
`Hi @${issueAuthor}, thanks for opening an issue.`,
'This issue was auto-closed. All issues from new contributors are auto-closed by default.',
'',
`OSS weekend is active until ${reopenDate}, so new issues from unapproved contributors are being auto-closed for now.`,
...(reason ? ['', `Current focus: ${reason}`] : []),
`Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`,
'',
`Please reopen or submit this issue again after ${reopenDate}. For support, join [Discord](${discordUrl}).`,
'If a maintainer replies `lgtmi` on one of your issues, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.',
'',
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
].join('\n');
await github.rest.issues.createComment({

View File

@@ -32,6 +32,9 @@ jobs:
return;
}
const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS';
const VALID_CAPABILITIES = new Set(['issue', 'pr']);
// --- Check APPROVED_CONTRIBUTORS ---
async function getTextFile(path) {
const { data } = await github.rest.repos.getContent({
@@ -47,12 +50,23 @@ jobs:
}
try {
const content = await getTextFile('.github/APPROVED_CONTRIBUTORS');
const approved = content
.split('\n')
.map(l => l.trim().toLowerCase())
.filter(l => l && !l.startsWith('#'));
if (approved.includes(author.toLowerCase())) {
const content = await getTextFile(APPROVED_FILE);
const approved = new Map();
for (const rawLine of content.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const parts = line.split(/\s+/);
if (parts.length !== 2) continue;
const [username, capability] = parts;
const normalizedCapability = capability.toLowerCase();
if (!VALID_CAPABILITIES.has(normalizedCapability)) continue;
approved.set(username.toLowerCase(), normalizedCapability);
}
if (approved.has(author.toLowerCase())) {
console.log(`${author} is in APPROVED_CONTRIBUTORS, passing`);
return;
}

View File

@@ -16,6 +16,8 @@ jobs:
uses: actions/github-script@v7
with:
script: |
const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS';
const VALID_CAPABILITIES = new Set(['issue', 'pr']);
const prAuthor = context.payload.pull_request.user.login;
const defaultBranch = context.payload.repository.default_branch;
@@ -52,6 +54,32 @@ jobs:
return Buffer.from(fileContent.content, 'base64').toString('utf8');
}
function parseApprovedUsers(content) {
const users = new Map();
for (const rawLine of content.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const parts = line.split(/\s+/);
if (parts.length !== 2) {
console.log(`Skipping malformed line: ${rawLine}`);
continue;
}
const [username, capability] = parts;
const normalizedCapability = capability.toLowerCase();
if (!VALID_CAPABILITIES.has(normalizedCapability)) {
console.log(`Skipping line with invalid capability: ${rawLine}`);
continue;
}
users.set(username.toLowerCase(), normalizedCapability);
}
return users;
}
async function closePullRequest(message) {
await github.rest.issues.createComment({
owner: context.repo.owner,
@@ -74,31 +102,25 @@ jobs:
return;
}
const approvedContent = await getTextFile('.github/APPROVED_CONTRIBUTORS');
const approvedList = approvedContent
.split('\n')
.map(line => line.trim().toLowerCase())
.filter(line => line && !line.startsWith('#'));
const isApprovedContributor = approvedList.includes(prAuthor.toLowerCase());
const approvedContent = await getTextFile(APPROVED_FILE);
const approvedUsers = parseApprovedUsers(approvedContent);
const capability = approvedUsers.get(prAuthor.toLowerCase());
if (isApprovedContributor) {
console.log(`${prAuthor} is in the approved contributors list`);
if (capability === 'pr') {
console.log(`${prAuthor} is approved for PRs`);
return;
}
console.log(`${prAuthor} is not approved, closing PR`);
const message = [
`Hi @${prAuthor}, thanks for your interest in contributing!`,
'This PR was auto-closed. Only contributors approved with `lgtm` can open PRs. Open an issue first.',
'',
'We ask new contributors to open an issue first before submitting a PR. This helps us discuss the approach and avoid wasted effort.',
`Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`,
'',
'**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',
'If a maintainer replies `lgtmi`, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.',
'',
`This PR will be closed automatically. See https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md for more details.`,
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
].join('\n');
await closePullRequest(message);

View File

@@ -40,12 +40,14 @@ When reading issues:
gh issue view <number> --json title,body,comments,labels,state
```
## OSS Weekend
- If the user says `enable OSS weekend mode until X`, run `node scripts/oss-weekend.mjs --mode=close --end-date=YYYY-MM-DD --git` with the requested end date
- If the user says `end OSS weekend mode`, run `node scripts/oss-weekend.mjs --mode=open --git`
- The script updates `README.md`, `packages/coding-agent/README.md`, and `.github/oss-weekend.json`
- With `--git`, the script stages only those OSS weekend files, commits them, and pushes them
- During OSS weekend, `.github/workflows/oss-weekend-issues.yml` auto-closes new issues from non-maintainers, and `.github/workflows/pr-gate.yml` auto-closes PRs from approved non-maintainers with the weekend message
## Contribution Gate
- New issues from new contributors are auto-closed by `.github/workflows/issue-gate.yml`
- New PRs from new contributors without PR rights are auto-closed by `.github/workflows/pr-gate.yml`
- Maintainer approval comments are handled by `.github/workflows/approve-contributor.yml`
- Maintainers review auto-closed issues daily
- Issues that do not meet the quality bar in `CONTRIBUTING.md` are not reopened and do not receive a reply
- `lgtmi` approves future issues
- `lgtm` approves future issues and rights to submit PRs
When creating issues:
- Add `pkg:*` labels to indicate which package(s) the issue affects

View File

@@ -1,42 +1,69 @@
# Contributing to pi
Thanks for wanting to contribute! This guide exists to save both of us time.
This guide exists to save both sides 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.
**You must understand your code.** If you cannot 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.
Using AI to write code is fine. Submitting AI-generated slop without understanding it is not.
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
## Contribution Gate
We use an approval gate for new contributors:
All issues and PRs from new contributors are auto-closed by default.
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
Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar below will not be reopened or receive a reply.
This exists because AI makes it trivial to generate plausible-looking but low-quality contributions. The issue step lets us filter early.
Approval happens through maintainer replies on issues:
- `lgtmi`: your future issues will not be auto-closed
- `lgtm`: your future issues and PRs will not be auto-closed
`lgtmi` does not grant rights to submit PRs. Only `lgtm` grants rights to submit PRs.
## Quality Bar For Issues
If you open an issue, you must use one of the two GitHub issue templates.
If you open an issue, keep it short, concrete, and worth reading.
- Keep it concise. If it does not fit on one screen, it is too long.
- Write in your own voice.
- State the bug or request clearly.
- Explain why it matters.
- If you want to implement the change yourself, say so.
If the issue is real and written well, a maintainer may reopen it, reply `lgtmi`, or reply `lgtm`.
## Blocking
If you ignore this document twice, or if you spam the tracker with agent-generated issues, your GitHub account will be permanently blocked.
If you send a large volume of issues through automation, your GitHub account will be permanently blocked. No taksies backsies.
## Before Submitting a PR
Do not open a PR unless you have already been approved with `lgtm`.
Before submitting a PR:
```bash
npm run check # must pass with no errors
./test.sh # must pass
npm run check
./test.sh
```
Both 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.
If you are 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.
pi's core is minimal. If your feature does not 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.com/invite/nKXTsAcmbT).
Ask on [Discord](https://discord.com/invite/nKXTsAcmbT).

View File

@@ -1,13 +1,3 @@
<!-- OSS_WEEKEND_START -->
# 🏖️ OSS Weekend
**Issue tracker reopens Monday, April 20, 2026.**
OSS weekend runs Monday, April 13, 2026 through Monday, April 20, 2026. New issues and PRs from unapproved contributors are auto-closed during this time. Approved contributors can still open issues and PRs if something is genuinely urgent, but please keep that to pressing matters only. For support, join [Discord](https://discord.com/invite/3cU7Bz4UPx).
<!-- OSS_WEEKEND_END -->
---
<p align="center">
<a href="https://shittycodingagent.ai">
<img src="https://shittycodingagent.ai/logo.svg" alt="pi logo" width="128">
@@ -23,6 +13,10 @@ OSS weekend runs Monday, April 13, 2026 through Monday, April 20, 2026. New issu
<a href="https://exe.dev"><img src="packages/coding-agent/docs/images/exy.png" alt="Exy mascot" width="48" /><br />exe.dev</a>
</p>
> New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](CONTRIBUTING.md).
---
# Pi Monorepo
> **Looking for the pi coding agent?** See **[packages/coding-agent](packages/coding-agent)** for installation and usage.

View File

@@ -1,13 +1,3 @@
<!-- OSS_WEEKEND_START -->
# 🏖️ OSS Weekend
**Issue tracker reopens Monday, April 20, 2026.**
OSS weekend runs Monday, April 13, 2026 through Monday, April 20, 2026. New issues and PRs from unapproved contributors are auto-closed during this time. Approved contributors can still open issues and PRs if something is genuinely urgent, but please keep that to pressing matters only. For support, join [Discord](https://discord.com/invite/3cU7Bz4UPx).
<!-- OSS_WEEKEND_END -->
---
<p align="center">
<a href="https://shittycodingagent.ai">
<img src="https://shittycodingagent.ai/logo.svg" alt="pi logo" width="128">
@@ -24,6 +14,10 @@ OSS weekend runs Monday, April 13, 2026 through Monday, April 20, 2026. New issu
<a href="https://exe.dev"><img src="docs/images/exy.png" alt="Exy mascot" width="48" /><br />exe.dev</a>
</p>
> New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](../../CONTRIBUTING.md).
---
Pi is a minimal terminal coding harness. Adapt pi to your workflows, not the other way around, without having to fork and modify pi internals. Extend it with TypeScript [Extensions](#extensions), [Skills](#skills), [Prompt Templates](#prompt-templates), and [Themes](#themes). Put your extensions, skills, prompt templates, and themes in [Pi Packages](#pi-packages) and share them with others via npm or git.
Pi ships with powerful defaults but skips features like sub agents and plan mode. Instead, you can ask pi to build what you want or install a third party pi package that matches your workflow.

View File

@@ -1,353 +0,0 @@
import { execFileSync } from "node:child_process";
import { readFile, rm, writeFile } from "node:fs/promises";
import process from "node:process";
const TIME_ZONE = "Europe/Berlin";
const DEFAULT_README_PATHS = ["README.md", "packages/coding-agent/README.md"];
const DEFAULT_STATE_PATH = ".github/oss-weekend.json";
const MARKER_START = "<!-- OSS_WEEKEND_START -->";
const MARKER_END = "<!-- OSS_WEEKEND_END -->";
const DISCORD_URL = "https://discord.com/invite/3cU7Bz4UPx";
function normalizeReason(reason) {
const trimmedReason = reason.trim();
return trimmedReason ? trimmedReason : null;
}
function parseArgs(argv) {
const options = {};
for (const arg of argv) {
if (!arg.startsWith("--")) continue;
const trimmedArg = arg.slice(2);
const separatorIndex = trimmedArg.indexOf("=");
if (separatorIndex === -1) {
options[trimmedArg] = "true";
continue;
}
const key = trimmedArg.slice(0, separatorIndex);
const value = trimmedArg.slice(separatorIndex + 1);
options[key] = value;
}
return options;
}
function getOption(name, cliOptions, envName, fallback) {
const cliValue = cliOptions[name];
if (cliValue !== undefined) return cliValue;
const envValue = process.env[envName];
if (envValue !== undefined && envValue !== "") return envValue;
return fallback;
}
function isTruthy(value) {
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function formatLongDate(date) {
return new Intl.DateTimeFormat("en-US", {
timeZone: TIME_ZONE,
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
}).format(date);
}
function parseDateInput(value) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) {
throw new Error(`Invalid end date: ${value}. Use YYYY-MM-DD.`);
}
const [, year, month, day] = match;
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 12, 0, 0));
}
function buildBanner(now, endDate, reason) {
const startDate = formatLongDate(now);
const reopenDate = formatLongDate(endDate);
const normalizedReason = normalizeReason(reason ?? "");
return [
MARKER_START,
"# 🏖️ OSS Weekend",
"",
`**Issue tracker reopens ${reopenDate}.**`,
"",
`OSS weekend runs ${startDate} through ${reopenDate}. New issues and PRs from unapproved contributors are auto-closed during this time. Approved contributors can still open issues and PRs if something is genuinely urgent, but please keep that to pressing matters only. For support, join [Discord](${DISCORD_URL}).`,
...(normalizedReason ? ["", `> _Current focus: ${normalizedReason}_`] : []),
MARKER_END,
"",
"---",
"",
"",
].join("\n");
}
function upsertBanner(readme, now, endDate, reason) {
const banner = buildBanner(now, endDate, reason);
const bannerPattern = new RegExp(
`${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n\\n---\\n\\n?`,
"m",
);
if (bannerPattern.test(readme)) {
return readme.replace(bannerPattern, banner);
}
return `${banner}${readme}`;
}
function removeBanner(readme) {
const bannerPattern = new RegExp(
`^${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n\\n---\\n\\n?`,
"m",
);
return readme.replace(bannerPattern, "");
}
function parseReadmePaths(cliOptions) {
const readmeOption = getOption("readme", cliOptions, "OSS_WEEKEND_README_PATH", "");
if (!readmeOption) return DEFAULT_README_PATHS;
return readmeOption
.split(",")
.map((path) => path.trim())
.filter(Boolean);
}
function buildState(now, endDateInput, endDate, reason) {
const normalizedReason = normalizeReason(reason ?? "");
return JSON.stringify(
{
active: true,
mode: "weekend",
startsAt: now.toISOString(),
startsAtText: formatLongDate(now),
reopensOn: endDateInput,
reopensOnText: formatLongDate(endDate),
reason: normalizedReason,
discordUrl: DISCORD_URL,
},
null,
2,
);
}
async function readOptionalFile(path) {
try {
return await readFile(path, "utf8");
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return null;
}
throw error;
}
}
function runCommand(command, args, options = {}) {
return execFileSync(command, args, { encoding: "utf8", ...options });
}
function quoteArg(arg) {
return /[^A-Za-z0-9_./:=@-]/.test(arg) ? JSON.stringify(arg) : arg;
}
function formatCommand(command, args) {
return [command, ...args].map(quoteArg).join(" ");
}
function hasStagedChanges(paths) {
try {
runCommand("git", ["diff", "--cached", "--quiet", "--", ...paths], { stdio: "ignore" });
return false;
} catch {
return true;
}
}
function runGitOperations(mode, paths, dryRun) {
const commitMessage = mode === "close" ? "docs: enable OSS weekend" : "docs: disable OSS weekend";
const addArgs = ["add", "--", ...paths];
const pushArgs = ["push"];
const commands = [formatCommand("git", addArgs)];
if (dryRun) {
commands.push(`git commit -m ${quoteArg(commitMessage)}`);
commands.push(formatCommand("git", pushArgs));
return {
commitMessage,
commands,
committed: false,
pushed: false,
stagedChanges: false,
};
}
runCommand("git", addArgs, { stdio: "inherit" });
if (!hasStagedChanges(paths)) {
return {
commitMessage,
commands,
committed: false,
pushed: false,
stagedChanges: false,
};
}
const commitArgs = ["commit", "-m", commitMessage];
commands.push(formatCommand("git", commitArgs));
runCommand("git", commitArgs, { stdio: "inherit" });
commands.push(formatCommand("git", pushArgs));
runCommand("git", pushArgs, { stdio: "inherit" });
return {
commitMessage,
commands,
committed: true,
pushed: true,
stagedChanges: true,
};
}
function printUsage() {
process.stdout.write(
[
"Usage:",
" node scripts/oss-weekend.mjs --mode=close --end-date=2026-03-23",
" node scripts/oss-weekend.mjs --mode=close --end-date=2026-03-23 --git",
" node scripts/oss-weekend.mjs --mode=open",
" node scripts/oss-weekend.mjs --mode=open --git",
"",
"Options:",
" --mode=close|open Required. close enables OSS weekend mode. open disables it.",
" --end-date=YYYY-MM-DD Required for --mode=close.",
" --readme=PATHS Optional comma-separated README paths. Defaults to README.md,packages/coding-agent/README.md.",
" --state=PATH Optional state file path. Defaults to .github/oss-weekend.json.",
" --reason=TEXT Optional reason shown in the README banner and weekend auto-close comments.",
" --git Stage only the OSS weekend files, commit, and push after updating them.",
" --dry-run Preview without editing files or running git operations.",
" --now=ISO Optional current timestamp override for testing.",
" --help Show this message.",
"",
].join("\n"),
);
}
async function main() {
const cliOptions = parseArgs(process.argv.slice(2));
if (isTruthy(cliOptions.help ?? "false")) {
printUsage();
return;
}
const mode = getOption("mode", cliOptions, "OSS_WEEKEND_MODE", "");
if (mode !== "close" && mode !== "open") {
throw new Error("--mode must be close or open.");
}
const dryRun = isTruthy(getOption("dry-run", cliOptions, "OSS_WEEKEND_DRY_RUN", "false"));
const runGit = isTruthy(getOption("git", cliOptions, "OSS_WEEKEND_GIT", "false"));
const nowInput = getOption("now", cliOptions, "OSS_WEEKEND_NOW", "");
const readmePaths = parseReadmePaths(cliOptions);
const statePath = getOption("state", cliOptions, "OSS_WEEKEND_STATE_PATH", DEFAULT_STATE_PATH);
const endDateInput = getOption("end-date", cliOptions, "OSS_WEEKEND_END_DATE", "");
const reason = normalizeReason(getOption("reason", cliOptions, "OSS_WEEKEND_REASON", ""));
const now = nowInput ? new Date(nowInput) : new Date();
if (Number.isNaN(now.getTime())) {
throw new Error(`Invalid date: ${nowInput}`);
}
if (mode === "close" && !endDateInput) {
throw new Error("--end-date is required when --mode=close.");
}
const endDate = mode === "close" ? parseDateInput(endDateInput) : null;
const readmeResults = [];
for (const readmePath of readmePaths) {
const currentReadme = await readFile(readmePath, "utf8");
const nextReadme = mode === "close" ? upsertBanner(currentReadme, now, endDate, reason) : removeBanner(currentReadme);
const changed = nextReadme !== currentReadme;
if (changed && !dryRun) {
await writeFile(readmePath, nextReadme, "utf8");
}
readmeResults.push({ path: readmePath, changed });
}
const currentState = await readOptionalFile(statePath);
const nextState = mode === "close" ? buildState(now, endDateInput, endDate, reason) : null;
const stateChanged = mode === "close" ? currentState !== nextState : currentState !== null;
if (!dryRun) {
if (mode === "close") {
await writeFile(statePath, `${nextState}\n`, "utf8");
} else {
await rm(statePath, { force: true });
}
}
const gitPaths = [...readmePaths, statePath];
const gitResult = runGit ? runGitOperations(mode, gitPaths, dryRun) : null;
const output = {
mode,
dry_run: dryRun ? "true" : "false",
weekend_active: mode === "close" ? "true" : "false",
readme_paths: readmeResults.map((result) => result.path).join(","),
readme_changed: readmeResults.some((result) => result.changed) ? "true" : "false",
readme_changed_paths: readmeResults
.filter((result) => result.changed)
.map((result) => result.path)
.join(","),
state_path: statePath,
state_changed: stateChanged ? "true" : "false",
git_enabled: runGit ? "true" : "false",
git_paths: gitPaths.join(","),
git_commit_message: gitResult?.commitMessage ?? "",
git_committed: gitResult?.committed ? "true" : "false",
git_pushed: gitResult?.pushed ? "true" : "false",
git_commands: gitResult ? gitResult.commands.join(" && ") : "",
end_date: endDate ? endDateInput : "",
end_date_text: endDate ? formatLongDate(endDate) : "",
reason: reason ?? "",
now_utc: now.toISOString(),
now_berlin: new Intl.DateTimeFormat("sv-SE", {
timeZone: TIME_ZONE,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
}).format(now),
};
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
printUsage();
process.exit(1);
});