Skip to content

Commit fd03580

Browse files
committed
Add generate_updates.js script
1 parent 38a4c45 commit fd03580

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed

scripts/generate_updates.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
const { Octokit } = require("@octokit/rest");
2+
const { GoogleGenerativeAI } = require("@google/generative-ai");
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
7+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
8+
const REPOSITORIES = process.env.REPOSITORIES || process.env.GITHUB_REPOSITORY;
9+
10+
const octokit = new Octokit({ auth: GITHUB_TOKEN });
11+
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
12+
13+
async function getIssueDetails(owner, repo, issueNumber) {
14+
try {
15+
const { data: issue } = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
16+
owner,
17+
repo,
18+
issue_number: issueNumber
19+
});
20+
return `Issue Title: ${issue.title}\nIssue Description: ${issue.body ? issue.body.slice(0, 500) + "..." : "No description provided."}`;
21+
} catch (e) {
22+
console.warn(`Could not fetch issue #${issueNumber} for ${owner}/${repo}: ${e.message}`);
23+
return null;
24+
}
25+
}
26+
27+
async function getMergedPRs(owner, repo) {
28+
try {
29+
const prs = await octokit.paginate("GET /repos/{owner}/{repo}/pulls", { owner, repo, state: 'closed', per_page: 50 });
30+
const thirtyDaysAgo = new Date();
31+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
32+
const merged = prs.filter(pr => pr.merged_at && new Date(pr.merged_at) > thirtyDaysAgo);
33+
34+
const results = [];
35+
for (const pr of merged) {
36+
let issueContext = "";
37+
const issueMatch = pr.body && pr.body.match(/(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+#(\d+)/i);
38+
if (issueMatch) {
39+
const issueNumber = issueMatch[1];
40+
const details = await getIssueDetails(owner, repo, issueNumber);
41+
if (details) issueContext = details;
42+
}
43+
results.push({ repository: `${owner}/${repo}`, title: pr.title, context: issueContext });
44+
}
45+
return results;
46+
} catch (e) { return []; }
47+
}
48+
49+
async function generateWithRetry(modelName, prompt, attempt = 1) {
50+
try {
51+
const model = genAI.getGenerativeModel({ model: modelName });
52+
const result = await model.generateContent(prompt);
53+
return result.response.text();
54+
} catch (e) {
55+
if ((e.status === 503 || e.status === 429) && attempt < 3) {
56+
console.log(`Model ${modelName} busy. Waiting 3s...`);
57+
await new Promise(r => setTimeout(r, 3000));
58+
return generateWithRetry(modelName, prompt, attempt + 1);
59+
}
60+
throw e;
61+
}
62+
}
63+
64+
async function main() {
65+
try {
66+
const repoList = REPOSITORIES.split(',').map(r => r.trim());
67+
let allUpdates = [];
68+
for (const repoStr of repoList) {
69+
const [o, r] = repoStr.split('/');
70+
const prs = await getMergedPRs(o, r);
71+
allUpdates = allUpdates.concat(prs);
72+
}
73+
74+
if (allUpdates.length === 0) return console.log("Nothing to report.");
75+
76+
let style = "Professional.";
77+
// Prioritize .gemini/styleguide.md, fallback to root
78+
if (fs.existsSync('.gemini/styleguide.md')) {
79+
style = fs.readFileSync('.gemini/styleguide.md', 'utf8');
80+
} else if (fs.existsSync('STYLE_GUIDE.md')) {
81+
style = fs.readFileSync('STYLE_GUIDE.md', 'utf8');
82+
}
83+
84+
const prSummaries = allUpdates.map(p => {
85+
let item = `- [${p.repository}] ${p.title}`;
86+
if (p.context) item += `\n Context (Linked Issue): ${p.context.replace(/\n/g, ' ')}`;
87+
return item;
88+
}).join("\n");
89+
90+
const instruction = `
91+
Role: You are a Product Marketing expert writing a customer-facing Product Update.
92+
Task: Transform raw technical pull request titles into a polished, user-friendly Product Update.
93+
94+
Instructions:
95+
1. Categorize updates clearly (e.g., 🚀 New Features, ⚡ Improvements, 🐛 Bug Fixes).
96+
2. Explain each item in customer-friendly, non-technical language.
97+
3. Clearly state the User Benefit for every major update.
98+
4. Use the provided 'Context (Linked Issue)' to explain the "why" and customer value.
99+
5. STRICTLY follow the provided Style Guide (tone, formatting, structure).
100+
6. Do not invent features or details; keep vague PRs high-level.
101+
7. Write with a product marketing tone (value-led, confident, announcement-style — not technical).
102+
8. Exclude internal maintenance tasks (e.g., dependency updates, CI/CD changes) that have no visible impact on the user.
103+
104+
Output: Scannable, customer-ready Product Update suitable for release notes or announcements.
105+
`;
106+
107+
const prompt = `${instruction}\n\nStyle Guide:\n${style}\n\nChanges:\n${prSummaries}`;
108+
109+
const models = ["gemini-3-flash-preview", "gemini-2.5-flash", "gemini-1.5-flash"];
110+
let content = null;
111+
for (const m of models) {
112+
try { content = await generateWithRetry(m, prompt); break; }
113+
catch (e) { console.log(`${m} failed, trying next...`); }
114+
}
115+
116+
if (!content) throw new Error("AI servers are unavailable. Try again in 5 minutes.");
117+
118+
if (!fs.existsSync('product-updates')) fs.mkdirSync('product-updates');
119+
fs.writeFileSync(`product-updates/${new Date().getFullYear()}.md`, content);
120+
console.log("SUCCESS!");
121+
} catch (e) { console.error("FATAL:", e.message); process.exit(1); }
122+
}
123+
main();

0 commit comments

Comments
 (0)