Generate a changelog from GitHub commits using AI

Generate a changelog from GitHub commits using AI

Trigger: eventTrigger, Webhook

An app that generates a changelog from GitHub commits using Open AI.

Framework:

Repository:

/auto-changelog

Categories:

AI

Dev Ops

Overview

Generate a changelog automatically from an open source repo using our OpenAI, GitHub and Supabase Integrations.

Features

Preview

Demo video of the Auto Changelog app

Job code

There are 2 Jobs each separated into Tasks. Here's an overview of the code below:

Job 1: Changelog created

The Task in this first Job triggers the main Job 2 below when a new Supabase database entry is created.


_17
client.defineJob({
_17
id: "supabase-changelog-inserted",
_17
name: "Changelog created",
_17
version: "0.1.0",
_17
trigger: supabaseTriggers.onInserted({
_17
table: "changelogs",
_17
}),
_17
run: async (payload, io) => {
_17
await io.sendEvent("Generate changelog", {
_17
id: payload.record.id.toString(),
_17
name: "generate.changelog",
_17
payload: {
_17
changelogId: payload.record.id,
_17
},
_17
});
_17
},
_17
});

Job 2: Generate Changelog

This Job is separated into 4 Tasks. It gets the changelog and repo record from Supabase, gets all of the commit messages from Github, gets the commits into the correct format for the prompt and finally, generates the changelog using OpenAI.


_179
client.defineJob({
_179
id: "generate-changelog",
_179
name: "Generate Changelog",
_179
version: "0.1.0",
_179
trigger: eventTrigger({
_179
name: "generate.changelog",
_179
schema: z.object({
_179
changelogId: z.number(),
_179
}),
_179
}),
_179
integrations: { supabase, github, openai },
_179
run: async (payload, io) => {
_179
const gettingRecordStatus = await io.createStatus(
_179
"Generating changelog...",
_179
{
_179
label: "Fetching repo",
_179
state: "loading",
_179
}
_179
);
_179
_179
//1. get the changelog and repo record from Supabase
_179
const changelog = await io.supabase.runTask(
_179
"Get changelog",
_179
async (client) => {
_179
const record = await client
_179
.from("changelogs")
_179
.select(
_179
`
_179
id,
_179
start_date,
_179
end_date,
_179
repo: repos(*)
_179
`
_179
)
_179
.eq("id", payload.changelogId)
_179
.maybeSingle();
_179
_179
if (!record.data) {
_179
throw new Error("No changelog found");
_179
}
_179
_179
return record.data;
_179
}
_179
);
_179
_179
if (!changelog.repo) {
_179
await gettingRecordStatus.update("fetch failed", {
_179
label: `No repo found`,
_179
state: "failure",
_179
});
_179
throw new Error("No repo found");
_179
}
_179
const { owner, repo } = changelog.repo;
_179
_179
await gettingRecordStatus.update("record fetched", {
_179
label: "Fetched repo",
_179
state: "success",
_179
});
_179
_179
const gettingCommitsStatus = await io.createStatus("Getting commits", {
_179
label: "Fetching commits from GitHub",
_179
state: "loading",
_179
});
_179
_179
//2. get all of the commit messages from Github
_179
const rawCommits = await io.github.runTask(
_179
"Getting commits...",
_179
async (client) => {
_179
const { data } = await client.rest.repos.listCommits({
_179
owner,
_179
repo,
_179
// Default to one week ago
_179
since: changelog.start_date,
_179
until: changelog.end_date,
_179
per_page: 100,
_179
});
_179
_179
return data;
_179
}
_179
);
_179
_179
if (rawCommits.length === 0) {
_179
await gettingCommitsStatus.update("No commits found", {
_179
label: "No commits found",
_179
state: "failure",
_179
});
_179
throw new Error("No commits found");
_179
}
_179
_179
await gettingCommitsStatus.update("Got commits", {
_179
label: `Fetched ${rawCommits.length} commits from GitHub`,
_179
state: "success",
_179
});
_179
_179
const summarizingStatus = await io.createStatus("Summarizing commits", {
_179
label: `Summarizing ${rawCommits.length} commits using OpenAI`,
_179
state: "loading",
_179
});
_179
_179
//3. Get the commits into the correct format for the prompt
_179
const commits = rawCommits.map(({ commit, author }) => ({
_179
message: commit.message
_179
.trim()
_179
.replaceAll("\r", "")
_179
.replaceAll("\n\n", "\n"),
_179
author: author?.login,
_179
}));
_179
_179
//4. Generate the changelog using OpenAI
_179
const promptPrefix = `
_179
Limit prose. Be extremely concise.
_179
You're the head of developer relations at a SaaS. You'll write a short and professional but fun changelog.
_179
Below are the commit messages since the last changelog.
_179
Title: catchy and foretelling.
_179
Intro: fun; themes and highlights.
_179
Then, summarize the most important changes in bullet points.
_179
Write in markdown. Ignore numbers, IDs, and timestamps. Keep it light.
_179
Limit prose.`;
_179
_179
const prompt = `${promptPrefix}\n\n${commits
_179
.map((c) => c.message)
_179
.join("\n")
_179
.slice(0, maxTokens)}`;
_179
_179
const response = await io.openai.backgroundCreateChatCompletion(
_179
"OpenAI Completions API",
_179
{
_179
model: "gpt-3.5-turbo",
_179
messages: [
_179
{
_179
role: "user",
_179
content: prompt,
_179
},
_179
],
_179
}
_179
);
_179
_179
const changelogMarkdown = response.choices.at(0)?.message?.content;
_179
_179
if (!changelogMarkdown) {
_179
await summarizingStatus.update("completed", {
_179
label: "Failed to summarize commits",
_179
state: "failure",
_179
});
_179
throw new Error("OpenAI failed to return a response");
_179
}
_179
_179
await summarizingStatus.update("completed summary", {
_179
label: `Summarized ${rawCommits.length} commits using OpenAI`,
_179
state: "success",
_179
});
_179
_179
const savingStatus = await io.createStatus("Storing commits", {
_179
label: "Saving changelog to Supabase",
_179
state: "loading",
_179
});
_179
_179
await io.supabase.runTask(
_179
"Update changelog with markdown",
_179
async (client) => {
_179
await client
_179
.from("changelogs")
_179
.update({
_179
markdown: changelogMarkdown,
_179
})
_179
.eq("id", payload.changelogId);
_179
}
_179
);
_179
_179
await savingStatus.update("completed saving", {
_179
label: "Saved changelog to Supabase",
_179
state: "success",
_179
});
_179
_179
return {
_179
markdown: changelogMarkdown,
_179
};
_179
},
_179
});