

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
- Next.js (hosted on Vercel)
- Trigger.dev's OpenAI, Supabase and GitHub Integrations
- Trigger.dev's React hooks to show the live status of the Job in the UI
- Supabase to store the changelogs
- Tailwind CSS and ShadCN UI for styling
Preview

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