generated from ztimson/template
Compare commits
63 Commits
dcbdb231a6
...
release-bo
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cfcb3f95c | |||
| e625782eec | |||
| 72ffe3dcc7 | |||
| 1ab97c2676 | |||
| 7447204351 | |||
| 3b01e1bfc1 | |||
| 1460c3a0ae | |||
| ebc3da8605 | |||
| 677f84c97a | |||
| f543e08e36 | |||
| 5b9f8e0e13 | |||
| 019b05105a | |||
| decd533e4e | |||
| 7becf99be2 | |||
| 99a1e55471 | |||
| 23cb66544e | |||
| 9e5372f37b | |||
| eb4486f196 | |||
| f3df34ec47 | |||
| f2936ae4dc | |||
| e62e11fb75 | |||
| 604e04559b | |||
| 8add830d2b | |||
| 57bbc1fdb4 | |||
| 078892297e | |||
| 3c2d6f7824 | |||
| 2d9662e86d | |||
| 91d22b8b16 | |||
| c7f8ffb32a | |||
| 27fad6a3d3 | |||
| 3daf5442d8 | |||
| 002e809ef6 | |||
| d1c0b7a872 | |||
| 1b026529fe | |||
| af09bd0f53 | |||
| 397c9aeb90 | |||
| 1f48b5a872 | |||
| 6bb78d0862 | |||
| 73ad9293ae | |||
| 4175bf363c | |||
| c0281cd57c | |||
| a24961a55c | |||
| e0f3a3cd82 | |||
| 6fccede7ba | |||
| f00906045a | |||
| bee0029469 | |||
| 1b3232b10c | |||
| 40ade3fef1 | |||
| f65fd15220 | |||
| 93e7be5280 | |||
| 97895096f3 | |||
| 001d016a8a | |||
| 882b845b11 | |||
| f50324a072 | |||
| 1396f4b305 | |||
| 5d3e15f0f5 | |||
| c27802ca44 | |||
| 2b11979b66 | |||
| 45d43c0977 | |||
| 1b4fd05d65 | |||
| d6857bd8e3 | |||
| 6cc3d5eb00 | |||
| b2673b79e7 |
11
.env
Normal file
11
.env
Normal file
@@ -0,0 +1,11 @@
|
||||
; DO NOT MODIFY! this is an example environment file
|
||||
; Create a copy called .env.local with the needed settings
|
||||
|
||||
GIT_HOST=
|
||||
GIT_OWNER=
|
||||
GIT_REPO=
|
||||
GIT_TOKEN=
|
||||
PULL_REQUEST=
|
||||
AI_HOST=
|
||||
AI_MODEL=
|
||||
AI_TOKEN=
|
||||
19
.github/issue_template/ai-refinement.md
vendored
Normal file
19
.github/issue_template/ai-refinement.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
|
||||
name: "AI refinement"
|
||||
about: "Use AI to refine ticket"
|
||||
ref: "develop"
|
||||
labels:
|
||||
- Review/AI
|
||||
|
||||
---
|
||||
|
||||
Describe your request:
|
||||
|
||||
What are you trying to do and what's happening?
|
||||
|
||||
How can it be fixed or improved?
|
||||
|
||||
Steps to reproduce?
|
||||
|
||||
Any other useful information? Logs, screenshots, steps to reproduce?
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -9,7 +9,5 @@
|
||||
|
||||
## Checklist
|
||||
<!-- Complete after creating PR -->
|
||||
- [ ] Linked issues
|
||||
- [ ] Reviewed changes
|
||||
- [ ] Reviewed changes (or use `Review/AI` label)
|
||||
- [ ] Updated comments/documentation
|
||||
|
||||
45
.github/workflows/build.yml
vendored
Normal file
45
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Publish Library
|
||||
run-name: Publish Library
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build NPM Project
|
||||
runs-on: ubuntu-latest
|
||||
container: node:alpine
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: ztimson/actions/clone@develop
|
||||
|
||||
- name: Publish Library
|
||||
run: |
|
||||
if [ "${{github.ref_name}}" = "master" ]; then
|
||||
REGISTRY="${{github.server_url}}/api/packages/${{github.repository_owner}}/npm/"
|
||||
npm set registry "$REGISTRY"
|
||||
npm set $(echo $REGISTRY | sed s%http:%% | sed s%https:%% ):_authToken "${{secrets.DEPLOY_TOKEN}}"
|
||||
npm publish || echo "Failed to publish"
|
||||
|
||||
REGISTRY="https://registry.npmjs.org/"
|
||||
npm set registry "$REGISTRY"
|
||||
npm set $(echo $REGISTRY | sed s%http:%% | sed s%https:%% ):_authToken "${{secrets.NPM_TOKEN}}"
|
||||
npm publish || echo "Failed to publish"
|
||||
fi
|
||||
tag:
|
||||
name: Tag Version
|
||||
needs: build
|
||||
if: ${{github.ref_name}} == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
container: node:alpine
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: ztimson/actions/clone@develop
|
||||
|
||||
- name: Get Version Number
|
||||
run: echo "VERSION=$(cat package.json | grep version | grep -Eo ':.+' | grep -Eo '[[:alnum:]\.\/\-]+')" >> $GITHUB_ENV
|
||||
|
||||
- name: Tag Version
|
||||
uses: ztimson/actions/tag@develop
|
||||
with:
|
||||
tag: ${{env.VERSION}}
|
||||
29
.github/workflows/code-review.yml
vendored
Normal file
29
.github/workflows/code-review.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Code review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22
|
||||
steps:
|
||||
- name: Fetch code
|
||||
run: |
|
||||
git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
|
||||
git checkout ${{ github.event.pull_request.head.sha }}
|
||||
git fetch origin ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Create review
|
||||
run: npx -y -p @ztimson/ai-agents@latest review $GITHUB_WORKSPACE
|
||||
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 }}
|
||||
GIT_BRANCH: origin/${{ github.event.pull_request.base.ref }}
|
||||
PULL_REQUEST: ${{ github.event.pull_request.number }}
|
||||
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 }}
|
||||
27
.github/workflows/ticket-refinement.yml
vendored
Normal file
27
.github/workflows/ticket-refinement.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Ticket refinement
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22
|
||||
steps:
|
||||
- name: Fetch code
|
||||
run: |
|
||||
git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
|
||||
git checkout ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Refine ticket
|
||||
run: npx -y -p @ztimson/ai-agents@latest refine
|
||||
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 }}
|
||||
TICKET: ${{ github.event.issue.number }}
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env.local
|
||||
84
README.md
84
README.md
@@ -3,94 +3,48 @@
|
||||
<br />
|
||||
|
||||
<!-- Logo -->
|
||||
<img src="https://git.zakscode.com/repo-avatars/2b4ee6ba1f2e2618bf7694e4a52fb56d1d0ea6abafa2dcbe496ab786b86d5a76" alt="Logo" width="200" height="200">
|
||||
<img src="https://git.zakscode.com/repo-avatars/309c233243bcd1c1e9b3f359ec3f59769bb01b655e8ed7b32587781be4c8b21c" alt="Logo" width="200" height="200">
|
||||
|
||||
<!-- Title -->
|
||||
### Template
|
||||
### AI Agents
|
||||
|
||||
<!-- Description -->
|
||||
Simple repository template
|
||||
AI-powered Gitea agents for automating reviews and administration
|
||||
|
||||
<!-- Repo badges -->
|
||||
[](https://git.zakscode.com/ztimson/template/tags)
|
||||
[](https://git.zakscode.com/ztimson/template/pulls)
|
||||
[](https://git.zakscode.com/ztimson/template/issues)
|
||||
[](https://git.zakscode.com/ztimson/ai-agents/tags)
|
||||
[](https://git.zakscode.com/ztimson/ai-agents/pulls)
|
||||
[](https://git.zakscode.com/ztimson/ai-agents/issues)
|
||||
|
||||
<!-- Links -->
|
||||
|
||||
---
|
||||
<div>
|
||||
<a href="https://git.zakscode.com/ztimson/template/wiki" target="_blank">Documentation</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/template/releases" target="_blank">Release Notes</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/template/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/template/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
|
||||
<a href="https://git.zakscode.com/ztimson/ai-agents/releases" target="_blank">Release Notes</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/ai-agents/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/ai-agents/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
- [Template](#top)
|
||||
- [AI Agents](#top)
|
||||
- [About](#about)
|
||||
- [Demo](#demo)
|
||||
- [Built With](#built-with)
|
||||
- [Setup](#setup)
|
||||
- [Production](#production)
|
||||
- [Development](#development)
|
||||
- [License](#license)
|
||||
|
||||
## About
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
Only supports Gitea
|
||||
|
||||
### Demo
|
||||
|
||||
Website: https://git.zakscode.com
|
||||
Use LLM models from Anthropic, OpenAI, or Ollama to automate ticket refinement, code reviews, and releases.
|
||||
|
||||
### Built With
|
||||
[](https://angular.io/)
|
||||
[](https://www.android.com/)
|
||||
[](https://www.arduino.cc/)
|
||||
[](https://getbootstrap.com)
|
||||
[](https://en.cppreference.com/w/c/language)
|
||||
[](https://cplusplus.com/)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://www.w3.org/Style/CSS/Overview.en.html)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://docker.com/)
|
||||
[](https://www.electronjs.org/)
|
||||
[](https://firebase.google.com/)
|
||||
[](https://go.dev/)
|
||||
[](https://graphql.org/)
|
||||
[](https://developer.mozilla.org/en-US/docs/Glossary/HTML)
|
||||
[](https://java.com/)
|
||||
[](https://javascript.com/)
|
||||
[](https://jquery.com )
|
||||
[](https://laravel.com)
|
||||
[](https://www.linux.org/)
|
||||
[](https://git.zakscode.com/ztimson/momentum)
|
||||
[](https://www.mongodb.com/)
|
||||
[](https://www.mysql.com/)
|
||||
[](https://nestjs.com/)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://nextjs.org/)
|
||||
[](https://www.nginx.com/)
|
||||
[](https://nodejs.org/)
|
||||
[](https://p5js.org/)
|
||||
[](https://www.php.net/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.python.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://redis.com/)
|
||||
[](https://sass-lang.com/)
|
||||
[](https://en.wikipedia.org/wiki/Shell_script)
|
||||
[](https://www.microsoft.com/en-ca/sql-server)
|
||||
[](https://www.sqlite.org/index.html)
|
||||
[](https://svelte.dev/)
|
||||
[](https://typescriptlang.org/)
|
||||
[](https://microsoft.com/windows)
|
||||
[](https://vitejs.dev/)
|
||||
[](https://vuejs.org/)
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -102,11 +56,11 @@ Website: https://git.zakscode.com
|
||||
</summary>
|
||||
|
||||
#### Prerequisites
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
|
||||
#### Instructions
|
||||
1. Run the docker image: `docker run -p 80:80 git.zakscode.com/ztimson/template:latest`
|
||||
2. Open [http://localhost](http://localhost)
|
||||
1. Run using npx: `npx -y @ztimson/ai-agents@latest review`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -120,13 +74,13 @@ Website: https://git.zakscode.com
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
|
||||
#### Instructions
|
||||
1. Install the dependencies: `npm install`
|
||||
2. Start the Angular server: `npm run start`
|
||||
3. Open [http://localhost:4200](http://localhost:4200)
|
||||
1. Install the dependencies: `npm i`
|
||||
2. Build library: `npm run review`
|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
Copyright © 2023 Zakary Timson | All Rights Reserved | Available under MIT Licensing
|
||||
|
||||
Copyright © 2025 Zakary Timson | All Rights Reserved | Available under MIT Licensing
|
||||
|
||||
See the [license](./LICENSE) for more information.
|
||||
|
||||
1757
package-lock.json
generated
Normal file
1757
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@ztimson/ai-agents",
|
||||
"version": "0.1.3",
|
||||
"description": "AI agents",
|
||||
"keywords": ["ai", "review"],
|
||||
"author": "ztimson",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"refine": "./src/refine.mjs",
|
||||
"release": "./src/release.mjs",
|
||||
"review": "./src/review.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ztimson/ai-utils": "^0.2.4",
|
||||
"@ztimson/node-utils": "^1.0.7",
|
||||
"@ztimson/utils": "^0.28.3",
|
||||
"dotenv": "^17.2.3"
|
||||
},
|
||||
"files": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
217
src/refine.mjs
Normal file
217
src/refine.mjs
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {Ai} from '@ztimson/ai-utils';
|
||||
import * as os from 'node:os';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
dotenv.config({quiet: true, debug: false});
|
||||
dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
||||
|
||||
(async () => {
|
||||
let p = process.argv[process.argv.length - 1];
|
||||
if(p === 'refine' || p.endsWith('refine.mjs')) p = null;
|
||||
if(!/^(\/|[A-Z]:)/m.test(p)) p = path.join(process.cwd(), p);
|
||||
|
||||
const git = process.env['GIT_HOST'],
|
||||
owner = process.env['GIT_OWNER'],
|
||||
repo = process.env['GIT_REPO'],
|
||||
auth = process.env['GIT_TOKEN'],
|
||||
ticket = process.env['TICKET'],
|
||||
host = process.env['AI_HOST'],
|
||||
model = process.env['AI_MODEL'],
|
||||
labelDupe = process.env['LABELS_DUPE'] || 'Review/Duplicate',
|
||||
labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
|
||||
labelsReq = process.env['LABELS_REQ'] || 'Kind/Aesthetic,Kind/Bug,Kind/DevOps,Kind/Document,Kind/Enhancement,Kind/Refactor,Kind/Security',
|
||||
labelsOpt = process.env['LABELS_OPT'] || 'Breaking,Priority,QA',
|
||||
token = process.env['AI_TOKEN'];
|
||||
|
||||
console.log(`Processing issue #${ticket}`);
|
||||
|
||||
// Fetch issue
|
||||
const issueData = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
|
||||
headers: {'Authorization': `token ${auth}`}
|
||||
}).then(async resp => {
|
||||
if(resp.ok) return resp.json();
|
||||
else throw new Error(`${resp.status} ${await resp.text()}`);
|
||||
});
|
||||
if(issueData.labels?.length !== 1 || issueData.labels[0]?.name !== labelEnabled) {
|
||||
console.log('Skipping');
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
// Gather readme & template
|
||||
let title = '', labels = [], readme = '', readmeP = path.join(process.cwd(), 'README.md');
|
||||
if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8');
|
||||
const template = p ? fs.readFileSync(p, 'utf-8') : `## Description
|
||||
|
||||
A clear explanation of the request
|
||||
|
||||
---
|
||||
|
||||
## Current Behavior
|
||||
|
||||
what's happening now or the current state/gap
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
What should happen instead
|
||||
|
||||
## Steps to Reproduce / Desired Flow
|
||||
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Third step
|
||||
|
||||
## Additional Context
|
||||
|
||||
Logs, screenshots, links, related issues
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Todo requirement
|
||||
- [X] Completed requirement
|
||||
|
||||
## Technical Notes
|
||||
|
||||
Implementation details, constraints, dependencies, design decisions
|
||||
|
||||
|
||||
| Effort / Weight | Score |
|
||||
|-----------------|----------|
|
||||
| Size | 0-5 |
|
||||
| Complexity | 0-5 |
|
||||
| Unknowns | 0-5 |
|
||||
| **Total** | **0-15** |
|
||||
`;
|
||||
|
||||
// Create AI
|
||||
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],
|
||||
path: process.env['path'] || os.tmpdir(),
|
||||
tools: [{
|
||||
name: 'title',
|
||||
description: 'Set the ticket title, must be called EXACTLY ONCE',
|
||||
args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}},
|
||||
fn: (args) => title = args.title
|
||||
}, {
|
||||
name: 'add_label',
|
||||
description: 'Add a label to the ticket',
|
||||
args: {label: {type: 'string', description: 'Label name', required: true}},
|
||||
fn: async (args) => {
|
||||
labels.push(args.label);
|
||||
return await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
||||
body: `{"labels":["${args.label}"]}`
|
||||
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
||||
}
|
||||
}],
|
||||
system: `Transform raw tickets into structured markdown following the template EXACTLY.
|
||||
|
||||
**MANDATORY STEPS:**
|
||||
1. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
|
||||
2. Identify one label from each group which best applies to the ticket: ${labelsReq.replaceAll(',', ', ')}
|
||||
3. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
|
||||
4. Filter the following labels to any that apply to this ticket: ${labelsOpt.replaceAll(',', ', ')}
|
||||
5. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
|
||||
6. Output the new ticket description in formatted markdown matching the following rules:
|
||||
|
||||
**TEMPLATE RULES:**
|
||||
- Use ## headers (match template exactly)
|
||||
- Description: Clear summary of the request
|
||||
- Current Behavior: What's happening now (remove for Document tickets)
|
||||
- Expected Behavior: What should happen (remove for Document tickets)
|
||||
- Steps to Reproduce: Numbered list for bugs, flow for enhancements, remove if not applicable
|
||||
- Additional Context: Logs, screenshots, links provided by user
|
||||
- Acceptance Criteria: Convert to checkboxes (- [ ] format)
|
||||
- Technical Notes: Implementation approach, constraints, dependencies
|
||||
- Weight table (use exact format below):
|
||||
|
||||
| Effort / Weight | Score |
|
||||
|-----------------|----------|
|
||||
| Size | 0-5 |
|
||||
| Complexity | 0-5 |
|
||||
| Unknowns | 0-5 |
|
||||
| **Total** | **0-15** |
|
||||
|
||||
**SCORING:**
|
||||
- Size: # of modules/layers/files affected
|
||||
- Complexity: Technical difficulty
|
||||
- Unknowns: Research/uncertainty needed
|
||||
|
||||
**PROJECT README:**
|
||||
\`\`\`markdown
|
||||
${readme.trim() || 'No README available'}
|
||||
\`\`\`
|
||||
|
||||
**TEMPLATE:**
|
||||
\`\`\`markdown
|
||||
${template.trim()}
|
||||
\`\`\`
|
||||
|
||||
Output ONLY markdown. No explanations, labels, or extra formatting.`});
|
||||
|
||||
// Format ticket with AI
|
||||
const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []);
|
||||
const body = messages?.pop()?.content;
|
||||
if(!body) throw new Error('Invalid response from AI');
|
||||
|
||||
// Check for duplicates
|
||||
const repoInfo = await fetch(`${git}/api/v1/repos/${owner}/${repo}`, {headers: {'Authorization': `token ${auth}`},}).then(resp => resp.ok ? resp.json() : null);
|
||||
const search = await fetch(`${git}/api/v1/repos/issues/search`, {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
owner,
|
||||
priority_repo_id: repoInfo.id,
|
||||
type: 'issues',
|
||||
limit: 3,
|
||||
q: title
|
||||
})
|
||||
}).then(resp => resp.ok ? resp.json() : []);
|
||||
let dupeId = null;
|
||||
const dupeIds = search.map(t => t.id);
|
||||
const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n');
|
||||
const hasDuplicates = (await ai.language.ask(`ID: ${issueData.id}\nTitle: ${title}\n\`\`\`markdown\n${body}\n\`\`\``, {
|
||||
system: `Your job is to identify duplicates. Respond ONLY with the duplicate's ID number or "NONE" if no match exists\n\n${dupes}`
|
||||
}))?.pop()?.content;
|
||||
|
||||
// Handle duplicates
|
||||
if(hasDuplicates && !hasDuplicates.toUpperCase().includes('NONE') && (dupeId = dupeIds.find(id => id == hasDuplicates.trim())) != null && dupeId != issueData.id) {
|
||||
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
||||
body: `{"body": "Duplicate of #${dupeId}"}`
|
||||
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
||||
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
||||
body: `{"labels":["${labelDupe}"]}`
|
||||
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
||||
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
|
||||
method: 'PATCH',
|
||||
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
||||
body: '{"state": "closed"}'
|
||||
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
||||
console.log('Duplicate');
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
// Update ticket
|
||||
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
|
||||
method: 'PATCH',
|
||||
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({title, body})
|
||||
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
||||
|
||||
console.log(`Title: ${title}\nLabels: ${labels.join(', ')}\nBody:\n${body}`);
|
||||
})().catch(err => {
|
||||
console.error(`Error: ${err.message || err.toString()}`);
|
||||
process.exit(1);
|
||||
});
|
||||
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);
|
||||
});
|
||||
132
src/review.mjs
Normal file
132
src/review.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {Ai} from '@ztimson/ai-utils';
|
||||
import {$} from '@ztimson/node-utils';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({quiet: true, debug: false});
|
||||
dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
||||
|
||||
(async () => {
|
||||
let p = process.argv[process.argv.length - 1];
|
||||
if(p === 'review' || p.endsWith('review.mjs')) p = null;
|
||||
|
||||
const root = p || process.cwd(),
|
||||
git = process.env['GIT_HOST'],
|
||||
owner = process.env['GIT_OWNER'],
|
||||
repo = process.env['GIT_REPO'],
|
||||
auth = process.env['GIT_TOKEN'],
|
||||
labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
|
||||
pr = process.env['PULL_REQUEST'],
|
||||
host = process.env['AI_HOST'],
|
||||
model = process.env['AI_MODEL'],
|
||||
token = process.env['AI_TOKEN'];
|
||||
|
||||
console.log(`Reviewing: ${root}\n`);
|
||||
const info = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}`)
|
||||
.then(async resp => {
|
||||
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');
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
const branch = process.env['GIT_BRANCH'] || await $`cd ${root} && git symbolic-ref refs/remotes/origin/HEAD`;
|
||||
const comments = [];
|
||||
const commit = await $`cd ${root} && git log -1 --pretty=format:%H`;
|
||||
const gitDiff = await $`cd ${root} && git diff ${branch}`;
|
||||
|
||||
if(!gitDiff) {
|
||||
console.warn('No diff found');
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
let existingComments = 'Existing Comments:\n';
|
||||
if(git && pr) {
|
||||
const reviews = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews`, {
|
||||
headers: {'Authorization': `token ${auth}`}
|
||||
}).then(resp => resp.ok ? resp.json() : []);
|
||||
const comments = await Promise.all(reviews.map(r => fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews/${r.id}/comments`, {
|
||||
headers: {'Authorization': `token ${auth}`}
|
||||
}).then(resp => resp.ok ? resp.json() : [])));
|
||||
existingComments += comments.flat().map(c => `${c.path}:${c.position}\n${c.body}`).join('\n\n');
|
||||
}
|
||||
|
||||
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],
|
||||
path: process.env['path'] || os.tmpdir(),
|
||||
system: `You are a code reviewer. Analyze the git diff and use the \`recommend\` tool for EACH issue you find. You must call \`recommend\` exactly once for every bug or improvement opportunity directly related to changes. Ignore formatting recommendations. After making all recommendations, provide a quick 75 words or less sitrep.${existingComments}`,
|
||||
tools: [{
|
||||
name: 'read_file',
|
||||
description: 'Read contents of a file',
|
||||
args: {
|
||||
path: {type: 'string', description: 'Path to file relative to project root'}
|
||||
},
|
||||
fn: (args) => fs.readFileSync(path.join(root, args.path), 'utf-8')
|
||||
}, {
|
||||
name: 'get_diff',
|
||||
description: 'Check a file for differences using git',
|
||||
args: {
|
||||
path: {type: 'string', description: 'Path to file relative to project root'}
|
||||
},
|
||||
fn: async (args) => await $`git diff HEAD^..HEAD -- ${path.join(root, args.path)}`
|
||||
}, {
|
||||
name: 'recommend',
|
||||
description: 'REQUIRED: Call this once for every bug, improvement, or concern identified in the review.',
|
||||
args: {
|
||||
file: {type: 'string', description: 'File path'},
|
||||
line: {type: 'number', description: 'Line number in new file'},
|
||||
comment: {type: 'string', description: 'Review comment explaining the issue'}
|
||||
},
|
||||
fn: (args) => {
|
||||
comments.push({
|
||||
path: args.file,
|
||||
new_position: args.line,
|
||||
body: args.comment,
|
||||
});
|
||||
return 'Comment recorded, continue reviewing';
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
const messages = await ai.language.ask(`Title: ${info.title || 'None'}
|
||||
Description:
|
||||
\`\`\`md
|
||||
${info.body || 'None'}
|
||||
\`\`\`
|
||||
|
||||
Git Diff:
|
||||
\`\`\`
|
||||
${gitDiff}
|
||||
\`\`\``);
|
||||
const summary = messages.pop().content;
|
||||
if(git) {
|
||||
const res = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `token ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: summary,
|
||||
commit_id: commit,
|
||||
event: 'COMMENT',
|
||||
comments,
|
||||
})
|
||||
});
|
||||
if(!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
||||
}
|
||||
console.log(comments.map(c => `${c.path}${c.new_position ? `:${c.new_position}` : ''}\n${c.body}`).join('\n\n') + '\n\n' + summary);
|
||||
})().catch(err => {
|
||||
console.error(`Error: ${err.message || err.toString()}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user