chore: replace OSS weekend with permanent contribution gate
This commit is contained in:
292
.github/APPROVED_CONTRIBUTORS
vendored
292
.github/APPROVED_CONTRIBUTORS
vendored
@@ -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
|
||||
|
||||
118
.github/APPROVED_CONTRIBUTORS.vacation
vendored
118
.github/APPROVED_CONTRIBUTORS.vacation
vendored
@@ -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
|
||||
9
.github/ISSUE_TEMPLATE/bug.yml
vendored
9
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -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:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/contribution.yml
vendored
4
.github/ISSUE_TEMPLATE/contribution.yml
vendored
@@ -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
|
||||
|
||||
10
.github/oss-weekend.json
vendored
10
.github/oss-weekend.json
vendored
@@ -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"
|
||||
}
|
||||
137
.github/workflows/approve-contributor.yml
vendored
137
.github/workflows/approve-contributor.yml
vendored
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
26
.github/workflows/openclaw-gate.yml
vendored
26
.github/workflows/openclaw-gate.yml
vendored
@@ -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;
|
||||
}
|
||||
|
||||
52
.github/workflows/pr-gate.yml
vendored
52
.github/workflows/pr-gate.yml
vendored
@@ -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);
|
||||
|
||||
14
AGENTS.md
14
AGENTS.md
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
14
README.md
14
README.md
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user