chore: add OSS weekend gating

This commit is contained in:
Mario Zechner
2026-03-14 16:28:02 +01:00
parent 78d184447e
commit 572876be1e
5 changed files with 437 additions and 346 deletions

259
scripts/oss-weekend.mjs Normal file
View File

@@ -0,0 +1,259 @@
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 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) {
const startDate = formatLongDate(now);
const reopenDate = formatLongDate(endDate);
return [
MARKER_START,
"# 🏖️ OSS Weekend",
"",
`**Issue tracker reopens ${reopenDate}.**`,
"",
`OSS weekend runs ${startDate} through ${reopenDate}. New issues are auto-closed during this time. For support, join [Discord](${DISCORD_URL}).`,
MARKER_END,
"",
"---",
"",
"",
].join("\n");
}
function upsertBanner(readme, now, endDate) {
const banner = buildBanner(now, endDate);
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) {
return JSON.stringify(
{
active: true,
mode: "weekend",
startsAt: now.toISOString(),
startsAtText: formatLongDate(now),
reopensOn: endDateInput,
reopensOnText: formatLongDate(endDate),
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 printUsage() {
process.stdout.write(
[
"Usage:",
" node scripts/oss-weekend.mjs --mode=close --end-date=2026-03-23",
" node scripts/oss-weekend.mjs --mode=open",
"",
"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.",
" --dry-run Preview without editing files.",
" --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 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 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) : 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) : 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 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",
end_date: endDate ? endDateInput : "",
end_date_text: endDate ? formatLongDate(endDate) : "",
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);
});