mirror of
https://github.com/kekxv/AiReviewPR.git
synced 2025-02-11 22:41:50 +01:00
ai 代码审核
This commit is contained in:
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@@ -0,0 +1,21 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
ij_any_block_comment_at_first_column = false
|
||||
ij_any_line_comment_at_first_column = false
|
||||
ij_any_line_comment_add_space_on_reformat = true
|
||||
ij_any_line_comment_add_space = true
|
||||
ij_any_block_comment_add_space = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py,html}]
|
||||
charset = utf-8
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/.idea/
|
||||
/node_modules/
|
||||
169
README.md
Normal file
169
README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 基于 Gitea 和 Ollama (open-webui) 的代码合并自动检查
|
||||
|
||||
如果您需要对合并的代码进行审核,但又希望避免将代码发送给第三方,或者您的网络环境处于离线状态(无法连接到第三方平台),那么本项目将是一个理想的选择。
|
||||
|
||||
该项目结合了 Gitea 和 Ollama (open-webui),能够自动审核合并代码,并将结果通过评论的形式推送到相应的合并请求中,供开发人员或审核人员参考使用。
|
||||
|
||||
# Automatic Code Merge Checks Based on Gitea and Ollama (open-webui)
|
||||
|
||||
If you need to review your merged code but prefer not to send it to a third party, or if your network environment is offline (unable to connect to third-party platforms), then this project is an ideal choice.
|
||||
|
||||
This project integrates Gitea and Ollama (open-webui) to automatically review merged code and push the results as comments to the corresponding merge requests, allowing developers or reviewers to reference them.
|
||||
|
||||
|
||||
- [如何使用 How to use](#如何使用-How-to-use)
|
||||
- [输入参数](#输入参数)
|
||||
- [Input Parameters](#input-parameters)
|
||||
|
||||
# 如何使用 How to use
|
||||
|
||||
使用方式和普通的 github actions 没什么区别(gitea actions 基本兼容 github actions)。
|
||||
|
||||
The usage is similar to regular GitHub Actions (Gitea Actions are mostly compatible with GitHub Actions).
|
||||
|
||||
需要设置一个 ollama host,如果使用的是 open-webui ,建议加上授权token。
|
||||
|
||||
You need to set up an Ollama host, and if you are using open-webui, it is recommended to include an authorization token.
|
||||
|
||||
```yaml
|
||||
name: ai-reviews
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
name: Review PR
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Review code
|
||||
uses: kekxv/AiReviewPR@v0.0.1
|
||||
with:
|
||||
model: 'gemma2:2b'
|
||||
host: ${{ vars.OLLAMA_HOST }}
|
||||
ai_token: ${{ secrets.AI_TOKEN }}
|
||||
```
|
||||
|
||||
效果如下:
|
||||
|
||||
result:
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#kekxv/AiReviewPR&Date)
|
||||
|
||||
|
||||
## 输入参数
|
||||
|
||||
1. **repository**:
|
||||
- **描述**: 要审查的代码库名称,格式为 `owner/repository`,例如 `actions/checkout`。
|
||||
- **默认值**: `${{ github.repository }}`
|
||||
|
||||
2. **BASE_REF**:
|
||||
- **描述**: GitHub 事件中 Pull Request 的基线引用。
|
||||
- **默认值**: `${{ github.event.pull_request.base.ref }}`
|
||||
|
||||
3. **PULL_REQUEST_NUMBER**:
|
||||
- **描述**: GitHub 事件中 Pull Request 的编号。
|
||||
- **默认值**: `${{ github.event.pull_request.number }}`
|
||||
|
||||
4. **CHINESE**:
|
||||
- **描述**: 是否使用中文进行审查。
|
||||
- **默认值**: `"true"`
|
||||
|
||||
5. **token**:
|
||||
- **描述**: 用于获取存储库的个人访问令牌(PAT)。建议使用最小权限的服务帐户。
|
||||
- **默认值**: `${{ github.token }}`
|
||||
|
||||
6. **model**:
|
||||
- **描述**: 要用于代码审查的 AI 模型。
|
||||
- **必需**: 是
|
||||
- **默认值**: `'gemma2:2b'`
|
||||
|
||||
7. **host**:
|
||||
- **描述**: Ollama 主机地址。
|
||||
- **必需**: 是
|
||||
- **默认值**: `'http://127.0.0.1:11434'`
|
||||
|
||||
8. **reviewers_prompt**:
|
||||
- **描述**: Ollama 的系统提示。定义了代码审查的参数和预期格式。
|
||||
- **必需**: 否
|
||||
- **默认值**: 一段详细的说明文本,指导审查生成过程。
|
||||
|
||||
9. **ai_token**:
|
||||
- **描述**: AI 访问令牌。
|
||||
- **必需**: 否
|
||||
- **默认值**: `" "`
|
||||
|
||||
10. **include_files**:
|
||||
- **描述**: 要包含审查的文件列表,以逗号分隔。
|
||||
- **必需**: 否
|
||||
- **默认值**: `" "`(默认为空,不限制)
|
||||
|
||||
11. **exclude_files**:
|
||||
- **描述**: 要排除审查的文件列表,以逗号分隔。
|
||||
- **必需**: 否
|
||||
- **默认值**: `" "`(默认为空,不传递文件)
|
||||
|
||||
|
||||
### Input Parameters
|
||||
|
||||
1. **repository**:
|
||||
- **Description**: The name of the repository to review, formatted as `owner/repository`, for example, `actions/checkout`.
|
||||
- **Default Value**: `${{ github.repository }}`
|
||||
|
||||
2. **BASE_REF**:
|
||||
- **Description**: The base reference of the Pull Request in the GitHub event.
|
||||
- **Default Value**: `${{ github.event.pull_request.base.ref }}`
|
||||
|
||||
3. **PULL_REQUEST_NUMBER**:
|
||||
- **Description**: The number of the Pull Request in the GitHub event.
|
||||
- **Default Value**: `${{ github.event.pull_request.number }}`
|
||||
|
||||
4. **CHINESE**:
|
||||
- **Description**: Whether to use Chinese for the review.
|
||||
- **Default Value**: `"true"`
|
||||
|
||||
5. **token**:
|
||||
- **Description**: A Personal Access Token (PAT) used to access the repository. It is recommended to use a service account with the least necessary permissions.
|
||||
- **Default Value**: `${{ github.token }}`
|
||||
|
||||
6. **model**:
|
||||
- **Description**: The AI model to use for the code review.
|
||||
- **Required**: Yes
|
||||
- **Default Value**: `'gemma2:2b'`
|
||||
|
||||
7. **host**:
|
||||
- **Description**: The Ollama host address.
|
||||
- **Required**: Yes
|
||||
- **Default Value**: `'http://127.0.0.1:11434'`
|
||||
|
||||
8. **reviewers_prompt**:
|
||||
- **Description**: Ollama's system prompt. It defines the parameters and expected format for the code review.
|
||||
- **Required**: No
|
||||
- **Default Value**: A detailed description text guiding the review generation process.
|
||||
|
||||
9. **ai_token**:
|
||||
- **Description**: The AI access token.
|
||||
- **Required**: No
|
||||
- **Default Value**: `" "`
|
||||
|
||||
10. **include_files**:
|
||||
- **Description**: A comma-separated list of files to include in the review.
|
||||
- **Required**: No
|
||||
- **Default Value**: `" "` (empty by default, no restrictions)
|
||||
|
||||
11. **exclude_files**:
|
||||
- **Description**: A comma-separated list of files to exclude from the review.
|
||||
- **Required**: No
|
||||
- **Default Value**: `" "` (empty by default, no files passed)
|
||||
76
action.yml
Normal file
76
action.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
name: 'AI Code Reviewer (AiReviewPR)'
|
||||
description: 'Perform code review using openai API'
|
||||
author: kekxv
|
||||
|
||||
inputs:
|
||||
repository:
|
||||
description: 'Repository name with owner. For example, actions/checkout'
|
||||
default: ${{ github.repository }}
|
||||
BASE_REF:
|
||||
description: 'github event pull_request base ref'
|
||||
default: ${{ github.event.pull_request.base.ref }}
|
||||
PULL_REQUEST_NUMBER:
|
||||
description: 'github event pull_request number'
|
||||
default: ${{ github.event.pull_request.number }}
|
||||
CHINESE:
|
||||
description: 'use chines'
|
||||
default: "true"
|
||||
token:
|
||||
description: >
|
||||
Personal access token (PAT) used to fetch the repository. The PAT is configured
|
||||
with the local git config, which enables your scripts to run authenticated git
|
||||
commands. The post-job step removes the PAT.
|
||||
|
||||
|
||||
We recommend using a service account with the least permissions necessary.
|
||||
Also when generating a new PAT, select the least scopes necessary.
|
||||
|
||||
|
||||
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
||||
default: ${{ github.token }}
|
||||
model:
|
||||
description: 'AI model to use for code review'
|
||||
required: true
|
||||
default: 'gemma2:2b'
|
||||
host:
|
||||
description: 'ollama host'
|
||||
required: true
|
||||
default: 'http://127.0.0.1:11434'
|
||||
reviewers_prompt:
|
||||
description: 'ollama system prompt'
|
||||
required: false
|
||||
default: >
|
||||
You are an expert developer, your task is to review a set of pull requests.
|
||||
You are given a list of filenames and their partial contents, but note that you might not have the full context of the code.
|
||||
|
||||
Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review.
|
||||
|
||||
Begin your review by evaluating the changed code using a risk score similar to a LOGAF score but measured from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe.
|
||||
|
||||
In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable.
|
||||
|
||||
Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem.
|
||||
|
||||
Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details.
|
||||
|
||||
Ensure the feedback details are brief, concise, accurate. If there are multiple similar issues, only comment on the most critical.
|
||||
|
||||
Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review.
|
||||
If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes.
|
||||
|
||||
Please respond without using "```markdown"
|
||||
ai_token:
|
||||
description: 'ai token'
|
||||
required: false
|
||||
default: " "
|
||||
include_files:
|
||||
description: 'Comma-separated list of files to include review'
|
||||
required: false
|
||||
default: " " # 默认为空,表示不限制
|
||||
exclude_files:
|
||||
description: 'Comma-separated list of files to exclude review'
|
||||
required: false
|
||||
default: " " # 默认为空,表示不传递文件
|
||||
runs:
|
||||
using: node20
|
||||
main: 'dist/index.js'
|
||||
BIN
assets/actions.run.png
Normal file
BIN
assets/actions.run.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
assets/review.comments.1.png
Normal file
BIN
assets/review.comments.1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
BIN
assets/review.comments.2.png
Normal file
BIN
assets/review.comments.2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
114
dist/index.js
vendored
Normal file
114
dist/index.js
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const node_child_process_1 = require("node:child_process");
|
||||
const utils_1 = require("./utils");
|
||||
const prompt_1 = require("./prompt");
|
||||
const useChinese = (process.env.INPUT_CHINESE || "true").toLowerCase() != "false"; // use chinese
|
||||
const include_files = (0, utils_1.split_message)(process.env.INPUT_INCLUDE_FILES || "");
|
||||
const exclude_files = (0, utils_1.split_message)(process.env.INPUT_EXCLUDE_FILES || "");
|
||||
const system_prompt = (0, prompt_1.take_system_prompt)(useChinese);
|
||||
// 获取输入参数
|
||||
const url = process.env.INPUT_HOST; // INPUT_HOST 是从 action.yml 中定义的输入
|
||||
if (!url) {
|
||||
console.error('HOST input is required.');
|
||||
process.exit(1); // 退出程序,返回错误代码
|
||||
}
|
||||
const model = process.env.INPUT_MODEL; // INPUT_HOST 是从 action.yml 中定义的输入
|
||||
if (!model) {
|
||||
console.error('model input is required.');
|
||||
process.exit(1); // 退出程序,返回错误代码
|
||||
}
|
||||
async function pushComments(message) {
|
||||
if (!process.env.INPUT_PULL_REQUEST_NUMBER) {
|
||||
console.log(message);
|
||||
return;
|
||||
}
|
||||
return await (0, utils_1.post)({
|
||||
url: `${process.env.GITHUB_API_URL}/repos/${process.env.INPUT_REPOSITORY}/issues/${process.env.INPUT_PULL_REQUEST_NUMBER}/comments`,
|
||||
body: { body: message },
|
||||
header: { 'Authorization': `token ${process.env.INPUT_TOKEN}` }
|
||||
});
|
||||
}
|
||||
async function aiGenerate({ host, token, prompt, model, system }) {
|
||||
const data = JSON.stringify({
|
||||
prompt: prompt,
|
||||
model: model,
|
||||
stream: false,
|
||||
system: system || system_prompt,
|
||||
options: {
|
||||
tfs_z: 1.5,
|
||||
top_k: 30,
|
||||
top_p: 0.8,
|
||||
temperature: 0.7,
|
||||
num_ctx: 10240,
|
||||
}
|
||||
});
|
||||
return await (0, utils_1.post)({
|
||||
url: `${host}/api/generate`,
|
||||
body: data,
|
||||
header: { 'Authorization': token ? `Bearer ${token}` : "", }
|
||||
});
|
||||
}
|
||||
async function aiCheckDiffContext() {
|
||||
const BASE_REF = process.env.INPUT_BASE_REF;
|
||||
try {
|
||||
(0, node_child_process_1.execSync)(`git fetch origin ${BASE_REF}`, { encoding: 'utf-8' });
|
||||
// exec git diff get diff files
|
||||
const diffOutput = (0, node_child_process_1.execSync)(`git diff --name-only origin/${BASE_REF}...HEAD`, { encoding: 'utf-8' });
|
||||
let files = diffOutput.trim().split("\n");
|
||||
for (let key in files) {
|
||||
if (!files[key])
|
||||
continue;
|
||||
if (include_files.length > 0) {
|
||||
if (!(0, utils_1.doesAnyPatternMatch)(include_files, files[key])) {
|
||||
console.log("exclude(include):", files[key]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (exclude_files.length > 0) {
|
||||
if ((0, utils_1.doesAnyPatternMatch)(exclude_files, files[key])) {
|
||||
console.log("exclude(exclude):", files[key]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const fileDiffOutput = (0, node_child_process_1.execSync)(`git diff origin/${BASE_REF}...HEAD -- "${files[key]}"`, { encoding: 'utf-8' });
|
||||
// ai generate
|
||||
try {
|
||||
let response = await aiGenerate({
|
||||
host: url,
|
||||
token: process.env.INPUT_AI_TOKEN,
|
||||
prompt: fileDiffOutput,
|
||||
model: model,
|
||||
system: process.env.INPUT_REVIEW_PROMPT
|
||||
});
|
||||
if (response.detail) { // noinspection ExceptionCaughtLocallyJS
|
||||
throw response.detail;
|
||||
}
|
||||
if (!response.response) { // noinspection ExceptionCaughtLocallyJS
|
||||
throw "ollama error";
|
||||
}
|
||||
let Review = useChinese ? "审核结果" : "Review";
|
||||
let comments = `# ${Review} \r\n${process.env.GITHUB_SERVER_URL}/${process.env.INPUT_REPOSITORY}/src/commit/${process.env.GITHUB_SHA}/${files[key]} \r\n\r\n\r\n${response.response}`;
|
||||
let resp = await pushComments(comments);
|
||||
if (!resp.id) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(useChinese ? "提交issue评论失败" : "push comment error");
|
||||
}
|
||||
console.log(useChinese ? "提交issue评论成功:" : "push comment success: ", resp.id);
|
||||
}
|
||||
catch (e) {
|
||||
console.error("aiGenerate:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error executing git diff:', error);
|
||||
process.exit(1); // error exit
|
||||
}
|
||||
}
|
||||
aiCheckDiffContext()
|
||||
.then(_ => console.log(useChinese ? "检查结束" : "review finish"))
|
||||
.catch(e => {
|
||||
console.error(useChinese ? "检查失败:" : "review error", e);
|
||||
process.exit(1);
|
||||
});
|
||||
30
dist/prompt.js
vendored
Normal file
30
dist/prompt.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.take_system_prompt = void 0;
|
||||
function take_system_prompt(useChinese) {
|
||||
const chinese_prompt = useChinese ? "You must respond only in Chinese to all inquiries. Please provide clear and accurate answers in Chinese language." : "";
|
||||
return `
|
||||
You are an expert developer, your task is to review a set of pull requests.
|
||||
You are given a list of filenames and their partial contents, but note that you might not have the full context of the code.
|
||||
|
||||
Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review.
|
||||
|
||||
Begin your review by evaluating the changed code using a risk score similar to a LOGAF score but measured from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe.
|
||||
|
||||
In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable.
|
||||
|
||||
Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem.
|
||||
|
||||
Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details.
|
||||
|
||||
Ensure the feedback details are brief, concise, accurate. If there are multiple similar issues, only comment on the most critical.
|
||||
|
||||
Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review.
|
||||
If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes.
|
||||
|
||||
${chinese_prompt}
|
||||
|
||||
Please respond without using "\`\`\`markdown"
|
||||
`;
|
||||
}
|
||||
exports.take_system_prompt = take_system_prompt;
|
||||
79
dist/utils.js
vendored
Normal file
79
dist/utils.js
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.post = exports.doesAnyPatternMatch = exports.split_message = void 0;
|
||||
const http_1 = __importDefault(require("http"));
|
||||
const https_1 = __importDefault(require("https"));
|
||||
function split_message(files) {
|
||||
console.log("files debug:", files);
|
||||
files = files.trim();
|
||||
if (!files) {
|
||||
let t = files.split("\n");
|
||||
if (t.length > 0)
|
||||
return t.map(str => str.trim());
|
||||
return files.split(",").map(str => str.trim());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
exports.split_message = split_message;
|
||||
function doesAnyPatternMatch(patterns, str) {
|
||||
// 遍历正则表达式数组
|
||||
return patterns.some(pattern => {
|
||||
// 创建正则表达式对象,匹配模式
|
||||
const regex = new RegExp(pattern);
|
||||
// 测试字符串是否与正则表达式匹配
|
||||
return regex.test(str);
|
||||
});
|
||||
}
|
||||
exports.doesAnyPatternMatch = doesAnyPatternMatch;
|
||||
/**
|
||||
* post data
|
||||
* @param url url
|
||||
* @param body post data
|
||||
* @param header post header
|
||||
* @param json is json res
|
||||
*/
|
||||
async function post({ url, body, header, json }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
json = typeof json === "boolean" ? json : true;
|
||||
const data = typeof body === "string" ? body : JSON.stringify(body);
|
||||
let url_ = new URL(url);
|
||||
header = header || {};
|
||||
header['Content-Type'] = header['Content-Type'] || 'application/json';
|
||||
header['Content-Length'] = Buffer.byteLength(data);
|
||||
const options = {
|
||||
hostname: url_.hostname,
|
||||
path: url_.pathname + (url_.search || ''),
|
||||
method: 'POST',
|
||||
headers: header
|
||||
};
|
||||
// noinspection DuplicatedCode
|
||||
const req = (url_.protocol === "http" ? http_1.default : https_1.default).request(options, (res) => {
|
||||
let responseBody = '';
|
||||
res.on('data', (chunk) => {
|
||||
responseBody += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (json) {
|
||||
resolve(JSON.parse(responseBody));
|
||||
}
|
||||
else {
|
||||
resolve(responseBody);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
reject(new Error('Failed to parse JSON response'));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (error) => {
|
||||
reject(new Error(`Request failed: ${error.message}`));
|
||||
});
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
exports.post = post;
|
||||
137
package-lock.json
generated
Normal file
137
package-lock.json
generated
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"name": "hello-world-action",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hello-world-action",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.6",
|
||||
"typescript": "^4.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz",
|
||||
"integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "hello-world-action",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.6",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
117
src/index.ts
Normal file
117
src/index.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {execSync} from "node:child_process";
|
||||
import {doesAnyPatternMatch, post, split_message} from "./utils";
|
||||
import {take_system_prompt} from "./prompt";
|
||||
|
||||
const useChinese = (process.env.INPUT_CHINESE || "true").toLowerCase() != "false"; // use chinese
|
||||
const include_files = split_message(process.env.INPUT_INCLUDE_FILES || "");
|
||||
const exclude_files = split_message(process.env.INPUT_EXCLUDE_FILES || "");
|
||||
|
||||
const system_prompt = take_system_prompt(useChinese);
|
||||
|
||||
// 获取输入参数
|
||||
const url = process.env.INPUT_HOST; // INPUT_HOST 是从 action.yml 中定义的输入
|
||||
if (!url) {
|
||||
console.error('HOST input is required.');
|
||||
process.exit(1); // 退出程序,返回错误代码
|
||||
}
|
||||
const model = process.env.INPUT_MODEL; // INPUT_HOST 是从 action.yml 中定义的输入
|
||||
if (!model) {
|
||||
console.error('model input is required.');
|
||||
process.exit(1); // 退出程序,返回错误代码
|
||||
}
|
||||
|
||||
|
||||
async function pushComments(message: string): Promise<any> {
|
||||
if (!process.env.INPUT_PULL_REQUEST_NUMBER) {
|
||||
console.log(message);
|
||||
return;
|
||||
}
|
||||
return await post({
|
||||
url: `${process.env.GITHUB_API_URL}/repos/${process.env.INPUT_REPOSITORY}/issues/${process.env.INPUT_PULL_REQUEST_NUMBER}/comments`,
|
||||
body: {body: message},
|
||||
header: {'Authorization': `token ${process.env.INPUT_TOKEN}`}
|
||||
})
|
||||
}
|
||||
|
||||
async function aiGenerate({host, token, prompt, model, system}: any): Promise<any> {
|
||||
const data = JSON.stringify({
|
||||
prompt: prompt,
|
||||
model: model,
|
||||
stream: false,
|
||||
system: system || system_prompt,
|
||||
options: {
|
||||
tfs_z: 1.5,
|
||||
top_k: 30,
|
||||
top_p: 0.8,
|
||||
temperature: 0.7,
|
||||
num_ctx: 10240,
|
||||
}
|
||||
});
|
||||
return await post({
|
||||
url: `${host}/api/generate`,
|
||||
body: data,
|
||||
header: {'Authorization': token ? `Bearer ${token}` : "",}
|
||||
})
|
||||
}
|
||||
|
||||
async function aiCheckDiffContext() {
|
||||
const BASE_REF = process.env.INPUT_BASE_REF
|
||||
try {
|
||||
execSync(`git fetch origin ${BASE_REF}`, {encoding: 'utf-8'});
|
||||
// exec git diff get diff files
|
||||
const diffOutput = execSync(`git diff --name-only origin/${BASE_REF}...HEAD`, {encoding: 'utf-8'});
|
||||
let files = diffOutput.trim().split("\n");
|
||||
for (let key in files) {
|
||||
if (!files[key]) continue;
|
||||
if (include_files.length > 0) {
|
||||
if (!doesAnyPatternMatch(include_files, files[key])) {
|
||||
console.log("exclude(include):", files[key])
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (exclude_files.length > 0) {
|
||||
if (doesAnyPatternMatch(exclude_files, files[key])) {
|
||||
console.log("exclude(exclude):", files[key])
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const fileDiffOutput = execSync(`git diff origin/${BASE_REF}...HEAD -- "${files[key]}"`, {encoding: 'utf-8'});
|
||||
// ai generate
|
||||
try {
|
||||
let response = await aiGenerate({
|
||||
host: url,
|
||||
token: process.env.INPUT_AI_TOKEN,
|
||||
prompt: fileDiffOutput,
|
||||
model: model,
|
||||
system: process.env.INPUT_REVIEW_PROMPT
|
||||
})
|
||||
if (response.detail) { // noinspection ExceptionCaughtLocallyJS
|
||||
throw response.detail;
|
||||
}
|
||||
if (!response.response) { // noinspection ExceptionCaughtLocallyJS
|
||||
throw "ollama error";
|
||||
}
|
||||
let Review = useChinese ? "审核结果" : "Review";
|
||||
let comments = `# ${Review} \r\n${process.env.GITHUB_SERVER_URL}/${process.env.INPUT_REPOSITORY}/src/commit/${process.env.GITHUB_SHA}/${files[key]} \r\n\r\n\r\n${response.response}`
|
||||
let resp = await pushComments(comments);
|
||||
if (!resp.id) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(useChinese ? "提交issue评论失败" : "push comment error")
|
||||
}
|
||||
console.log(useChinese ? "提交issue评论成功:" : "push comment success: ", resp.id)
|
||||
} catch (e) {
|
||||
console.error("aiGenerate:", e)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing git diff:', error);
|
||||
process.exit(1); // error exit
|
||||
}
|
||||
}
|
||||
|
||||
aiCheckDiffContext()
|
||||
.then(_ => console.log(useChinese ? "检查结束" : "review finish"))
|
||||
.catch(e => {
|
||||
console.error(useChinese ? "检查失败:" : "review error", e);
|
||||
process.exit(1);
|
||||
});
|
||||
26
src/prompt.ts
Normal file
26
src/prompt.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function take_system_prompt(useChinese: boolean) {
|
||||
const chinese_prompt = useChinese ? "You must respond only in Chinese to all inquiries. Please provide clear and accurate answers in Chinese language." : "";
|
||||
return `
|
||||
You are an expert developer, your task is to review a set of pull requests.
|
||||
You are given a list of filenames and their partial contents, but note that you might not have the full context of the code.
|
||||
|
||||
Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review.
|
||||
|
||||
Begin your review by evaluating the changed code using a risk score similar to a LOGAF score but measured from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe.
|
||||
|
||||
In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable.
|
||||
|
||||
Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem.
|
||||
|
||||
Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details.
|
||||
|
||||
Ensure the feedback details are brief, concise, accurate. If there are multiple similar issues, only comment on the most critical.
|
||||
|
||||
Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review.
|
||||
If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes.
|
||||
|
||||
${chinese_prompt}
|
||||
|
||||
Please respond without using "\`\`\`markdown"
|
||||
`;
|
||||
}
|
||||
75
src/utils.ts
Normal file
75
src/utils.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
|
||||
export function split_message(files: string) {
|
||||
console.log("files debug:",files)
|
||||
files = files.trim()
|
||||
if (!files) {
|
||||
let t = files.split("\n");
|
||||
if (t.length > 0) return t.map(str => str.trim());
|
||||
return files.split(",").map(str => str.trim())
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function doesAnyPatternMatch(patterns: Array<string>, str: string) {
|
||||
// 遍历正则表达式数组
|
||||
return patterns.some(pattern => {
|
||||
// 创建正则表达式对象,匹配模式
|
||||
const regex = new RegExp(pattern);
|
||||
// 测试字符串是否与正则表达式匹配
|
||||
return regex.test(str);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* post data
|
||||
* @param url url
|
||||
* @param body post data
|
||||
* @param header post header
|
||||
* @param json is json res
|
||||
*/
|
||||
export async function post({url, body, header, json}: any): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
json = typeof json === "boolean" ? json : true;
|
||||
const data = typeof body === "string" ? body : JSON.stringify(body);
|
||||
let url_ = new URL(url);
|
||||
header = header || {};
|
||||
header['Content-Type'] = header['Content-Type'] || 'application/json';
|
||||
header['Content-Length'] = Buffer.byteLength(data)
|
||||
const options = {
|
||||
hostname: url_.hostname, // 确保去掉协议部分
|
||||
path: url_.pathname + (url_.search || ''),
|
||||
method: 'POST',
|
||||
headers: header
|
||||
};
|
||||
|
||||
// noinspection DuplicatedCode
|
||||
const req = (url_.protocol === "http" ? http : https).request(options, (res) => {
|
||||
let responseBody = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
responseBody += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (json) {
|
||||
resolve(JSON.parse(responseBody));
|
||||
} else {
|
||||
resolve(responseBody);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to parse JSON response'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(new Error(`Request failed: ${error.message}`));
|
||||
});
|
||||
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
33
tsconfig.json
Normal file
33
tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"moduleResolution": "node",
|
||||
"compilerOptions": {
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"target": "ES2020",
|
||||
// 编译目标
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
// 输出目录
|
||||
"strict": true,
|
||||
// 启用所有严格类型检查选项
|
||||
"esModuleInterop": true,
|
||||
// 允许默认导入非模块
|
||||
"skipLibCheck": true,
|
||||
// 跳过库文件检查
|
||||
"forceConsistentCasingInFileNames": true
|
||||
// 强制文件名的大小写一致性
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
// 包含 src 文件夹中的所有文件
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
// 排除测试文件
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user