From 72ffe3dcc7ad2b79a4f0f048f643fe0b1ec5d52d Mon Sep 17 00:00:00 2001 From: ztimson Date: Wed, 14 Jan 2026 15:43:53 -0500 Subject: [PATCH] Added a release bot to create release notes from closed milestones --- .github/workflows/code-review.yml | 4 +- .github/workflows/release-creator.yml | 27 +++++++++ .github/workflows/ticket-refinement.yml | 4 +- package.json | 3 +- src/release.mjs | 81 +++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release-creator.yml create mode 100644 src/release.mjs diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 78ef451..0433282 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -15,8 +15,8 @@ jobs: git checkout ${{ github.event.pull_request.head.sha }} git fetch origin ${{ github.event.pull_request.base.ref }} - - name: Run AI Review - run: npx -y -p @ztimson/ai-agents@latest review $GITHUB_WORKSPACE + - name: Create review + run: npx --no-cache -y -p @ztimson/ai-agents@latest review $GITHUB_WORKSPACE env: AI_HOST: anthropic AI_MODEL: claude-sonnet-4-5 diff --git a/.github/workflows/release-creator.yml b/.github/workflows/release-creator.yml new file mode 100644 index 0000000..25b8c5a --- /dev/null +++ b/.github/workflows/release-creator.yml @@ -0,0 +1,27 @@ +name: Release Bot + +on: + milestone: + types: [closed] + +jobs: + release: + runs-on: ubuntu-latest + container: node:22 + steps: + - name: Checkout + run: | + git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" . + git checkout ${{ github.event.repository.default_branch }} + + - name: Create release + run: npx --no-cache -y @ztimson/ai-agents@latest release + env: + AI_HOST: anthropic + AI_MODEL: claude-sonnet-4-5 + AI_TOKEN: ${{ secrets.ANTHROPIC_TOKEN }} + GIT_HOST: ${{ github.server_url }} + GIT_OWNER: ${{ github.repository_owner }} + GIT_REPO: ${{ github.event.repository.name }} + GIT_TOKEN: ${{ secrets.ASSISTANT_TOKEN }} + MILESTONE: ${{ github.event.milestone.number }} diff --git a/.github/workflows/ticket-refinement.yml b/.github/workflows/ticket-refinement.yml index ac728ec..891478e 100644 --- a/.github/workflows/ticket-refinement.yml +++ b/.github/workflows/ticket-refinement.yml @@ -14,8 +14,8 @@ jobs: git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" . git checkout ${{ github.event.repository.default_branch }} - - name: Run AI Formatter - run: npx -y -p @ztimson/ai-agents@latest refine + - name: Refine ticket + run: npx --no-cache -y -p @ztimson/ai-agents@latest refine env: AI_HOST: anthropic AI_MODEL: claude-sonnet-4-5 diff --git a/package.json b/package.json index 56f036b..4838ebc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/ai-agents", - "version": "0.1.2", + "version": "0.1.3", "description": "AI agents", "keywords": ["ai", "review"], "author": "ztimson", @@ -8,6 +8,7 @@ "type": "module", "bin": { "refine": "./src/refine.mjs", + "release": "./src/release.mjs", "review": "./src/review.mjs" }, "dependencies": { diff --git a/src/release.mjs b/src/release.mjs new file mode 100644 index 0000000..e8793a2 --- /dev/null +++ b/src/release.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +import {Ai} from '@ztimson/ai'; +import * as dotenv from 'dotenv'; + +dotenv.config({quiet: true, debug: false}); +dotenv.config({path: '.env.local', override: true, quiet: true, debug: false}); + +(async () => { + const git = process.env['GIT_HOST'], + owner = process.env['GIT_OWNER'], + repo = process.env['GIT_REPO'], + auth = process.env['GIT_TOKEN'], + milestone = process.env['MILESTONE'], + host = process.env['AI_HOST'] || 'ollama', + model = process.env['AI_MODEL'] || 'llama3', + token = process.env['AI_TOKEN']; + + // Get milestone info + const milestoneData = await fetch(`${git}/api/v1/repos/${owner}/${repo}/milestones/${milestone}`, { + headers: {'Authorization': `token ${auth}`} + }).then(resp => resp.ok ? resp.json() : {}); + + // Get closed issues + const issues = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues?state=closed&milestone=${milestone}`, { + headers: {'Authorization': `token ${auth}`} + }).then(resp => resp.ok ? resp.json() : []); + + // Get closed PRs + const prs = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls?state=closed&milestone=${milestone}`, { + headers: {'Authorization': `token ${auth}`} + }).then(resp => resp.ok ? resp.json() : []); + + // Get latest tag + const latestTag = await $`git describe --tags --abbrev=0`.text(); + + // Build context + let context = `Milestone: ${milestoneData.title} +Description: +\`\`\`md +${milestoneData.description || ''} +\`\`\` + +PRs: +${prs.map(pr => `- ${pr.title}\n${pr.body || ''}`).join('\n\n')} + +Issues: +${issues.filter(i => !i.pull_request).map(i => `- ${i.title}\n${i.body || ''}`).join('\n\n')}`; + + // Generate release notes + let options = {ollama: {model, host}}; + if(host === 'anthropic') options = {anthropic: {model, token}}; + else if(host === 'openai') options = {openAi: {model, token}}; + + const ai = new Ai({ + ...options, + model: [host, model], + system: `You are a release notes writer. Format the provided milestone info, PRs, and issues into clean, organized release notes. Use markdown with sections like "Features", "Bug Fixes", "Breaking Changes", etc. Be concise but informative. Include issue/PR numbers in format #123.` + }); + + const body = (await ai.chat(context)).pop().content; + + // Create release + const name = latestTag.trim(); + await fetch(`${git}/api/v1/repos/${owner}/${repo}/releases`, { + method: 'POST', + headers: { + 'Authorization': `token ${auth}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name, + tag_name: name, + body + }) + }).then(resp => resp.ok ? console.log('Release created! 🎉') : console.error('Failed to create release')); + + console.log(`Title: ${name}\nDescription:\n${body}`); +})().catch(err => { + console.error(`Error: ${err.message || err.toString()}`); + process.exit(1); +});