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.
_17client.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.
_179client.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});