Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 56 additions & 97 deletions agents/changelog/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { fileURLToPath } from "url";

const REPOS = [
{ owner: "ArcadeAI", repo: "docs", private: false },
{ owner: "ArcadeAI", repo: "arcade-mcp", private: false },
{ owner: "ArcadeAI", repo: "monorepo", private: true },
{ owner: "ArcadeAI", repo: "arcade-mcp", private: false, productionBranch: "production" },
{ owner: "ArcadeAI", repo: "monorepo", private: true, productionBranch: "production" },
];

const CATEGORIES = [
Expand Down Expand Up @@ -51,6 +51,7 @@ type PR = {
labels: string[];
merged_at: string;
is_private: boolean;
merge_commit_sha: string;
};

type CategorizedPR = {
Expand All @@ -63,12 +64,6 @@ type CategorizedPR = {
is_private: boolean;
};

type FinalEntry = {
category: string;
type: string;
description: string;
sources: { repo: string; pr_number: number }[];
};

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

Expand Down Expand Up @@ -105,7 +100,7 @@ async function fetchMergedPRs(
let page = 1;

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

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

const COMBINE_SCHEMA = {
name: "combined_changelog",
strict: true,
schema: {
type: "object" as const,
properties: {
entries: {
type: "array" as const,
items: {
type: "object" as const,
properties: {
category: { type: "string" as const, enum: [...CATEGORIES] },
type: { type: "string" as const, enum: [...TYPES] },
description: { type: "string" as const },
sources: {
type: "array" as const,
items: {
type: "object" as const,
properties: {
repo: { type: "string" as const },
pr_number: { type: "integer" as const },
},
required: ["repo", "pr_number"] as const,
additionalProperties: false,
},
},
},
required: ["category", "type", "description", "sources"] as const,
additionalProperties: false,
},
},
Comment thread
torresmateo marked this conversation as resolved.
},
required: ["entries"] as const,
additionalProperties: false,
},
};

async function combineEntries(
categorized: CategorizedPR[],
openai: OpenAI,
): Promise<FinalEntry[]> {
const model = process.env.OPENAI_MODEL || "gpt-4o-mini";
// --- Step 5a: Filter to production-deployed PRs ---

const input = categorized
.sort((a, b) => a.merged_at.localeCompare(b.merged_at))
.map((pr) => ({
category: pr.category,
type: pr.type,
description: pr.description,
repo: pr.repo,
pr_number: pr.pr_number,
merged_at: pr.merged_at,
}));
async function getUndeployedSHAs(
owner: string,
repo: string,
productionBranch: string,
): Promise<Set<string>> {
const token = process.env.GITHUB_TOKEN;
if (!token) throw new Error("GITHUB_TOKEN env var is required");

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

return JSON.parse(res.choices[0].message.content!).entries;
if (!res.ok) {
const body = await res.text();
throw new Error(`GitHub compare API error for ${owner}/${repo}: ${res.status} ${body}`);
}

const data: any = await res.json();
const shas = new Set<string>();
for (const commit of data.commits) {
shas.add(commit.sha);
}
return shas;
Comment thread
torresmateo marked this conversation as resolved.
}

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

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

const grouped: Record<string, FinalEntry[]> = {};
for (const entry of entries) {
const sorted = [...entries].sort((a, b) => a.merged_at.localeCompare(b.merged_at));

const grouped: Record<string, CategorizedPR[]> = {};
for (const entry of sorted) {
if (!grouped[entry.category]) grouped[entry.category] = [];
grouped[entry.category].push(entry);
}
Expand All @@ -296,14 +251,7 @@ function formatEntry(date: string, entries: FinalEntry[]): string {

for (const item of items) {
const emoji = EMOJI[item.type] || EMOJI.maintenance;
const sources = item.sources
.map((s) => {
const prefix = privateRepos.has(s.repo) ? s.repo : s.repo;
return `${prefix} PR #${s.pr_number}`;
})
.join(", ");

result += `- \`[${item.type} - ${emoji}]\` ${item.description} (${sources})\n`;
result += `- \`[${item.type} - ${emoji}]\` ${item.description} (${item.repo} PR #${item.pr_number})\n`;
}
}

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

if (allPRs.length === 0) {
console.log("No new PRs. Exiting.");
// Step 3a: Filter to only PRs deployed to production
console.log("Filtering to production-deployed PRs...");
const undeployedByRepo: Record<string, Set<string>> = {};
await Promise.all(
REPOS.filter((r) => r.productionBranch).map(async (r) => {
undeployedByRepo[r.repo] = await getUndeployedSHAs(r.owner, r.repo, r.productionBranch!);
}),
);
const deployedPRs = allPRs.filter((pr) => {
const undeployed = undeployedByRepo[pr.repo];
if (!undeployed) return true;
return !undeployed.has(pr.merge_commit_sha);
});
console.log(`${deployedPRs.length} PRs are on production`);

if (deployedPRs.length === 0) {
console.log("No deployed PRs. Exiting.");
return;
}

// Step 4
console.log("Categorizing PRs...");
const categorized = await Promise.all(allPRs.map((pr) => categorizePR(pr, openai)));
const categorized = await Promise.all(deployedPRs.map((pr) => categorizePR(pr, openai)));

// Step 5
console.log("Combining related entries...");
const combined = await combineEntries(categorized, openai);

// Step 6
const entry = formatEntry(fridayDate, combined);
const entry = formatEntry(fridayDate, categorized);
console.log("\nGenerated entry:\n");
console.log(entry);

Expand Down
26 changes: 26 additions & 0 deletions app/en/references/changelog/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ import { Callout } from "nextra/components";

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

## 2026-04-10

**Frameworks**

- `[documentation - πŸ“]` Add AG2 as a supported agent framework in the docs with a new walkthrough.
- `[documentation - πŸ“]` Add documentation for arcade-java and Spring AI guide including quickstart and error-handling updates.

**Arcade MCP Servers**

- `[feature - πŸš€]` Add formatting support to GoogleDocs.CreateDocumentFromText to interpret input as Markdown. (monorepo PR #789)
- `[feature - πŸš€]` Add a new PostHog toolkit with 41 tools.
- `[bugfix - πŸ›]` Fix escaping and wrapping of '$search' queries in Microsoft Outlook Mail tool.
- `[bugfix - πŸ›]` Fix MS Outlook Mail issues and standardize timezone-aware datetime across Mail & Calendar toolkits.
- `[bugfix - πŸ›]` Fix five API response parsing bugs in the Attio toolkit to prevent silent data loss. (monorepo PR #709)
- `[documentation - πŸ“]` Updates documentation for new Microsoft Outlook and Granola.
- `[documentation - πŸ“]` Add reference docs for MCP Resources including URI templates and examples.
- `[documentation - πŸ“]` Updating MCP Servers documentation with toolkit metadata.
- `[documentation - πŸ“]` Document Linear OAuth refresh tokens for custom apps.

**Platform and Engine**

- `[feature - πŸš€]` Add Arcade Auth support to Claude Code gateway connect option.
- `[documentation - πŸ“]` Added documentation on project membership removal and reassignment process.
- `[bugfix - πŸ›]` Fix dashboard navigation issue to prevent chat page redirect when navigating away.


## 2026-03-27

**Frameworks**
Expand Down
Loading