fix: issues with pagination not resolving correct url templates
This commit is contained in:
@@ -187,11 +187,11 @@ export async function main(): Promise<MainResult> {
|
|||||||
|
|
||||||
let defaultBranch: string | undefined;
|
let defaultBranch: string | undefined;
|
||||||
try {
|
try {
|
||||||
const repoData = await gitea.rest.repository.repoGet({
|
const repoData = await gitea.request(
|
||||||
owner: repoContext.owner,
|
"GET /repos/{owner}/{repo}",
|
||||||
repo: repoContext.name,
|
{ owner: repoContext.owner, repo: repoContext.name }
|
||||||
});
|
);
|
||||||
defaultBranch = repoData.data.default_branch;
|
defaultBranch = (repoData.data as { default_branch?: string }).default_branch;
|
||||||
} catch {
|
} catch {
|
||||||
defaultBranch = "main";
|
defaultBranch = "main";
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-24
@@ -136,15 +136,13 @@ export async function fetchAndFormatPrDiff(
|
|||||||
ctx: ToolContext,
|
ctx: ToolContext,
|
||||||
pullNumber: number
|
pullNumber: number
|
||||||
): Promise<FetchAndFormatPrDiffResult> {
|
): Promise<FetchAndFormatPrDiffResult> {
|
||||||
const raw = await ctx.gitea.paginate(ctx.gitea.rest.repository.repoGetPullRequestFiles, {
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/pulls/{index}/files",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pullNumber, limit: 50 }
|
||||||
index: pullNumber,
|
);
|
||||||
});
|
const files: DiffFile[] = (r.data as Array<{ filename?: string; patch?: string }>).map((f) => ({
|
||||||
// ChangedFile from the SDK omits `patch`; Gitea does return it, so we map here.
|
|
||||||
const files: DiffFile[] = raw.map((f) => ({
|
|
||||||
filename: f.filename,
|
filename: f.filename,
|
||||||
patch: (f as unknown as { patch?: string }).patch,
|
patch: f.patch,
|
||||||
}));
|
}));
|
||||||
return { ...formatFilesWithLineNumbers(files), files };
|
return { ...formatFilesWithLineNumbers(files), files };
|
||||||
}
|
}
|
||||||
@@ -190,7 +188,8 @@ type CheckoutPrBranchParams = {
|
|||||||
|
|
||||||
async function abortIfPullRequestMoved(args: { gitea: Gitea; owner: string; repo: string; pr: PrData }): Promise<void> {
|
async function abortIfPullRequestMoved(args: { gitea: Gitea; owner: string; repo: string; pr: PrData }): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const data = (await args.gitea.rest.repository.repoGetPullRequest({ owner: args.owner, repo: args.repo, index: args.pr.number })).data;
|
const r = await args.gitea.request("GET /repos/{owner}/{repo}/pulls/{index}", { owner: args.owner, repo: args.repo, index: args.pr.number });
|
||||||
|
const data = r.data as { state?: string; head?: { sha?: string } };
|
||||||
if (data.state !== "open" || data.head?.sha !== args.pr.headSha) {
|
if (data.state !== "open" || data.head?.sha !== args.pr.headSha) {
|
||||||
throw new Error(`PR #${args.pr.number} is no longer in the state it was at dispatch. Aborting.`);
|
throw new Error(`PR #${args.pr.number} is no longer in the state it was at dispatch. Aborting.`);
|
||||||
}
|
}
|
||||||
@@ -203,15 +202,14 @@ async function abortIfPullRequestMoved(args: { gitea: Gitea; owner: string; repo
|
|||||||
type CreateTempBranchParams = { gitea: Gitea; owner: string; repo: string; branchName: string; sha: string };
|
type CreateTempBranchParams = { gitea: Gitea; owner: string; repo: string; branchName: string; sha: string };
|
||||||
|
|
||||||
async function createTempBranch(params: CreateTempBranchParams) {
|
async function createTempBranch(params: CreateTempBranchParams) {
|
||||||
await params.gitea.rest.repository.repoCreateBranch({
|
await params.gitea.request("POST /repos/{owner}/{repo}/branches", {
|
||||||
owner: params.owner,
|
owner: params.owner, repo: params.repo,
|
||||||
repo: params.repo,
|
new_branch_name: params.branchName, old_ref_name: params.sha,
|
||||||
body: { new_branch_name: params.branchName, old_ref_name: params.sha },
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
async [Symbol.asyncDispose]() {
|
async [Symbol.asyncDispose]() {
|
||||||
try {
|
try {
|
||||||
await params.gitea.rest.repository.repoDeleteBranch({ owner: params.owner, repo: params.repo, branch: params.branchName });
|
await params.gitea.request("DELETE /repos/{owner}/{repo}/branches/{branch}", { owner: params.owner, repo: params.repo, branch: params.branchName });
|
||||||
log.debug(`» deleted temp branch ${params.branchName}`);
|
log.debug(`» deleted temp branch ${params.branchName}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.debug(`» failed to delete temp branch ${params.branchName}: ${e instanceof Error ? e.message : String(e)}`);
|
log.debug(`» failed to delete temp branch ${params.branchName}: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
@@ -300,13 +298,13 @@ export async function checkoutPrBranch(
|
|||||||
let deepenDepth = 0;
|
let deepenDepth = 0;
|
||||||
try {
|
try {
|
||||||
const [prComp, beforeComp] = await Promise.all([
|
const [prComp, beforeComp] = await Promise.all([
|
||||||
gitea.rest.repository.repoCompareDiff({ owner, repo: name, basehead: `${pr.baseRef}...${toolState.checkoutSha}` }),
|
gitea.request("GET /repos/{owner}/{repo}/compare/{basehead}", { owner, repo: name, basehead: `${pr.baseRef}...${toolState.checkoutSha}` }),
|
||||||
beforeSha && beforeShaReachable
|
beforeSha && beforeShaReachable
|
||||||
? gitea.rest.repository.repoCompareDiff({ owner, repo: name, basehead: `${pr.baseRef}...${beforeSha}` })
|
? gitea.request("GET /repos/{owner}/{repo}/compare/{basehead}", { owner, repo: name, basehead: `${pr.baseRef}...${beforeSha}` })
|
||||||
: undefined,
|
: undefined,
|
||||||
]);
|
]);
|
||||||
const prTotal = prComp.data.total_commits ?? 0;
|
const prTotal = (prComp.data as { total_commits?: number }).total_commits ?? 0;
|
||||||
const beforeTotal = beforeComp?.data.total_commits ?? 0;
|
const beforeTotal = (beforeComp?.data as { total_commits?: number } | undefined)?.total_commits ?? 0;
|
||||||
deepenDepth = Math.max(prTotal, beforeTotal) + 10;
|
deepenDepth = Math.max(prTotal, beforeTotal) + 10;
|
||||||
log.debug(`» compare: PR=${prTotal}, before=${beforeTotal}, deepen=${deepenDepth}`);
|
log.debug(`» compare: PR=${prTotal}, before=${beforeTotal}, deepen=${deepenDepth}`);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -358,12 +356,17 @@ function describeHead(h: InitialHead): string {
|
|||||||
|
|
||||||
export function CheckoutPrTool(ctx: ToolContext) {
|
export function CheckoutPrTool(ctx: ToolContext) {
|
||||||
const runCheckout = async (pull_number: number): Promise<CheckoutPrResult> => {
|
const runCheckout = async (pull_number: number): Promise<CheckoutPrResult> => {
|
||||||
const prResult = await ctx.gitea.rest.repository.repoGetPullRequest({
|
const prResult = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/pulls/{index}",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number }
|
||||||
index: pull_number,
|
);
|
||||||
});
|
const prData = prResult.data as {
|
||||||
const prData = prResult.data;
|
number?: number; title?: string; body?: string | null; html_url?: string;
|
||||||
|
merged?: boolean; allow_maintainer_edit?: boolean;
|
||||||
|
head?: { sha?: string; ref?: string; repo?: { full_name?: string } | null };
|
||||||
|
base?: { ref?: string; repo?: { full_name?: string } };
|
||||||
|
state?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const headRepo = prData.head?.repo;
|
const headRepo = prData.head?.repo;
|
||||||
if (!headRepo) throw new Error(`PR #${pull_number} source repository was deleted`);
|
if (!headRepo) throw new Error(`PR #${pull_number} source repository was deleted`);
|
||||||
|
|||||||
+41
-121
@@ -15,57 +15,41 @@ export {
|
|||||||
LEAPING_INTO_ACTION_PREFIX,
|
LEAPING_INTO_ACTION_PREFIX,
|
||||||
} from "../utils/leapingComment.ts";
|
} from "../utils/leapingComment.ts";
|
||||||
|
|
||||||
|
interface GiteaComment { id: number; body?: string | null; html_url?: string; updated_at?: string }
|
||||||
|
|
||||||
function buildCommentFooter(ctx: ToolContext): string {
|
function buildCommentFooter(ctx: ToolContext): string {
|
||||||
return buildShockbotFooter({ model: ctx.toolState.model });
|
return buildShockbotFooter({ model: ctx.toolState.model });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addFooter(ctx: ToolContext, body: string): string {
|
export function addFooter(ctx: ToolContext, body: string): string {
|
||||||
if (/<br\s*\/?>[ \t]*\n(?!\s*\n)/i.test(body)) {
|
if (/<br\s*\/?>[ \t]*\n(?!\s*\n)/i.test(body)) {
|
||||||
throw new Error(
|
throw new Error("body contains <br/> followed by a non-blank line — add a blank line after <br/> tags.");
|
||||||
"body contains <br/> followed by a non-blank line, which breaks Gitea markdown rendering. always add a blank line after <br/> tags."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
return `${stripExistingFooter(fixDoubleEscapedString(body))}${buildCommentFooter(ctx)}`;
|
||||||
const footer = buildCommentFooter(ctx);
|
|
||||||
return `${bodyWithoutFooter}${footer}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Comment = type({
|
export const Comment = type({
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
body: type.string.describe("the comment body content"),
|
body: type.string.describe("the comment body content"),
|
||||||
type: type
|
type: type.enumerated("Plan", "Comment").optional(),
|
||||||
.enumerated("Plan", "Comment")
|
|
||||||
.describe("Plan: record as the plan for this run. Comment: regular comment (default).")
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function CreateCommentTool(ctx: ToolContext) {
|
export function CreateCommentTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_issue_comment",
|
name: "create_issue_comment",
|
||||||
description:
|
description:
|
||||||
"Create a comment on a Gitea issue or PR. " +
|
"Create a comment on a Gitea issue or PR. For progress/plan updates use report_progress instead.",
|
||||||
'Example: `create_issue_comment({ issueNumber: 1234, body: "Thanks for the report." })`. ' +
|
|
||||||
"For progress/plan updates use report_progress instead.",
|
|
||||||
parameters: Comment,
|
parameters: Comment,
|
||||||
execute: execute(async ({ issueNumber, body }) => {
|
execute: execute(async ({ issueNumber, body }) => {
|
||||||
const bodyWithFooter = addFooter(ctx, body);
|
const bodyWithFooter = addFooter(ctx, body);
|
||||||
|
const r = await ctx.gitea.request(
|
||||||
const result = await ctx.gitea.rest.issue.issueCreateComment({
|
"POST /repos/{owner}/{repo}/issues/{index}/comments",
|
||||||
owner: ctx.repo.owner,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: issueNumber, body: bodyWithFooter }
|
||||||
repo: ctx.repo.name,
|
);
|
||||||
index: issueNumber,
|
const data = r.data as GiteaComment;
|
||||||
body: { body: bodyWithFooter },
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
log.info(`» created comment ${result.data.id}`);
|
log.info(`» created comment ${data.id}`);
|
||||||
|
return { success: true, commentId: data.id, url: data.html_url, body: data.body };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
commentId: result.data.id,
|
|
||||||
url: result.data.html_url,
|
|
||||||
body: result.data.body,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -82,48 +66,30 @@ export function EditCommentTool(ctx: ToolContext) {
|
|||||||
parameters: EditComment,
|
parameters: EditComment,
|
||||||
execute: execute(async ({ commentId, body }) => {
|
execute: execute(async ({ commentId, body }) => {
|
||||||
const bodyWithFooter = addFooter(ctx, body);
|
const bodyWithFooter = addFooter(ctx, body);
|
||||||
|
const r = await ctx.gitea.request(
|
||||||
const result = await ctx.gitea.rest.issue.issueEditComment({
|
"PATCH /repos/{owner}/{repo}/issues/comments/{id}",
|
||||||
owner: ctx.repo.owner,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, id: commentId, body: bodyWithFooter }
|
||||||
repo: ctx.repo.name,
|
);
|
||||||
id: commentId,
|
const data = r.data as GiteaComment;
|
||||||
body: { body: bodyWithFooter },
|
log.info(`» updated comment ${data.id}`);
|
||||||
});
|
return { success: true, commentId: data.id, url: data.html_url, body: data.body, updatedAt: data.updated_at };
|
||||||
log.info(`» updated comment ${result.data.id}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
commentId: result.data.id,
|
|
||||||
url: result.data.html_url,
|
|
||||||
body: result.data.body,
|
|
||||||
updatedAt: result.data.updated_at,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReportProgress = type({
|
export const ReportProgress = type({
|
||||||
body: type.string.describe("the progress update content to share"),
|
body: type.string.describe("the progress update content to share"),
|
||||||
"target_plan_comment?": type("boolean").describe(
|
"target_plan_comment?": type("boolean"),
|
||||||
"for revising an existing plan comment ONLY."
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function reportProgress(
|
export async function reportProgress(
|
||||||
ctx: ToolContext,
|
ctx: ToolContext,
|
||||||
params: { body: string; target_plan_comment?: boolean }
|
params: { body: string; target_plan_comment?: boolean }
|
||||||
): Promise<{
|
): Promise<{ commentId?: number; url?: string; body: string; action: "created" | "updated" | "skipped" }> {
|
||||||
commentId?: number;
|
|
||||||
url?: string;
|
|
||||||
body: string;
|
|
||||||
action: "created" | "updated" | "skipped";
|
|
||||||
}> {
|
|
||||||
const { body, target_plan_comment } = params;
|
const { body, target_plan_comment } = params;
|
||||||
ctx.toolState.lastProgressBody = body;
|
ctx.toolState.lastProgressBody = body;
|
||||||
|
|
||||||
if (ctx.payload.event.silent) {
|
if (ctx.payload.event.silent) return { body, action: "skipped" };
|
||||||
return { body, action: "skipped" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
||||||
const apiCtx = { gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name };
|
const apiCtx = { gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name };
|
||||||
@@ -145,30 +111,14 @@ export async function reportProgress(
|
|||||||
return { commentId: result.id, url: result.html_url, body: result.body || "", action: "updated" };
|
return { commentId: result.id, url: result.html_url, body: result.body || "", action: "updated" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingComment === null) {
|
if (existingComment === null) return { body, action: "skipped" };
|
||||||
return { body, action: "skipped" };
|
if (issueNumber === undefined) return { body, action: "skipped" };
|
||||||
}
|
|
||||||
|
|
||||||
if (issueNumber === undefined) {
|
|
||||||
return { body, action: "skipped" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialBody = addFooter(ctx, body);
|
const initialBody = addFooter(ctx, body);
|
||||||
const created = await createLeapingProgressComment(
|
const created = await createLeapingProgressComment(apiCtx, { kind: "issue", issueNumber }, initialBody);
|
||||||
apiCtx,
|
|
||||||
{ kind: "issue", issueNumber },
|
|
||||||
initialBody
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.toolState.progressComment = created.comment;
|
ctx.toolState.progressComment = created.comment;
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
|
return { commentId: created.comment.id, url: created.html_url, body: created.body || "", action: "created" };
|
||||||
return {
|
|
||||||
commentId: created.comment.id,
|
|
||||||
url: created.html_url,
|
|
||||||
body: created.body || "",
|
|
||||||
action: "created",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportProgressTool(ctx: ToolContext) {
|
export function ReportProgressTool(ctx: ToolContext) {
|
||||||
@@ -180,26 +130,18 @@ export function ReportProgressTool(ctx: ToolContext) {
|
|||||||
parameters: ReportProgress,
|
parameters: ReportProgress,
|
||||||
execute: execute(async (params) => {
|
execute: execute(async (params) => {
|
||||||
let body = params.body;
|
let body = params.body;
|
||||||
|
|
||||||
if (!params.target_plan_comment && ctx.toolState.todoTracker) {
|
if (!params.target_plan_comment && ctx.toolState.todoTracker) {
|
||||||
ctx.toolState.todoTracker.cancel();
|
ctx.toolState.todoTracker.cancel();
|
||||||
await ctx.toolState.todoTracker.settled();
|
await ctx.toolState.todoTracker.settled();
|
||||||
const collapsible = ctx.toolState.todoTracker.renderCollapsible({ completeInProgress: true });
|
const collapsible = ctx.toolState.todoTracker.renderCollapsible({ completeInProgress: true });
|
||||||
if (collapsible) body = `${body}\n\n${collapsible}`;
|
if (collapsible) body = `${body}\n\n${collapsible}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reportParams: { body: string; target_plan_comment?: boolean } = { body };
|
const reportParams: { body: string; target_plan_comment?: boolean } = { body };
|
||||||
if (params.target_plan_comment !== undefined) reportParams.target_plan_comment = params.target_plan_comment;
|
if (params.target_plan_comment !== undefined) reportParams.target_plan_comment = params.target_plan_comment;
|
||||||
|
|
||||||
const result = await reportProgress(ctx, reportParams);
|
const result = await reportProgress(ctx, reportParams);
|
||||||
|
if (result.action === "skipped") return { success: true, message: "progress recorded (no comment created)" };
|
||||||
if (result.action === "skipped") {
|
|
||||||
return { success: true, message: "progress recorded (no comment created)" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.commentId !== undefined) log.info(`» ${result.action} comment ${result.commentId}`);
|
if (result.commentId !== undefined) log.info(`» ${result.action} comment ${result.commentId}`);
|
||||||
if (!params.target_plan_comment) ctx.toolState.finalSummaryWritten = true;
|
if (!params.target_plan_comment) ctx.toolState.finalSummaryWritten = true;
|
||||||
|
|
||||||
return { success: true, ...result };
|
return { success: true, ...result };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -208,16 +150,11 @@ export function ReportProgressTool(ctx: ToolContext) {
|
|||||||
export async function deleteProgressComment(ctx: ToolContext): Promise<boolean> {
|
export async function deleteProgressComment(ctx: ToolContext): Promise<boolean> {
|
||||||
const existing = ctx.toolState.progressComment;
|
const existing = ctx.toolState.progressComment;
|
||||||
if (!existing) return false;
|
if (!existing) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteProgressCommentApi(
|
await deleteProgressCommentApi({ gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name }, existing);
|
||||||
{ gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name },
|
|
||||||
existing
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof Error && error.message.includes("Not Found"))) throw error;
|
if (!(error instanceof Error && error.message.includes("Not Found"))) throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.toolState.progressComment = null;
|
ctx.toolState.progressComment = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -229,10 +166,7 @@ export const ReplyToReviewComment = type({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export interface DuplicateReplyDecision {
|
export interface DuplicateReplyDecision {
|
||||||
kind: "already-replied";
|
kind: "already-replied"; commentId: number; url: string | undefined; reason: string;
|
||||||
commentId: number;
|
|
||||||
url: string | undefined;
|
|
||||||
reason: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function duplicateReplyDecision(params: {
|
export function duplicateReplyDecision(params: {
|
||||||
@@ -246,47 +180,33 @@ export function duplicateReplyDecision(params: {
|
|||||||
kind: "already-replied",
|
kind: "already-replied",
|
||||||
commentId: existing.commentId,
|
commentId: existing.commentId,
|
||||||
url: existing.url,
|
url: existing.url,
|
||||||
reason: `reply ${existing.commentId} with identical body was already posted in this session; ignoring duplicate call`,
|
reason: `reply ${existing.commentId} with identical body was already posted in this session`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReplyToReviewCommentTool(ctx: ToolContext) {
|
export function ReplyToReviewCommentTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "reply_to_review_comment",
|
name: "reply_to_review_comment",
|
||||||
description:
|
description: "Reply to a PR review comment. Posts an issue comment on the PR. Keep replies to 1 sentence max.",
|
||||||
"Reply to a PR review comment. Posts an issue comment on the PR referencing the original. " +
|
|
||||||
"Keep replies extremely brief (1 sentence max).",
|
|
||||||
parameters: ReplyToReviewComment,
|
parameters: ReplyToReviewComment,
|
||||||
execute: execute(async ({ pull_number, comment_id, body }) => {
|
execute: execute(async ({ pull_number, comment_id, body }) => {
|
||||||
const bodyWithFooter = addFooter(ctx, body);
|
const bodyWithFooter = addFooter(ctx, body);
|
||||||
|
const dup = duplicateReplyDecision({ existing: ctx.toolState.reviewReplies?.get(comment_id), bodyWithFooter });
|
||||||
const dup = duplicateReplyDecision({
|
|
||||||
existing: ctx.toolState.reviewReplies?.get(comment_id),
|
|
||||||
bodyWithFooter,
|
|
||||||
});
|
|
||||||
if (dup) {
|
if (dup) {
|
||||||
log.info(`skipping duplicate review reply: ${dup.reason}`);
|
log.info(`skipping duplicate review reply: ${dup.reason}`);
|
||||||
return { success: true, skipped: true, reason: dup.reason, commentId: dup.commentId, url: dup.url };
|
return { success: true, skipped: true, reason: dup.reason, commentId: dup.commentId, url: dup.url };
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyBody = `> Reply to review comment #${comment_id}\n\n${bodyWithFooter}`;
|
const replyBody = `> Reply to review comment #${comment_id}\n\n${bodyWithFooter}`;
|
||||||
const result = await ctx.gitea.rest.issue.issueCreateComment({
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"POST /repos/{owner}/{repo}/issues/{index}/comments",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, body: replyBody }
|
||||||
index: pull_number,
|
);
|
||||||
body: { body: replyBody },
|
const data = r.data as GiteaComment;
|
||||||
});
|
log.info(`» created reply comment ${data.id}`);
|
||||||
log.info(`» created reply comment ${result.data.id}`);
|
|
||||||
|
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
ctx.toolState.reviewReplies ??= new Map();
|
ctx.toolState.reviewReplies ??= new Map();
|
||||||
ctx.toolState.reviewReplies.set(comment_id, {
|
ctx.toolState.reviewReplies.set(comment_id, { commentId: data.id!, url: data.html_url, bodyWithFooter });
|
||||||
commentId: result.data.id!,
|
return { success: true, commentId: data.id, url: data.html_url, body: data.body };
|
||||||
url: result.data.html_url,
|
|
||||||
bodyWithFooter,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, commentId: result.data.id, url: result.data.html_url, body: result.data.body };
|
|
||||||
}, "reply_to_review_comment"),
|
}, "reply_to_review_comment"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-29
@@ -6,52 +6,43 @@ import { formatFilesWithLineNumbers, type DiffFile } from "./checkout.ts";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const CommitInfo = type({
|
interface GiteaCommit {
|
||||||
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch"),
|
sha?: string; html_url?: string; parents?: Array<{ sha?: string }>;
|
||||||
});
|
commit?: { message?: string; author?: { name?: string; date?: string }; committer?: { name?: string; date?: string } };
|
||||||
|
author?: { login?: string }; committer?: { login?: string };
|
||||||
|
stats?: { additions?: number; deletions?: number; total?: number };
|
||||||
|
files?: Array<{ filename?: string; status?: string; patch?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommitInfo = type({ sha: type.string.describe("the commit SHA to fetch") });
|
||||||
|
|
||||||
export function CommitInfoTool(ctx: ToolContext) {
|
export function CommitInfoTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_commit_info",
|
name: "get_commit_info",
|
||||||
description:
|
description: "Retrieve commit metadata and diff via Gitea API. Returns diffPath pointing to formatted diff file.",
|
||||||
"Retrieve commit metadata and diff via Gitea API. Returns diffPath pointing to formatted diff file. " +
|
|
||||||
'Example: `get_commit_info({ sha: "2a6ab5d" })`.',
|
|
||||||
parameters: CommitInfo,
|
parameters: CommitInfo,
|
||||||
execute: execute(async ({ sha }) => {
|
execute: execute(async ({ sha }) => {
|
||||||
const result = await ctx.gitea.rest.repository.repoGetSingleCommit({
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/git/commits/{sha}",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, sha }
|
||||||
sha,
|
);
|
||||||
});
|
const data = r.data as GiteaCommit;
|
||||||
const data = result.data;
|
const files: DiffFile[] = (data.files ?? []).map((f) => ({ filename: f.filename, patch: f.patch }));
|
||||||
|
|
||||||
// The SDK's CommitAffectedFiles type omits `patch`, which Gitea does return
|
|
||||||
// in practice. Cast explicitly so we can pass it to formatFilesWithLineNumbers.
|
|
||||||
const files: DiffFile[] = (data.files ?? []).map((f) => ({
|
|
||||||
filename: f.filename,
|
|
||||||
patch: (f as unknown as { patch?: string }).patch,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const formatResult = formatFilesWithLineNumbers(files);
|
const formatResult = formatFilesWithLineNumbers(files);
|
||||||
const tempDir = process.env.SHOCKBOT_TEMP_DIR;
|
const tempDir = process.env.SHOCKBOT_TEMP_DIR;
|
||||||
if (!tempDir) {
|
if (!tempDir) throw new Error("SHOCKBOT_TEMP_DIR not set");
|
||||||
throw new Error("SHOCKBOT_TEMP_DIR not set - get_commit_info must run in shockbot action context");
|
|
||||||
}
|
|
||||||
const diffFile = join(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
const diffFile = join(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
||||||
writeFileSync(diffFile, formatResult.content);
|
writeFileSync(diffFile, formatResult.content);
|
||||||
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
log.debug(`wrote commit diff to ${diffFile}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sha: data.sha,
|
sha: data.sha, message: data.commit?.message,
|
||||||
message: data.commit?.message,
|
|
||||||
author: data.author?.login ?? data.commit?.author?.name ?? null,
|
author: data.author?.login ?? data.commit?.author?.name ?? null,
|
||||||
committer: data.committer?.login ?? data.commit?.committer?.name ?? null,
|
committer: data.committer?.login ?? data.commit?.committer?.name ?? null,
|
||||||
date: data.commit?.author?.date ?? data.commit?.committer?.date ?? "",
|
date: data.commit?.author?.date ?? data.commit?.committer?.date ?? "",
|
||||||
url: data.html_url,
|
url: data.html_url,
|
||||||
parents: (data.parents ?? []).map((p) => p.sha),
|
parents: (data.parents ?? []).map((p) => p.sha),
|
||||||
stats: data.stats ?? { additions: 0, deletions: 0, total: 0 },
|
stats: data.stats ?? { additions: 0, deletions: 0, total: 0 },
|
||||||
fileCount: files.length,
|
fileCount: files.length, diffFile,
|
||||||
diffFile,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+20
-31
@@ -4,17 +4,16 @@ import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
interface GiteaIssue {
|
||||||
|
number: number; html_url: string; title: string; state: string;
|
||||||
|
labels?: Array<{ name?: string }>; assignees?: Array<{ login: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
export const Issue = type({
|
export const Issue = type({
|
||||||
title: type.string.describe("the title of the issue"),
|
title: type.string.describe("the title of the issue"),
|
||||||
body: type.string.describe("the body content of the issue"),
|
body: type.string.describe("the body content of the issue"),
|
||||||
labels: type.string
|
labels: type.string.array().optional(),
|
||||||
.array()
|
assignees: type.string.array().optional(),
|
||||||
.describe("optional array of label names to apply to the issue")
|
|
||||||
.optional(),
|
|
||||||
assignees: type.string
|
|
||||||
.array()
|
|
||||||
.describe("optional array of usernames to assign to the issue")
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function IssueTool(ctx: ToolContext) {
|
export function IssueTool(ctx: ToolContext) {
|
||||||
@@ -23,30 +22,20 @@ export function IssueTool(ctx: ToolContext) {
|
|||||||
description: "Create a new Gitea issue",
|
description: "Create a new Gitea issue",
|
||||||
parameters: Issue,
|
parameters: Issue,
|
||||||
execute: execute(async (params) => {
|
execute: execute(async (params) => {
|
||||||
const result = await ctx.gitea.rest.issue.issueCreateIssue({
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"POST /repos/{owner}/{repo}/issues",
|
||||||
repo: ctx.repo.name,
|
{
|
||||||
body: {
|
owner: ctx.repo.owner, repo: ctx.repo.name,
|
||||||
title: params.title,
|
title: params.title, body: fixDoubleEscapedString(params.body),
|
||||||
body: fixDoubleEscapedString(params.body),
|
...(params.assignees ? { assignees: params.assignees } : {}),
|
||||||
assignees: params.assignees,
|
}
|
||||||
},
|
);
|
||||||
});
|
const data = r.data as GiteaIssue;
|
||||||
|
log.info(`» created issue #${data.number}`);
|
||||||
log.info(`» created issue #${result.data.number}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true, number: data.number, url: data.html_url, title: data.title, state: data.state,
|
||||||
number: result.data.number,
|
labels: data.labels?.map((l) => l.name).filter((n): n is string => n !== undefined),
|
||||||
url: result.data.html_url,
|
assignees: data.assignees?.map((a) => a.login).filter((n): n is string => n !== undefined),
|
||||||
title: result.data.title,
|
|
||||||
state: result.data.state,
|
|
||||||
labels: result.data.labels
|
|
||||||
?.map((l) => (typeof l === "string" ? l : l.name))
|
|
||||||
.filter((n): n is string => n !== undefined),
|
|
||||||
assignees: result.data.assignees
|
|
||||||
?.map((a) => a.login)
|
|
||||||
.filter((n): n is string => n !== undefined),
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-15
@@ -2,6 +2,8 @@ import { type } from "arktype";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
interface GiteaComment { id: number; body?: string | null; user?: { login?: string } }
|
||||||
|
|
||||||
export const GetIssueComments = type({
|
export const GetIssueComments = type({
|
||||||
issue_number: type.number.describe("The issue number to get comments for"),
|
issue_number: type.number.describe("The issue number to get comments for"),
|
||||||
});
|
});
|
||||||
@@ -9,26 +11,18 @@ export const GetIssueComments = type({
|
|||||||
export function GetIssueCommentsTool(ctx: ToolContext) {
|
export function GetIssueCommentsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_issue_comments",
|
name: "get_issue_comments",
|
||||||
description:
|
description: "Get all comments for a Gitea issue or PR. Example: `get_issue_comments({ issue_number: 1234 })`.",
|
||||||
"Get all comments for a Gitea issue or PR. " +
|
|
||||||
"Example: `get_issue_comments({ issue_number: 1234 })`.",
|
|
||||||
parameters: GetIssueComments,
|
parameters: GetIssueComments,
|
||||||
execute: execute(async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
ctx.toolState.issueNumber = issue_number;
|
ctx.toolState.issueNumber = issue_number;
|
||||||
|
const r = await ctx.gitea.request(
|
||||||
const comments = await ctx.gitea.paginate(ctx.gitea.rest.issue.issueGetComments, {
|
"GET /repos/{owner}/{repo}/issues/{index}/comments",
|
||||||
owner: ctx.repo.owner,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number, limit: 50 }
|
||||||
repo: ctx.repo.name,
|
);
|
||||||
index: issue_number,
|
const comments = r.data as GiteaComment[];
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
issue_number,
|
issue_number,
|
||||||
comments: comments.map((c) => ({
|
comments: comments.map((c) => ({ id: c.id, body: c.body, user: c.user?.login })),
|
||||||
id: c.id,
|
|
||||||
body: c.body,
|
|
||||||
user: c.user?.login,
|
|
||||||
})),
|
|
||||||
count: comments.length,
|
count: comments.length,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
+19
-30
@@ -2,6 +2,13 @@ import { type } from "arktype";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
interface GiteaIssue {
|
||||||
|
number: number; title: string; body?: string | null; state: string; html_url: string;
|
||||||
|
user?: { login: string }; labels?: Array<{ name?: string }>; assignees?: Array<{ login: string }>;
|
||||||
|
comments: number; created_at: string; updated_at: string; closed_at?: string | null;
|
||||||
|
milestone?: { title: string } | null; pull_request?: { html_url?: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const IssueInfo = type({
|
export const IssueInfo = type({
|
||||||
issue_number: type.number.describe("The issue number to fetch"),
|
issue_number: type.number.describe("The issue number to fetch"),
|
||||||
});
|
});
|
||||||
@@ -9,42 +16,24 @@ export const IssueInfo = type({
|
|||||||
export function IssueInfoTool(ctx: ToolContext) {
|
export function IssueInfoTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_issue",
|
name: "get_issue",
|
||||||
description:
|
description: "Retrieve Gitea issue information by issue number. Example: `get_issue({ issue_number: 1234 })`.",
|
||||||
"Retrieve Gitea issue information by issue number. " +
|
|
||||||
"Example: `get_issue({ issue_number: 1234 })`.",
|
|
||||||
parameters: IssueInfo,
|
parameters: IssueInfo,
|
||||||
execute: execute(async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
const result = await ctx.gitea.rest.issue.issueGetIssue({
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/issues/{index}",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number }
|
||||||
index: issue_number,
|
);
|
||||||
});
|
const data = r.data as GiteaIssue;
|
||||||
const data = result.data;
|
|
||||||
ctx.toolState.issueNumber = issue_number;
|
ctx.toolState.issueNumber = issue_number;
|
||||||
|
|
||||||
const hints: string[] = [];
|
const hints: string[] = [];
|
||||||
if ((data.comments ?? 0) > 0) {
|
if (data.comments > 0) hints.push("use get_issue_comments to retrieve all comments for this issue");
|
||||||
hints.push("use get_issue_comments to retrieve all comments for this issue");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number: data.number,
|
number: data.number, url: data.html_url, title: data.title, body: data.body,
|
||||||
url: data.html_url,
|
|
||||||
title: data.title,
|
|
||||||
body: data.body,
|
|
||||||
state: data.state,
|
state: data.state,
|
||||||
labels: data.labels
|
labels: data.labels?.map((l) => l.name).filter((n): n is string => n !== undefined),
|
||||||
?.map((l) => (typeof l === "string" ? l : l.name))
|
assignees: data.assignees?.map((a) => a.login).filter((n): n is string => n !== undefined),
|
||||||
.filter((n): n is string => n !== undefined),
|
user: data.user?.login, created_at: data.created_at, updated_at: data.updated_at,
|
||||||
assignees: data.assignees
|
closed_at: data.closed_at, comments: data.comments, milestone: data.milestone?.title,
|
||||||
?.map((a) => a.login)
|
|
||||||
.filter((n): n is string => n !== undefined),
|
|
||||||
user: data.user?.login,
|
|
||||||
created_at: data.created_at,
|
|
||||||
updated_at: data.updated_at,
|
|
||||||
closed_at: data.closed_at,
|
|
||||||
comments: data.comments,
|
|
||||||
milestone: data.milestone?.title,
|
|
||||||
pull_request: data.pull_request ? { html_url: data.pull_request.html_url } : null,
|
pull_request: data.pull_request ? { html_url: data.pull_request.html_url } : null,
|
||||||
hints,
|
hints,
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-18
@@ -3,6 +3,8 @@ import { log } from "../utils/cli.ts";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
interface GiteaLabel { id: number; name?: string }
|
||||||
|
|
||||||
export const AddLabelsParams = type({
|
export const AddLabelsParams = type({
|
||||||
issue_number: type.number.describe("the issue or PR number to add labels to"),
|
issue_number: type.number.describe("the issue or PR number to add labels to"),
|
||||||
labels: type.string.array().atLeastLength(1).describe("array of label names to add"),
|
labels: type.string.array().atLeastLength(1).describe("array of label names to add"),
|
||||||
@@ -11,15 +13,14 @@ export const AddLabelsParams = type({
|
|||||||
export function AddLabelsTool(ctx: ToolContext) {
|
export function AddLabelsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "add_labels",
|
name: "add_labels",
|
||||||
description:
|
description: "Add labels to a Gitea issue or pull request. Only use labels that already exist in the repository.",
|
||||||
"Add labels to a Gitea issue or pull request. Only use labels that already exist in the repository.",
|
|
||||||
parameters: AddLabelsParams,
|
parameters: AddLabelsParams,
|
||||||
execute: execute(async ({ issue_number, labels }) => {
|
execute: execute(async ({ issue_number, labels }) => {
|
||||||
// Resolve label names to IDs (Gitea uses IDs, not names)
|
const allLabelsR = await ctx.gitea.request(
|
||||||
const allLabels = await ctx.gitea.paginate(ctx.gitea.rest.issue.issueListLabels, {
|
"GET /repos/{owner}/{repo}/labels",
|
||||||
owner: ctx.repo.owner,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, limit: 50 }
|
||||||
repo: ctx.repo.name,
|
);
|
||||||
});
|
const allLabels = allLabelsR.data as GiteaLabel[];
|
||||||
const labelIds = labels
|
const labelIds = labels
|
||||||
.map((name) => allLabels.find((l) => l.name === name)?.id)
|
.map((name) => allLabels.find((l) => l.name === name)?.id)
|
||||||
.filter((id): id is number => typeof id === "number");
|
.filter((id): id is number => typeof id === "number");
|
||||||
@@ -28,18 +29,13 @@ export function AddLabelsTool(ctx: ToolContext) {
|
|||||||
return { success: true, labels: [], message: "No matching labels found in repository" };
|
return { success: true, labels: [], message: "No matching labels found in repository" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ctx.gitea.rest.issue.issueAddLabel({
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"POST /repos/{owner}/{repo}/issues/{index}/labels",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number, labels: labelIds }
|
||||||
index: issue_number,
|
);
|
||||||
body: { labels: labelIds },
|
|
||||||
});
|
|
||||||
log.info(`» added labels [${labels.join(", ")}] to issue #${issue_number}`);
|
log.info(`» added labels [${labels.join(", ")}] to issue #${issue_number}`);
|
||||||
|
const result = r.data as GiteaLabel[];
|
||||||
return {
|
return { success: true, labels: result.map((l) => l.name).filter(Boolean) };
|
||||||
success: true,
|
|
||||||
labels: result.data.map((label) => label.name).filter(Boolean),
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,10 @@ import { $ } from "../utils/shell.ts";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const PullRequest = type({
|
interface GiteaPull { number: number; html_url?: string; title?: string; head?: { ref?: string }; base?: { ref?: string } }
|
||||||
title: type.string.describe("the title of the pull request"),
|
|
||||||
body: type.string.describe("the body content of the pull request"),
|
|
||||||
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
|
|
||||||
"draft?": type.boolean.describe("if true, create the pull request as a draft."),
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
|
function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
|
||||||
const footer = buildShockbotFooter({ model: ctx.toolState.model });
|
return `${stripExistingFooter(fixDoubleEscapedString(body))}${buildShockbotFooter({ model: ctx.toolState.model })}`;
|
||||||
return `${stripExistingFooter(fixDoubleEscapedString(body))}${footer}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdatePullRequestBody = type({
|
export const UpdatePullRequestBody = type({
|
||||||
@@ -29,19 +23,25 @@ export function UpdatePullRequestBodyTool(ctx: ToolContext) {
|
|||||||
description: "Update the body/description of an existing pull request",
|
description: "Update the body/description of an existing pull request",
|
||||||
parameters: UpdatePullRequestBody,
|
parameters: UpdatePullRequestBody,
|
||||||
execute: execute(async (params) => {
|
execute: execute(async (params) => {
|
||||||
const result = await ctx.gitea.rest.repository.repoEditPullRequest({
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"PATCH /repos/{owner}/{repo}/pulls/{index}",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: params.pull_number, body: buildPrBodyWithFooter(ctx, params.body) }
|
||||||
index: params.pull_number,
|
);
|
||||||
body: { body: buildPrBodyWithFooter(ctx, params.body) },
|
const data = r.data as GiteaPull;
|
||||||
});
|
log.info(`» updated pull request #${data.number}`);
|
||||||
log.info(`» updated pull request #${result.data.number}`);
|
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
return { success: true, number: result.data.number, url: result.data.html_url };
|
return { success: true, number: data.number, url: data.html_url };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PullRequest = type({
|
||||||
|
title: type.string.describe("the title of the pull request"),
|
||||||
|
body: type.string.describe("the body content of the pull request"),
|
||||||
|
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
|
||||||
|
"draft?": type.boolean,
|
||||||
|
});
|
||||||
|
|
||||||
export function CreatePullRequestTool(ctx: ToolContext) {
|
export function CreatePullRequestTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_pull_request",
|
name: "create_pull_request",
|
||||||
@@ -49,42 +49,27 @@ export function CreatePullRequestTool(ctx: ToolContext) {
|
|||||||
parameters: PullRequest,
|
parameters: PullRequest,
|
||||||
execute: execute(async (params) => {
|
execute: execute(async (params) => {
|
||||||
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
||||||
|
const r = await ctx.gitea.request(
|
||||||
const result = await ctx.gitea.rest.repository.repoCreatePullRequest({
|
"POST /repos/{owner}/{repo}/pulls",
|
||||||
owner: ctx.repo.owner,
|
{
|
||||||
repo: ctx.repo.name,
|
owner: ctx.repo.owner, repo: ctx.repo.name,
|
||||||
body: {
|
title: params.title, body: buildPrBodyWithFooter(ctx, params.body),
|
||||||
title: params.title,
|
head: currentBranch, base: params.base,
|
||||||
body: buildPrBodyWithFooter(ctx, params.body),
|
}
|
||||||
head: currentBranch,
|
);
|
||||||
base: params.base,
|
const data = r.data as GiteaPull;
|
||||||
// draft: params.draft ?? false, // not in all Gitea versions
|
log.info(`» created pull request #${data.number}`);
|
||||||
},
|
|
||||||
});
|
|
||||||
log.info(`» created pull request #${result.data.number}`);
|
|
||||||
|
|
||||||
const reviewer = ctx.payload.triggerer;
|
const reviewer = ctx.payload.triggerer;
|
||||||
if (reviewer && result.data.number) {
|
if (reviewer && data.number) {
|
||||||
try {
|
try {
|
||||||
await ctx.gitea.rest.repository.repoCreatePullReviewRequests({
|
await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: data.number, reviewers: [reviewer] }
|
||||||
index: result.data.number,
|
);
|
||||||
body: { reviewers: [reviewer] },
|
} catch { log.debug(`failed to request review from ${reviewer}`); }
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
log.debug(`failed to request review from ${reviewer}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return { success: true, number: data.number, url: data.html_url, title: data.title, head: data.head?.ref, base: data.base?.ref };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
number: result.data.number,
|
|
||||||
url: result.data.html_url,
|
|
||||||
title: result.data.title,
|
|
||||||
head: result.data.head?.label ?? result.data.head?.ref,
|
|
||||||
base: result.data.base?.label ?? result.data.base?.ref,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-22
@@ -2,6 +2,15 @@ import { type } from "arktype";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
interface GiteaPull {
|
||||||
|
number: number; html_url: string; title: string; body?: string | null;
|
||||||
|
state: string; draft?: boolean; merged?: boolean; allow_maintainer_edit?: boolean;
|
||||||
|
head?: { sha: string; ref: string; repo?: { full_name: string } | null };
|
||||||
|
base?: { ref: string; repo?: { full_name: string } };
|
||||||
|
user?: { login: string }; assignees?: Array<{ login: string }>;
|
||||||
|
labels?: Array<string | { name?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
export const PullRequestInfo = type({
|
export const PullRequestInfo = type({
|
||||||
pull_number: type.number.describe("The pull request number to fetch"),
|
pull_number: type.number.describe("The pull request number to fetch"),
|
||||||
});
|
});
|
||||||
@@ -11,34 +20,22 @@ export function PullRequestInfoTool(ctx: ToolContext) {
|
|||||||
name: "get_pull_request",
|
name: "get_pull_request",
|
||||||
description:
|
description:
|
||||||
"Retrieve PR metadata (title, body, state, branches, author, labels). " +
|
"Retrieve PR metadata (title, body, state, branches, author, labels). " +
|
||||||
"Example: `get_pull_request({ pull_number: 1234 })`. " +
|
"Example: `get_pull_request({ pull_number: 1234 })`. To checkout a PR branch locally, use checkout_pr instead.",
|
||||||
"To checkout a PR branch locally, use checkout_pr instead.",
|
|
||||||
parameters: PullRequestInfo,
|
parameters: PullRequestInfo,
|
||||||
execute: execute(async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const result = await ctx.gitea.rest.repository.repoGetPullRequest({
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/pulls/{index}",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number }
|
||||||
index: pull_number,
|
);
|
||||||
});
|
const data = r.data as GiteaPull;
|
||||||
const data = result.data;
|
|
||||||
const isFork = data.head?.repo?.full_name !== data.base?.repo?.full_name;
|
const isFork = data.head?.repo?.full_name !== data.base?.repo?.full_name;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number: data.number,
|
number: data.number, url: data.html_url, title: data.title, body: data.body,
|
||||||
url: data.html_url,
|
state: data.state, draft: data.draft, merged: data.merged,
|
||||||
title: data.title,
|
|
||||||
body: data.body,
|
|
||||||
state: data.state,
|
|
||||||
draft: data.draft,
|
|
||||||
merged: data.merged,
|
|
||||||
maintainerCanModify: data.allow_maintainer_edit,
|
maintainerCanModify: data.allow_maintainer_edit,
|
||||||
base: data.base?.label,
|
base: data.base?.ref, head: data.head?.ref, isFork,
|
||||||
head: data.head?.label,
|
|
||||||
isFork,
|
|
||||||
author: data.user?.login,
|
author: data.user?.login,
|
||||||
assignees: data.assignees
|
assignees: data.assignees?.map((a) => a.login).filter((n): n is string => n !== undefined),
|
||||||
?.map((a) => a.login)
|
|
||||||
.filter((n): n is string => n !== undefined),
|
|
||||||
labels: data.labels
|
labels: data.labels
|
||||||
?.map((l) => (typeof l === "string" ? l : l.name))
|
?.map((l) => (typeof l === "string" ? l : l.name))
|
||||||
.filter((n): n is string => n !== undefined),
|
.filter((n): n is string => n !== undefined),
|
||||||
|
|||||||
+22
-24
@@ -62,11 +62,11 @@ export async function buildCommentableMap(
|
|||||||
const currentSha = ctx.toolState.checkoutSha;
|
const currentSha = ctx.toolState.checkoutSha;
|
||||||
if (cached && cachedFor === pullNumber && cachedSha && cachedSha === currentSha) return cached;
|
if (cached && cachedFor === pullNumber && cachedSha && cachedSha === currentSha) return cached;
|
||||||
|
|
||||||
const files = (await ctx.gitea.paginate(ctx.gitea.rest.repository.repoGetPullRequestFiles, {
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/pulls/{index}/files",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pullNumber, limit: 50 }
|
||||||
index: pullNumber,
|
);
|
||||||
})) as ChangedFileWithPatch[];
|
const files = r.data as ChangedFileWithPatch[];
|
||||||
|
|
||||||
const map = new Map<string, CommentableLines>();
|
const map = new Map<string, CommentableLines>();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -244,12 +244,11 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
let effectiveCommitId = commit_id;
|
let effectiveCommitId = commit_id;
|
||||||
if (!effectiveCommitId) {
|
if (!effectiveCommitId) {
|
||||||
try {
|
try {
|
||||||
const pr = await ctx.gitea.rest.repository.repoGetPullRequest({
|
const pr = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/pulls/{index}",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number }
|
||||||
index: pull_number,
|
);
|
||||||
});
|
latestHeadSha = (pr.data as { head?: { sha?: string } }).head?.sha;
|
||||||
latestHeadSha = pr.data.head?.sha;
|
|
||||||
effectiveCommitId = ctx.toolState.checkoutSha ?? latestHeadSha;
|
effectiveCommitId = ctx.toolState.checkoutSha ?? latestHeadSha;
|
||||||
} catch {
|
} catch {
|
||||||
effectiveCommitId = ctx.toolState.checkoutSha;
|
effectiveCommitId = ctx.toolState.checkoutSha;
|
||||||
@@ -301,18 +300,16 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
const footer = buildShockbotFooter({ model: ctx.toolState.model });
|
const footer = buildShockbotFooter({ model: ctx.toolState.model });
|
||||||
const fullBody = body ? `${body}${footer}` : footer.trimStart();
|
const fullBody = body ? `${body}${footer}` : footer.trimStart();
|
||||||
|
|
||||||
const result = await retry<Awaited<ReturnType<typeof ctx.gitea.rest.repository.repoCreatePullReview>>>(
|
const result = await retry(
|
||||||
() =>
|
() =>
|
||||||
ctx.gitea.rest.repository.repoCreatePullReview({
|
ctx.gitea.request("POST /repos/{owner}/{repo}/pulls/{index}/reviews", {
|
||||||
owner: ctx.repo.owner,
|
owner: ctx.repo.owner,
|
||||||
repo: ctx.repo.name,
|
repo: ctx.repo.name,
|
||||||
index: pull_number,
|
index: pull_number,
|
||||||
body: {
|
body: fullBody,
|
||||||
body: fullBody,
|
commit_id: effectiveCommitId,
|
||||||
commit_id: effectiveCommitId,
|
event,
|
||||||
event,
|
comments: sdkComments,
|
||||||
comments: sdkComments,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
delaysMs: [1_000, 3_000],
|
delaysMs: [1_000, 3_000],
|
||||||
@@ -321,7 +318,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const reviewId = result.data.id!;
|
const reviewData = result.data as { id?: number; html_url?: string; state?: string };
|
||||||
|
const reviewId = reviewData.id!;
|
||||||
log.info(`» created review ${reviewId} on pull request #${pull_number}`);
|
log.info(`» created review ${reviewId} on pull request #${pull_number}`);
|
||||||
|
|
||||||
ctx.toolState.review = {
|
ctx.toolState.review = {
|
||||||
@@ -344,8 +342,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
reviewId,
|
reviewId,
|
||||||
html_url: result.data.html_url,
|
html_url: reviewData.html_url,
|
||||||
state: result.data.state,
|
state: reviewData.state,
|
||||||
droppedComments: droppedComments.length > 0 ? droppedComments : undefined,
|
droppedComments: droppedComments.length > 0 ? droppedComments : undefined,
|
||||||
newCommits: {
|
newCommits: {
|
||||||
from: fromSha,
|
from: fromSha,
|
||||||
@@ -358,8 +356,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
reviewId,
|
reviewId,
|
||||||
html_url: result.data.html_url,
|
html_url: reviewData.html_url,
|
||||||
state: result.data.state,
|
state: reviewData.state,
|
||||||
droppedComments: droppedComments.length > 0 ? droppedComments : undefined,
|
droppedComments: droppedComments.length > 0 ? droppedComments : undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
+27
-57
@@ -1,57 +1,45 @@
|
|||||||
import { writeFileSync } from "node:fs";
|
import { writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { PullReviewComment } from "@go-gitea/sdk.js";
|
|
||||||
import { stripExistingFooter } from "../utils/buildShockbotFooter.ts";
|
import { stripExistingFooter } from "../utils/buildShockbotFooter.ts";
|
||||||
import { log } from "../utils/log.ts";
|
import { log } from "../utils/log.ts";
|
||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const GetReviewComments = type({
|
interface GiteaReview { id: number; user?: { login: string }; state?: string; body?: string; submitted_at?: string; commit_id?: string }
|
||||||
pull_number: type.number.describe("The pull request number to get review comments for"),
|
interface GiteaReviewComment { id: number; body?: string; path?: string; diff_hunk?: string; original_position?: number; position?: number; pull_request_review_id?: number; user?: { login: string } }
|
||||||
});
|
|
||||||
|
export const GetReviewComments = type({ pull_number: type.number });
|
||||||
|
|
||||||
export function GetReviewCommentsTool(ctx: ToolContext) {
|
export function GetReviewCommentsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_review_comments",
|
name: "get_review_comments",
|
||||||
description:
|
description: "Get all inline review comments for a pull request. Example: `get_review_comments({ pull_number: 1234 })`.",
|
||||||
"Get all inline review comments for a pull request. " +
|
|
||||||
"Example: `get_review_comments({ pull_number: 1234 })`.",
|
|
||||||
parameters: GetReviewComments,
|
parameters: GetReviewComments,
|
||||||
execute: execute(async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
ctx.toolState.issueNumber = pull_number;
|
ctx.toolState.issueNumber = pull_number;
|
||||||
|
const reviewsR = await ctx.gitea.request(
|
||||||
|
"GET /repos/{owner}/{repo}/pulls/{index}/reviews",
|
||||||
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, limit: 50 }
|
||||||
|
);
|
||||||
|
const reviews = reviewsR.data as GiteaReview[];
|
||||||
|
|
||||||
// Get all reviews, then collect their comments
|
const allComments: GiteaReviewComment[] = [];
|
||||||
const reviews = await ctx.gitea.paginate(ctx.gitea.rest.repository.repoListPullReviews, {
|
|
||||||
owner: ctx.repo.owner,
|
|
||||||
repo: ctx.repo.name,
|
|
||||||
index: pull_number,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allComments: PullReviewComment[] = [];
|
|
||||||
for (const review of reviews) {
|
for (const review of reviews) {
|
||||||
if (!review.id) continue;
|
if (!review.id) continue;
|
||||||
try {
|
try {
|
||||||
const commentsResult = await ctx.gitea.rest.repository.repoGetPullReviewComments({
|
const cr = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, id: review.id }
|
||||||
index: pull_number,
|
);
|
||||||
id: review.id,
|
allComments.push(...(cr.data as GiteaReviewComment[]));
|
||||||
});
|
} catch { /* best-effort */ }
|
||||||
allComments.push(...commentsResult.data);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempDir = process.env.SHOCKBOT_TEMP_DIR;
|
const tempDir = process.env.SHOCKBOT_TEMP_DIR;
|
||||||
let filePath: string | undefined;
|
let filePath: string | undefined;
|
||||||
|
|
||||||
const rendered = allComments.map((c) => ({
|
const rendered = allComments.map((c) => ({
|
||||||
id: c.id,
|
id: c.id, path: c.path,
|
||||||
path: c.path,
|
|
||||||
// PullReviewComment uses `position` for the diff offset; Gitea doesn't
|
|
||||||
// expose separate new_position/old_position on GET — use original_position as fallback
|
|
||||||
line: c.original_position ?? c.position,
|
line: c.original_position ?? c.position,
|
||||||
side: "RIGHT" as const,
|
side: "RIGHT" as const,
|
||||||
body: stripExistingFooter(c.body ?? ""),
|
body: stripExistingFooter(c.body ?? ""),
|
||||||
@@ -65,45 +53,27 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
|||||||
writeFileSync(filePath, JSON.stringify(rendered, null, 2));
|
writeFileSync(filePath, JSON.stringify(rendered, null, 2));
|
||||||
log.debug(`wrote review comments to ${filePath}`);
|
log.debug(`wrote review comments to ${filePath}`);
|
||||||
}
|
}
|
||||||
|
return { pull_number, comments: rendered, count: rendered.length, ...(filePath ? { filePath } : {}) };
|
||||||
return {
|
|
||||||
pull_number,
|
|
||||||
comments: rendered,
|
|
||||||
count: rendered.length,
|
|
||||||
...(filePath ? { filePath } : {}),
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListPullRequestReviews = type({
|
export const ListPullRequestReviews = type({ pull_number: type.number });
|
||||||
pull_number: type.number.describe("The pull request number"),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ListPullRequestReviewsTool(ctx: ToolContext) {
|
export function ListPullRequestReviewsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "list_pull_request_reviews",
|
name: "list_pull_request_reviews",
|
||||||
description:
|
description: "List all reviews submitted on a pull request. Example: `list_pull_request_reviews({ pull_number: 1234 })`.",
|
||||||
"List all reviews submitted on a pull request. " +
|
|
||||||
"Example: `list_pull_request_reviews({ pull_number: 1234 })`.",
|
|
||||||
parameters: ListPullRequestReviews,
|
parameters: ListPullRequestReviews,
|
||||||
execute: execute(async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const reviews = await ctx.gitea.paginate(ctx.gitea.rest.repository.repoListPullReviews, {
|
const r = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/pulls/{index}/reviews",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, limit: 50 }
|
||||||
index: pull_number,
|
);
|
||||||
});
|
const reviews = r.data as GiteaReview[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pull_number,
|
pull_number,
|
||||||
reviews: reviews.map((r) => ({
|
reviews: reviews.map((r) => ({ id: r.id, user: r.user?.login, state: r.state, body: r.body, submitted_at: r.submitted_at, commit_id: r.commit_id })),
|
||||||
id: r.id,
|
|
||||||
user: r.user?.login,
|
|
||||||
state: r.state,
|
|
||||||
body: r.body,
|
|
||||||
submitted_at: r.submitted_at,
|
|
||||||
commit_id: r.commit_id,
|
|
||||||
})),
|
|
||||||
count: reviews.length,
|
count: reviews.length,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
+5
-6
@@ -67,12 +67,11 @@ export function SelectModeTool(ctx: ToolContext) {
|
|||||||
// For Plan mode with issue_number, look up existing plan comment
|
// For Plan mode with issue_number, look up existing plan comment
|
||||||
if (foundMode.name === "Plan" && issue_number !== undefined) {
|
if (foundMode.name === "Plan" && issue_number !== undefined) {
|
||||||
try {
|
try {
|
||||||
const commentsResp = await ctx.gitea.paginate(ctx.gitea.rest.issue.issueGetComments, {
|
const commentsR = await ctx.gitea.request(
|
||||||
owner: ctx.repo.owner,
|
"GET /repos/{owner}/{repo}/issues/{index}/comments",
|
||||||
repo: ctx.repo.name,
|
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number, limit: 50 }
|
||||||
index: issue_number,
|
);
|
||||||
});
|
const comments = commentsR.data as Array<{ id?: number; body?: string | null }>;
|
||||||
const comments = commentsResp;
|
|
||||||
// Look for a plan comment (one with our footer)
|
// Look for a plan comment (one with our footer)
|
||||||
const planComment = comments.find((c) => c.body?.includes("<!-- shockbot-footer -->"));
|
const planComment = comments.find((c) => c.body?.includes("<!-- shockbot-footer -->"));
|
||||||
if (planComment) {
|
if (planComment) {
|
||||||
|
|||||||
+26
-37
@@ -1,6 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Single source of truth for reading, updating, deleting, and creating "progress comments" —
|
* Single source of truth for reading, updating, deleting, and creating "progress comments".
|
||||||
* the Gitea comments shockbot uses to surface a run's status.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Gitea } from "./gitea.ts";
|
import type { Gitea } from "./gitea.ts";
|
||||||
@@ -27,16 +26,22 @@ interface ApiCtx {
|
|||||||
repo: string;
|
repo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GiteaComment {
|
||||||
|
id: number;
|
||||||
|
body?: string | null;
|
||||||
|
html_url?: string;
|
||||||
|
node_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getProgressComment(
|
export async function getProgressComment(
|
||||||
ctx: ApiCtx,
|
ctx: ApiCtx,
|
||||||
comment: ProgressComment
|
comment: ProgressComment
|
||||||
): Promise<{ id: number; body: string | undefined; html_url: string }> {
|
): Promise<{ id: number; body: string | undefined; html_url: string }> {
|
||||||
const result = await ctx.gitea.rest.issue.issueGetComment({
|
const r = await ctx.gitea.request("GET /repos/{owner}/{repo}/issues/comments/{id}", {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner, repo: ctx.repo, id: comment.id,
|
||||||
repo: ctx.repo,
|
|
||||||
id: comment.id,
|
|
||||||
});
|
});
|
||||||
return { id: result.data.id!, body: result.data.body ?? undefined, html_url: result.data.html_url! };
|
const data = r.data as GiteaComment;
|
||||||
|
return { id: data.id, body: data.body ?? undefined, html_url: data.html_url ?? "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProgressComment(
|
export async function updateProgressComment(
|
||||||
@@ -44,28 +49,16 @@ export async function updateProgressComment(
|
|||||||
comment: ProgressComment,
|
comment: ProgressComment,
|
||||||
body: string
|
body: string
|
||||||
): Promise<{ id: number; body: string | undefined; html_url: string; node_id: string | undefined }> {
|
): Promise<{ id: number; body: string | undefined; html_url: string; node_id: string | undefined }> {
|
||||||
const result = await ctx.gitea.rest.issue.issueEditComment({
|
const r = await ctx.gitea.request("PATCH /repos/{owner}/{repo}/issues/comments/{id}", {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner, repo: ctx.repo, id: comment.id, body,
|
||||||
repo: ctx.repo,
|
|
||||||
id: comment.id,
|
|
||||||
body: { body },
|
|
||||||
});
|
});
|
||||||
return {
|
const data = r.data as GiteaComment;
|
||||||
id: result.data.id!,
|
return { id: data.id, body: data.body ?? undefined, html_url: data.html_url ?? "", node_id: undefined };
|
||||||
body: result.data.body ?? undefined,
|
|
||||||
html_url: result.data.html_url!,
|
|
||||||
node_id: undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProgressCommentApi(
|
export async function deleteProgressCommentApi(ctx: ApiCtx, comment: ProgressComment): Promise<void> {
|
||||||
ctx: ApiCtx,
|
await ctx.gitea.request("DELETE /repos/{owner}/{repo}/issues/comments/{id}", {
|
||||||
comment: ProgressComment
|
owner: ctx.owner, repo: ctx.repo, id: comment.id,
|
||||||
): Promise<void> {
|
|
||||||
await ctx.gitea.rest.issue.issueDeleteComment({
|
|
||||||
owner: ctx.owner,
|
|
||||||
repo: ctx.repo,
|
|
||||||
id: comment.id,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,18 +77,14 @@ export async function createLeapingProgressComment(
|
|||||||
target: CreateProgressCommentTarget,
|
target: CreateProgressCommentTarget,
|
||||||
body: string
|
body: string
|
||||||
): Promise<CreatedProgressComment> {
|
): Promise<CreatedProgressComment> {
|
||||||
const issueNumber =
|
const issueNumber = target.kind === "issue" ? target.issueNumber : target.pullNumber;
|
||||||
target.kind === "issue" ? target.issueNumber : target.pullNumber;
|
const r = await ctx.gitea.request("POST /repos/{owner}/{repo}/issues/{index}/comments", {
|
||||||
|
owner: ctx.owner, repo: ctx.repo, index: issueNumber, body,
|
||||||
const result = await ctx.gitea.rest.issue.issueCreateComment({
|
|
||||||
owner: ctx.owner,
|
|
||||||
repo: ctx.repo,
|
|
||||||
index: issueNumber,
|
|
||||||
body: { body },
|
|
||||||
});
|
});
|
||||||
|
const data = r.data as GiteaComment;
|
||||||
return {
|
return {
|
||||||
comment: { id: result.data.id!, type: "issue" },
|
comment: { id: data.id, type: "issue" },
|
||||||
body: result.data.body ?? undefined,
|
body: data.body ?? undefined,
|
||||||
html_url: result.data.html_url!,
|
html_url: data.html_url ?? "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user