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);