mirror of
https://github.com/Heretek-AI/awesome-opencode.git
synced 2026-11-07 10:45:57 -10:00
- health.yml: rolling "Catalog health report" issue; bot PRs that follow renamed repos and remove entries archived/missing/deprecated for 30+ days, and V1-only plugins after the 2027-01-01 sunset. Inactivity alone only changes a badge. - reprobe.yml: re-checks needs-v2 PRs nightly; unblocks and notifies the original author when the target migrates (filling kind/topics/compat on same-repo branches), closes after 60 days on V1. - scout.yml: weekly issue ranking npm packages published since V2 that depend on @opencode/* and are not listed yet; ticking a box opens a PR (maintainers only). discovery/declined.txt suppresses candidates. Co-Authored-By: Claude Opus 5.5 <[email protected]>
131 lines
6.3 KiB
JavaScript
131 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Nightly re-check of pull requests held for V2 migration (label: needs-v2).
|
|
* Usage: node scripts/reprobe.js [--dry-run] [--pr <number>]
|
|
*
|
|
* - Target migrated to V2: relabel, thank the author, fill in missing kind/topics/compat on
|
|
* same-repo branches, and re-run the validate check.
|
|
* - Still V1 after HOLD_DAYS since needs-v2 was applied: close with a reopen-anytime note.
|
|
*/
|
|
|
|
const yaml = require('js-yaml');
|
|
const { loadCatalog } = require('./utils/catalog');
|
|
const { probeCompat } = require('./utils/compat');
|
|
const { dumpEntry } = require('./utils/format');
|
|
const { rest } = require('./utils/github');
|
|
const { analyzePullRequest } = require('./utils/pr-checks');
|
|
const { REPO, comment, listOpenPulls, pullChanges, revalidate, setTriageLabels } = require('./utils/repo');
|
|
const { suggestKind, suggestTopics } = require('./utils/suggest');
|
|
|
|
const HOLD_DAYS = 60;
|
|
const DAY = 24 * 60 * 60 * 1000;
|
|
|
|
async function labeledAt(number, label) {
|
|
let last = null;
|
|
for (let page = 1; ; page++) {
|
|
const { data } = await rest(`/repos/${REPO}/issues/${number}/events?per_page=100&page=${page}`);
|
|
if (!Array.isArray(data)) break;
|
|
for (const ev of data) if (ev.event === 'labeled' && ev.label && ev.label.name === label) last = ev.created_at;
|
|
if (data.length < 100) break;
|
|
}
|
|
return last ? new Date(last) : null;
|
|
}
|
|
|
|
/** Original author of an imported upstream PR, else the PR author. */
|
|
function authorOf(pull) {
|
|
const upstream = String(pull.body || '').match(/\*\*Original Author\*\*:\s*\[@([A-Za-z0-9-]+)\]/);
|
|
return upstream ? upstream[1] : pull.user.login;
|
|
}
|
|
|
|
async function autofix(pull, change, compat) {
|
|
const data = yaml.load(change.content);
|
|
const [, category] = change.path.split('/');
|
|
const text = [data.name, data.tagline, data.description].join(' ');
|
|
let changed = false;
|
|
if (!data.kind) { data.kind = suggestKind(category, text); changed = true; }
|
|
if (!data.topics || !data.topics.length) { data.topics = suggestTopics(text); changed = true; }
|
|
if (data.compat !== compat) { data.compat = compat; changed = true; }
|
|
if (!changed) return false;
|
|
const { data: current } = await rest(`/repos/${REPO}/contents/${change.path}?ref=${encodeURIComponent(pull.head.ref)}`);
|
|
const res = await rest(`/repos/${REPO}/contents/${change.path}`, {
|
|
method: 'PUT',
|
|
body: {
|
|
message: `chore: fill in kind, topics and compat for ${change.path}`,
|
|
content: Buffer.from(dumpEntry(data)).toString('base64'),
|
|
sha: current.sha,
|
|
branch: pull.head.ref
|
|
}
|
|
});
|
|
if (res.status >= 300) throw new Error(`autofix ${change.path}: HTTP ${res.status}`);
|
|
return true;
|
|
}
|
|
|
|
async function handle(pull, baseEntries, dryRun) {
|
|
const changes = await pullChanges(pull);
|
|
const plugins = changes.filter(c => /^data\/plugins\/[^/]+\.yaml$/.test(c.path) && c.status === 'added' && c.content);
|
|
const parsed = plugins.map(c => { try { return { c, data: yaml.load(c.content) }; } catch { return null; } }).filter(x => x && x.data && x.data.repo);
|
|
if (!parsed.length) return `#${pull.number}: no plugin entries to re-check`;
|
|
|
|
const results = await probeCompat(parsed.map(({ c, data }) => ({ key: c.path, repo: data.repo, path: data.path, npm: data.npm, plugin: true })));
|
|
const stillV1 = parsed.filter(({ c }) => results.get(c.path).compat === 'v1');
|
|
|
|
if (stillV1.length === 0) {
|
|
const sameRepo = pull.head.repo && pull.head.repo.full_name === REPO;
|
|
if (dryRun) return `#${pull.number}: now ${parsed.map(({ c }) => results.get(c.path).compat).join(', ')} -> would unblock`;
|
|
let fixed = false;
|
|
if (sameRepo) {
|
|
for (const { c } of parsed) fixed = (await autofix(pull, c, results.get(c.path).compat)) || fixed;
|
|
}
|
|
const fresh = fixed ? await pullChanges((await rest(`/repos/${REPO}/pulls/${pull.number}`)).data) : changes;
|
|
const analysis = await analyzePullRequest({ changes: fresh, baseEntries });
|
|
await setTriageLabels(pull.number, analysis.labels, pull.labels.map(l => l.name));
|
|
const evidence = parsed.map(({ c }) => `\`${c.path}\`: ${results.get(c.path).compat} (${results.get(c.path).evidence.join('; ')})`).join('\n- ');
|
|
await comment(pull.number, [
|
|
`@${authorOf(pull)} the nightly check now detects OpenCode V2 support. Thank you for migrating!`,
|
|
'',
|
|
`- ${evidence}`,
|
|
fixed ? '\nThe entry was updated with `kind`, `topics` and `compat` so it matches the current schema.' : '',
|
|
analysis.errors.length ? '\nA few metadata items still need fixing; see the checks on this PR.' : '\nAll automated checks pass; a maintainer will merge this shortly.'
|
|
].join('\n'));
|
|
await revalidate((await rest(`/repos/${REPO}/pulls/${pull.number}`)).data);
|
|
return `#${pull.number}: unblocked (${analysis.labels.join(', ')})`;
|
|
}
|
|
|
|
const since = await labeledAt(pull.number, 'needs-v2');
|
|
const days = since ? Math.floor((Date.now() - since.getTime()) / DAY) : 0;
|
|
if (days < HOLD_DAYS) return `#${pull.number}: still V1 (${days}/${HOLD_DAYS} days)`;
|
|
if (dryRun) return `#${pull.number}: still V1 after ${days} days -> would close`;
|
|
await comment(pull.number, [
|
|
`@${authorOf(pull)} closing this for now: the target is still on the legacy V1 plugin API ${days} days after the V2 migration request.`,
|
|
'',
|
|
'Reopen this PR (or submit again through the "Add an entry" issue form) once the plugin depends on `@opencode/plugin` and exports `Plugin.define({ id, setup(ctx) })`. See [docs/policy.md](https://github.com/Heretek-AI/awesome-opencode/blob/main/docs/policy.md).'
|
|
].join('\n'));
|
|
await rest(`/repos/${REPO}/pulls/${pull.number}`, { method: 'PATCH', body: { state: 'closed' } });
|
|
return `#${pull.number}: closed after ${days} days on V1`;
|
|
}
|
|
|
|
async function main() {
|
|
const argv = process.argv.slice(2);
|
|
const dryRun = argv.includes('--dry-run');
|
|
const only = argv.includes('--pr') ? Number(argv[argv.indexOf('--pr') + 1]) : null;
|
|
|
|
const pulls = (await listOpenPulls()).filter(p => (only ? p.number === only : p.labels.some(l => l.name === 'needs-v2')));
|
|
const { entries } = loadCatalog();
|
|
console.log(`Re-checking ${pulls.length} PR(s) labeled needs-v2${dryRun ? ' (dry run)' : ''}`);
|
|
let failures = 0;
|
|
for (const pull of pulls) {
|
|
try {
|
|
console.log(await handle(pull, entries, dryRun));
|
|
} catch (err) {
|
|
failures++;
|
|
console.error(`#${pull.number}: ${err.message}`);
|
|
}
|
|
}
|
|
if (failures) process.exit(1);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|