Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

120 lines
3.4 KiB
JavaScript

/**
* Backfill contributor:{github_id} KV keys from merged PR history.
*
* Usage:
* node --env-file=.env scripts/backfill-contributors.mjs
*
* Requires: GITHUB_OWNER_LOGIN, and a GITHUB_TOKEN with repo read access
* (or no token for public repos, but you'll hit rate limits fast).
*
* Uses wrangler kv:key put under the hood, so wrangler must be available.
*/
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
const owner = "emdash-cms";
const repo = "emdash";
const ownerLogin = process.env.GITHUB_OWNER_LOGIN || "ascorbic";
const token = process.env.GITHUB_TOKEN;
// Read namespace ID from wrangler.jsonc to avoid hardcoding
const wranglerConfig = JSON.parse(
readFileSync(new URL("../wrangler.jsonc", import.meta.url), "utf-8")
.replace(/\/\/.*$/gm, "") // strip line comments
.replace(/\/\*[\s\S]*?\*\//g, "") // strip block comments
.replace(/,\s*([}\]])/g, "$1"), // strip trailing commas
);
const namespaceId = wranglerConfig.kv_namespaces?.[0]?.id;
if (!namespaceId) {
console.error("Could not find KV namespace ID in wrangler.jsonc");
process.exit(1);
}
const headers = {
Accept: "application/vnd.github+json",
"User-Agent": "emdash-discord-bot-backfill",
"X-GitHub-Api-Version": "2022-11-28",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const contributors = new Map(); // github_id -> github_login
let page = 1;
let hasMore = true;
console.log(`Fetching merged PRs from ${owner}/${repo}...`);
while (hasMore) {
const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=${page}`;
const res = await fetch(url, { headers });
if (!res.ok) {
console.error(`GitHub API error (${res.status}): ${await res.text()}`);
process.exit(1);
}
const prs = await res.json();
if (prs.length === 0) {
hasMore = false;
break;
}
for (const pr of prs) {
if (!pr.merged_at) continue;
const login = pr.user.login;
const id = pr.user.id;
// Skip owner and bots
if (login === ownerLogin || login.endsWith("[bot]")) continue;
if (!contributors.has(id)) {
contributors.set(id, login);
}
}
console.log(` Page ${page}: ${prs.length} PRs, ${contributors.size} unique contributors so far`);
page++;
// Respect rate limits
const remaining = res.headers.get("x-ratelimit-remaining");
if (remaining === "0") {
const reset = Number(res.headers.get("x-ratelimit-reset")) * 1000;
const wait = Math.max(0, reset - Date.now()) + 1000;
console.log(` Rate limited, waiting ${Math.ceil(wait / 1000)}s...`);
await new Promise((r) => setTimeout(r, wait));
}
}
console.log(`\nFound ${contributors.size} unique contributors. Writing to KV...\n`);
// Validate inputs before passing to execFileSync (defense in depth)
const idPattern = /^[0-9]+$/;
const loginPattern = /^[a-zA-Z0-9_-]+$/;
let written = 0;
for (const [id, login] of contributors) {
if (!idPattern.test(String(id)) || !loginPattern.test(login)) {
console.error(` Skipping invalid contributor: id=${id}, login=${login}`);
continue;
}
const key = `contributor:${id}`;
try {
execFileSync(
"pnpm",
["wrangler", "kv", "key", "put", "--remote", `--namespace-id=${namespaceId}`, key, login],
{ stdio: "pipe" },
);
written++;
if (written % 10 === 0) {
console.log(` ${written}/${contributors.size} written`);
}
} catch (err) {
console.error(` Failed to write ${key}: ${err.message}`);
}
}
console.log(`\nDone. Wrote ${written} contributor keys to KV.`);