Skip to content

Commit 9697426

Browse files
authored
πŸ€– Friday changelogs (#918)
* πŸ€– Friday changelogs + upgraded changelog agent to include only production commits * remove storybook reference * addressing Eric's comments
1 parent 4ddecac commit 9697426

2 files changed

Lines changed: 82 additions & 97 deletions

File tree

β€Žagents/changelog/generate.tsβ€Ž

Lines changed: 56 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { fileURLToPath } from "url";
77

88
const REPOS = [
99
{ owner: "ArcadeAI", repo: "docs", private: false },
10-
{ owner: "ArcadeAI", repo: "arcade-mcp", private: false },
11-
{ owner: "ArcadeAI", repo: "monorepo", private: true },
10+
{ owner: "ArcadeAI", repo: "arcade-mcp", private: false, productionBranch: "production" },
11+
{ owner: "ArcadeAI", repo: "monorepo", private: true, productionBranch: "production" },
1212
];
1313

1414
const CATEGORIES = [
@@ -51,6 +51,7 @@ type PR = {
5151
labels: string[];
5252
merged_at: string;
5353
is_private: boolean;
54+
merge_commit_sha: string;
5455
};
5556

5657
type CategorizedPR = {
@@ -63,12 +64,6 @@ type CategorizedPR = {
6364
is_private: boolean;
6465
};
6566

66-
type FinalEntry = {
67-
category: string;
68-
type: string;
69-
description: string;
70-
sources: { repo: string; pr_number: number }[];
71-
};
7267

7368
// --- Step 1: Compute upcoming Friday (or today if Friday) ---
7469

@@ -105,7 +100,7 @@ async function fetchMergedPRs(
105100
let page = 1;
106101

107102
while (true) {
108-
const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=${page}`;
103+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed&base=main&sort=updated&direction=desc&per_page=100&page=${page}`;
109104
const res = await fetch(url, {
110105
headers: {
111106
Accept: "application/vnd.github+json",
@@ -134,6 +129,7 @@ async function fetchMergedPRs(
134129
labels: pr.labels?.map((l: any) => l.name) || [],
135130
merged_at: pr.merged_at,
136131
is_private: isPrivate,
132+
merge_commit_sha: pr.merge_commit_sha,
137133
});
138134
}
139135
}
@@ -199,89 +195,48 @@ async function categorizePR(pr: PR, openai: OpenAI): Promise<CategorizedPR> {
199195
};
200196
}
201197

202-
// --- Step 5: Final combining call ---
203198

204-
const COMBINE_SCHEMA = {
205-
name: "combined_changelog",
206-
strict: true,
207-
schema: {
208-
type: "object" as const,
209-
properties: {
210-
entries: {
211-
type: "array" as const,
212-
items: {
213-
type: "object" as const,
214-
properties: {
215-
category: { type: "string" as const, enum: [...CATEGORIES] },
216-
type: { type: "string" as const, enum: [...TYPES] },
217-
description: { type: "string" as const },
218-
sources: {
219-
type: "array" as const,
220-
items: {
221-
type: "object" as const,
222-
properties: {
223-
repo: { type: "string" as const },
224-
pr_number: { type: "integer" as const },
225-
},
226-
required: ["repo", "pr_number"] as const,
227-
additionalProperties: false,
228-
},
229-
},
230-
},
231-
required: ["category", "type", "description", "sources"] as const,
232-
additionalProperties: false,
233-
},
234-
},
235-
},
236-
required: ["entries"] as const,
237-
additionalProperties: false,
238-
},
239-
};
240-
241-
async function combineEntries(
242-
categorized: CategorizedPR[],
243-
openai: OpenAI,
244-
): Promise<FinalEntry[]> {
245-
const model = process.env.OPENAI_MODEL || "gpt-4o-mini";
199+
// --- Step 5a: Filter to production-deployed PRs ---
246200

247-
const input = categorized
248-
.sort((a, b) => a.merged_at.localeCompare(b.merged_at))
249-
.map((pr) => ({
250-
category: pr.category,
251-
type: pr.type,
252-
description: pr.description,
253-
repo: pr.repo,
254-
pr_number: pr.pr_number,
255-
merged_at: pr.merged_at,
256-
}));
201+
async function getUndeployedSHAs(
202+
owner: string,
203+
repo: string,
204+
productionBranch: string,
205+
): Promise<Set<string>> {
206+
const token = process.env.GITHUB_TOKEN;
207+
if (!token) throw new Error("GITHUB_TOKEN env var is required");
257208

258-
const res = await openai.chat.completions.create({
259-
model,
260-
response_format: { type: "json_schema", json_schema: COMBINE_SCHEMA },
261-
messages: [
262-
{
263-
role: "system",
264-
content: [
265-
`You are combining changelog entries.`,
266-
`If a docs PR and a non-docs PR are about the same feature, combine them into one entry under the non-docs category.`,
267-
`Do not alter categories or types unless combining.`,
268-
`Keep descriptions concise. Return ALL entries.`,
269-
].join(" "),
270-
},
271-
{ role: "user", content: JSON.stringify(input) },
272-
],
209+
const url = `https://api.github.com/repos/${owner}/${repo}/compare/${productionBranch}...main`;
210+
const res = await fetch(url, {
211+
headers: {
212+
Accept: "application/vnd.github+json",
213+
Authorization: `Bearer ${token}`,
214+
"X-GitHub-Api-Version": "2022-11-28",
215+
},
273216
});
274217

275-
return JSON.parse(res.choices[0].message.content!).entries;
218+
if (!res.ok) {
219+
const body = await res.text();
220+
throw new Error(`GitHub compare API error for ${owner}/${repo}: ${res.status} ${body}`);
221+
}
222+
223+
const data: any = await res.json();
224+
const shas = new Set<string>();
225+
for (const commit of data.commits) {
226+
shas.add(commit.sha);
227+
}
228+
return shas;
276229
}
277230

278231
// --- Step 6: Format the entry ---
279232

280-
function formatEntry(date: string, entries: FinalEntry[]): string {
233+
function formatEntry(date: string, entries: CategorizedPR[]): string {
281234
const privateRepos = new Set(REPOS.filter((r) => r.private).map((r) => r.repo));
282235

283-
const grouped: Record<string, FinalEntry[]> = {};
284-
for (const entry of entries) {
236+
const sorted = [...entries].sort((a, b) => a.merged_at.localeCompare(b.merged_at));
237+
238+
const grouped: Record<string, CategorizedPR[]> = {};
239+
for (const entry of sorted) {
285240
if (!grouped[entry.category]) grouped[entry.category] = [];
286241
grouped[entry.category].push(entry);
287242
}
@@ -296,14 +251,7 @@ function formatEntry(date: string, entries: FinalEntry[]): string {
296251

297252
for (const item of items) {
298253
const emoji = EMOJI[item.type] || EMOJI.maintenance;
299-
const sources = item.sources
300-
.map((s) => {
301-
const prefix = privateRepos.has(s.repo) ? s.repo : s.repo;
302-
return `${prefix} PR #${s.pr_number}`;
303-
})
304-
.join(", ");
305-
306-
result += `- \`[${item.type} - ${emoji}]\` ${item.description} (${sources})\n`;
254+
result += `- \`[${item.type} - ${emoji}]\` ${item.description} (${item.repo} PR #${item.pr_number})\n`;
307255
}
308256
}
309257

@@ -351,21 +299,32 @@ async function main() {
351299
).flat();
352300
console.log(`Found ${allPRs.length} merged PRs since ${lastEntryDate}`);
353301

354-
if (allPRs.length === 0) {
355-
console.log("No new PRs. Exiting.");
302+
// Step 3a: Filter to only PRs deployed to production
303+
console.log("Filtering to production-deployed PRs...");
304+
const undeployedByRepo: Record<string, Set<string>> = {};
305+
await Promise.all(
306+
REPOS.filter((r) => r.productionBranch).map(async (r) => {
307+
undeployedByRepo[r.repo] = await getUndeployedSHAs(r.owner, r.repo, r.productionBranch!);
308+
}),
309+
);
310+
const deployedPRs = allPRs.filter((pr) => {
311+
const undeployed = undeployedByRepo[pr.repo];
312+
if (!undeployed) return true;
313+
return !undeployed.has(pr.merge_commit_sha);
314+
});
315+
console.log(`${deployedPRs.length} PRs are on production`);
316+
317+
if (deployedPRs.length === 0) {
318+
console.log("No deployed PRs. Exiting.");
356319
return;
357320
}
358321

359322
// Step 4
360323
console.log("Categorizing PRs...");
361-
const categorized = await Promise.all(allPRs.map((pr) => categorizePR(pr, openai)));
324+
const categorized = await Promise.all(deployedPRs.map((pr) => categorizePR(pr, openai)));
362325

363326
// Step 5
364-
console.log("Combining related entries...");
365-
const combined = await combineEntries(categorized, openai);
366-
367-
// Step 6
368-
const entry = formatEntry(fridayDate, combined);
327+
const entry = formatEntry(fridayDate, categorized);
369328
console.log("\nGenerated entry:\n");
370329
console.log(entry);
371330

β€Žapp/en/references/changelog/page.mdxβ€Ž

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ import { Callout } from "nextra/components";
99

1010
_Here's what's new at Arcade.dev!_
1111

12+
## 2026-04-10
13+
14+
**Frameworks**
15+
16+
- `[documentation - πŸ“]` Add AG2 as a supported agent framework in the docs with a new walkthrough.
17+
- `[documentation - πŸ“]` Add documentation for arcade-java and Spring AI guide including quickstart and error-handling updates.
18+
19+
**Arcade MCP Servers**
20+
21+
- `[feature - πŸš€]` Add formatting support to GoogleDocs.CreateDocumentFromText to interpret input as Markdown. (monorepo PR #789)
22+
- `[feature - πŸš€]` Add a new PostHog toolkit with 41 tools.
23+
- `[bugfix - πŸ›]` Fix escaping and wrapping of '$search' queries in Microsoft Outlook Mail tool.
24+
- `[bugfix - πŸ›]` Fix MS Outlook Mail issues and standardize timezone-aware datetime across Mail & Calendar toolkits.
25+
- `[bugfix - πŸ›]` Fix five API response parsing bugs in the Attio toolkit to prevent silent data loss. (monorepo PR #709)
26+
- `[documentation - πŸ“]` Updates documentation for new Microsoft Outlook and Granola.
27+
- `[documentation - πŸ“]` Add reference docs for MCP Resources including URI templates and examples.
28+
- `[documentation - πŸ“]` Updating MCP Servers documentation with toolkit metadata.
29+
- `[documentation - πŸ“]` Document Linear OAuth refresh tokens for custom apps.
30+
31+
**Platform and Engine**
32+
33+
- `[feature - πŸš€]` Add Arcade Auth support to Claude Code gateway connect option.
34+
- `[documentation - πŸ“]` Added documentation on project membership removal and reassignment process.
35+
- `[bugfix - πŸ›]` Fix dashboard navigation issue to prevent chat page redirect when navigating away.
36+
37+
1238
## 2026-03-27
1339

1440
**Frameworks**

0 commit comments

Comments
Β (0)