generated from ztimson/template
Compare commits
7 Commits
ebc3da8605
...
release-bo
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cfcb3f95c | |||
| e625782eec | |||
| 72ffe3dcc7 | |||
| 1ab97c2676 | |||
| 7447204351 | |||
| 3b01e1bfc1 | |||
| 1460c3a0ae |
2
.github/workflows/code-review.yml
vendored
2
.github/workflows/code-review.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
git checkout ${{ github.event.pull_request.head.sha }}
|
git checkout ${{ github.event.pull_request.head.sha }}
|
||||||
git fetch origin ${{ github.event.pull_request.base.ref }}
|
git fetch origin ${{ github.event.pull_request.base.ref }}
|
||||||
|
|
||||||
- name: Run AI Review
|
- name: Create review
|
||||||
run: npx -y -p @ztimson/ai-agents@latest review $GITHUB_WORKSPACE
|
run: npx -y -p @ztimson/ai-agents@latest review $GITHUB_WORKSPACE
|
||||||
env:
|
env:
|
||||||
AI_HOST: anthropic
|
AI_HOST: anthropic
|
||||||
|
|||||||
27
.github/workflows/release-creator.yml
vendored
Normal file
27
.github/workflows/release-creator.yml
vendored
Normal file
@@ -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 -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 }}
|
||||||
2
.github/workflows/ticket-refinement.yml
vendored
2
.github/workflows/ticket-refinement.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
|
git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
|
||||||
git checkout ${{ github.event.repository.default_branch }}
|
git checkout ${{ github.event.repository.default_branch }}
|
||||||
|
|
||||||
- name: Run AI Formatter
|
- name: Refine ticket
|
||||||
run: npx -y -p @ztimson/ai-agents@latest refine
|
run: npx -y -p @ztimson/ai-agents@latest refine
|
||||||
env:
|
env:
|
||||||
AI_HOST: anthropic
|
AI_HOST: anthropic
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/ai-agents",
|
"name": "@ztimson/ai-agents",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"description": "AI agents",
|
"description": "AI agents",
|
||||||
"keywords": ["ai", "review"],
|
"keywords": ["ai", "review"],
|
||||||
"author": "ztimson",
|
"author": "ztimson",
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"refine": "./src/refine.mjs",
|
"refine": "./src/refine.mjs",
|
||||||
|
"release": "./src/release.mjs",
|
||||||
"review": "./src/review.mjs"
|
"review": "./src/review.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
90
src/release.mjs
Normal file
90
src/release.mjs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import {Ai} from '@ztimson/ai';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import {$} from '@ztimson/node-utils';
|
||||||
|
|
||||||
|
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();
|
||||||
|
if(!latestTag) {
|
||||||
|
console.error('At least one Git tag is required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if(!body) {
|
||||||
|
console.error('No release notes were generated');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 => { if(!resp.ok) throw new Error(resp.status + ' ' + resp.statusText) });
|
||||||
|
|
||||||
|
console.log(`Title: ${name}\nDescription:\n${body}`);
|
||||||
|
})().catch(err => {
|
||||||
|
console.error(`Error: ${err.message || err.toString()}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -27,8 +27,11 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
|||||||
|
|
||||||
console.log(`Reviewing: ${root}\n`);
|
console.log(`Reviewing: ${root}\n`);
|
||||||
const info = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}`)
|
const info = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}`)
|
||||||
.then(async resp => { return resp.ok ? resp.json() : throw new Error(`${resp.status} ${await resp.text()}`); });
|
.then(async resp => {
|
||||||
if(info.labels?.length > 0 || !info.labels.some(l => l.name === labelEnabled)) {
|
if(resp.ok) return resp.json();
|
||||||
|
throw new Error(`${resp.status} ${await resp.text()}`);
|
||||||
|
});
|
||||||
|
if(!info.labels.some(l => l.name === labelEnabled)) {
|
||||||
console.log('Skipping');
|
console.log('Skipping');
|
||||||
return process.exit();
|
return process.exit();
|
||||||
}
|
}
|
||||||
@@ -95,7 +98,16 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
const messages = await ai.language.ask(gitDiff);
|
const messages = await ai.language.ask(`Title: ${info.title || 'None'}
|
||||||
|
Description:
|
||||||
|
\`\`\`md
|
||||||
|
${info.body || 'None'}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Git Diff:
|
||||||
|
\`\`\`
|
||||||
|
${gitDiff}
|
||||||
|
\`\`\``);
|
||||||
const summary = messages.pop().content;
|
const summary = messages.pop().content;
|
||||||
if(git) {
|
if(git) {
|
||||||
const res = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews`, {
|
const res = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user