feat(ai-pull-request): 以 opencode 分析 diff 自動產生並建立 Pull Request
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { log } from './util.js';
|
||||
|
||||
/**
|
||||
* 極簡的 Gitea API client,只實作這個 action 需要的 PR 相關操作。
|
||||
*/
|
||||
export class GiteaClient {
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {string} opts.serverUrl Gitea base URL(不含結尾斜線)
|
||||
* @param {string} opts.owner
|
||||
* @param {string} opts.repo
|
||||
* @param {string} opts.token
|
||||
*/
|
||||
constructor({ serverUrl, owner, repo, token }) {
|
||||
this.apiBase = `${serverUrl}/api/v1`;
|
||||
this.owner = owner;
|
||||
this.repo = repo;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
async _request(method, path, body) {
|
||||
const url = `${this.apiBase}${path}`;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `token ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
let json;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
json = { message: text };
|
||||
}
|
||||
return { ok: res.ok, status: res.status, json };
|
||||
}
|
||||
|
||||
/**
|
||||
* 查詢 head -> base 是否已存在開啟中的 PR。
|
||||
*
|
||||
* @param {string} head 來源分支
|
||||
* @param {string} base 目標分支
|
||||
* @returns {Promise<object|null>}
|
||||
*/
|
||||
async findOpenPull(head, base) {
|
||||
// Gitea pulls 不直接支援 head/base 過濾,這裡撈開啟中的 PR 自行比對
|
||||
const { ok, json } = await this._request(
|
||||
'GET',
|
||||
`/repos/${this.owner}/${this.repo}/pulls?state=open&limit=50`,
|
||||
);
|
||||
if (!ok || !Array.isArray(json)) return null;
|
||||
return (
|
||||
json.find(
|
||||
(pr) => pr?.head?.ref === head && pr?.base?.ref === base,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 Pull Request。若已存在相同 head/base 的 PR 則回傳既有 PR。
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.head 來源分支
|
||||
* @param {string} opts.base 目標分支
|
||||
* @param {string} opts.title
|
||||
* @param {string} opts.body
|
||||
* @returns {Promise<{ pull: object, created: boolean }>}
|
||||
*/
|
||||
async createPull({ head, base, title, body }) {
|
||||
log.info(`建立 PR: ${head} → ${base}`);
|
||||
const { ok, status, json } = await this._request(
|
||||
'POST',
|
||||
`/repos/${this.owner}/${this.repo}/pulls`,
|
||||
{ head, base, title, body },
|
||||
);
|
||||
|
||||
if (ok) {
|
||||
return { pull: json, created: true };
|
||||
}
|
||||
|
||||
// 422 通常代表 PR 已存在
|
||||
if (status === 422 || status === 409) {
|
||||
const existing = await this.findOpenPull(head, base);
|
||||
if (existing) {
|
||||
log.warn(`PR 已存在: #${existing.number}`);
|
||||
return { pull: existing, created: false };
|
||||
}
|
||||
}
|
||||
|
||||
const message = json?.message || JSON.stringify(json);
|
||||
throw new Error(`建立 PR 失敗 (${status}): ${message}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user