From c1d1acfa3a29037822d882e356a2c64d54ad3806 Mon Sep 17 00:00:00 2001 From: xmtp-coder-agent <> Date: Fri, 3 Apr 2026 04:39:27 +0000 Subject: [PATCH] feat: add codex template support for task creation When an issue title contains "codex" (case insensitive) or has a "codex" label, use the configurable coderTemplateNameCodex template instead of the default task-template. The new config defaults to "task-template-codex". Resolves https://github.com/xmtplabs/coder-action/issues/85 Co-Authored-By: Claude Opus 4.6 --- src/config.ts | 2 + src/handler-dispatcher.test.ts | 3 + src/handler-dispatcher.ts | 3 + src/handlers/close-task.test.ts | 1 + src/handlers/create-task.test.ts | 91 ++++++++++++++++++++++++++++++ src/handlers/create-task.ts | 19 ++++++- src/handlers/failed-check.test.ts | 1 + src/handlers/issue-comment.test.ts | 1 + src/handlers/pr-comment.test.ts | 1 + src/schemas.ts | 1 + src/webhook-router.ts | 6 ++ 11 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 8ba234c..a3d0f0f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,6 +11,7 @@ const AppConfigSchema = z.object({ coderToken: z.string().min(1), coderTaskNamePrefix: z.string().min(1).default("gh"), coderTemplateName: z.string().min(1).default("task-template"), + coderTemplateNameCodex: z.string().min(1).default("task-template-codex"), coderTemplatePreset: z.string().min(1).optional(), coderOrganization: z.string().min(1).default("default"), logFormat: z.string().optional(), @@ -35,6 +36,7 @@ export function loadConfig(env: Record): AppConfig { coderToken: env.CODER_TOKEN, coderTaskNamePrefix: env.CODER_TASK_NAME_PREFIX, coderTemplateName: env.CODER_TEMPLATE_NAME, + coderTemplateNameCodex: env.CODER_TEMPLATE_NAME_CODEX, coderTemplatePreset: env.CODER_TEMPLATE_PRESET, coderOrganization: env.CODER_ORGANIZATION, logFormat: env.LOG_FORMAT, diff --git a/src/handler-dispatcher.test.ts b/src/handler-dispatcher.test.ts index 72ae1ea..95833d2 100644 --- a/src/handler-dispatcher.test.ts +++ b/src/handler-dispatcher.test.ts @@ -30,6 +30,7 @@ const mockConfig: AppConfig = { coderToken: "coder-token", coderTaskNamePrefix: "gh", coderTemplateName: "task-template", + coderTemplateNameCodex: "task-template-codex", coderOrganization: "default", port: 3000, }; @@ -92,6 +93,8 @@ describe("HandlerDispatcher", () => { const createTaskContext = { issueNumber: 42, issueUrl: "https://github.com/xmtp/test-repo/issues/42", + issueTitle: "Fix some bug", + issueLabels: [] as string[], repoName: "test-repo", repoOwner: "xmtp", senderLogin: "human-dev", diff --git a/src/handler-dispatcher.ts b/src/handler-dispatcher.ts index 6ffd23b..3dfe669 100644 --- a/src/handler-dispatcher.ts +++ b/src/handler-dispatcher.ts @@ -51,6 +51,7 @@ export class HandlerDispatcher { coderToken: this.options.config.coderToken, coderTaskNamePrefix: this.options.config.coderTaskNamePrefix, coderTemplateName: this.options.config.coderTemplateName, + coderTemplateNameCodex: this.options.config.coderTemplateNameCodex, coderTemplatePreset: this.options.config.coderTemplatePreset, coderOrganization: this.options.config.coderOrganization, agentGithubUsername: this.options.config.agentGithubUsername, @@ -73,6 +74,8 @@ export class HandlerDispatcher { repo: ctx.repoName, issueNumber: ctx.issueNumber, issueUrl: ctx.issueUrl, + issueTitle: ctx.issueTitle, + issueLabels: ctx.issueLabels, senderLogin: ctx.senderLogin, }, logger, diff --git a/src/handlers/close-task.test.ts b/src/handlers/close-task.test.ts index 61ef710..7f9a367 100644 --- a/src/handlers/close-task.test.ts +++ b/src/handlers/close-task.test.ts @@ -16,6 +16,7 @@ const baseInputs: HandlerConfig = { coderUsername: "coder-agent", coderTaskNamePrefix: "gh", coderTemplateName: "task-template", + coderTemplateNameCodex: "task-template-codex", coderOrganization: "default", agentGithubUsername: "xmtp-coder-agent", }; diff --git a/src/handlers/create-task.test.ts b/src/handlers/create-task.test.ts index 8b05de6..f1f558a 100644 --- a/src/handlers/create-task.test.ts +++ b/src/handlers/create-task.test.ts @@ -15,6 +15,7 @@ const baseInputs: HandlerConfig = { coderUsername: "coder-agent", coderTaskNamePrefix: "gh", coderTemplateName: "task-template", + coderTemplateNameCodex: "task-template-codex", coderOrganization: "default", agentGithubUsername: "xmtp-coder-agent", }; @@ -24,6 +25,8 @@ const issueContext = { repo: "libxmtp", issueNumber: 42, issueUrl: "https://github.com/xmtp/libxmtp/issues/42", + issueTitle: "Fix some bug", + issueLabels: [], senderLogin: "human-dev", }; @@ -185,6 +188,94 @@ describe("CreateTaskHandler", () => { expect(String(taskNameArg)).toBe("gh-libxmtp-42"); }); + test("uses codex template when issue title contains codex", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + const ctx = { + ...issueContext, + issueTitle: "Add Codex Support", + }; + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + ctx, + logger, + ); + await handler.run(); + + const templateCall = coder.getTemplateByOrganizationAndName.mock + .calls[0] as unknown as [string, string]; + expect(templateCall[1]).toBe("task-template-codex"); + }); + + test("uses codex template when issue has codex label", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + const ctx = { + ...issueContext, + issueLabels: ["enhancement", "codex"], + }; + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + ctx, + logger, + ); + await handler.run(); + + const templateCall = coder.getTemplateByOrganizationAndName.mock + .calls[0] as unknown as [string, string]; + expect(templateCall[1]).toBe("task-template-codex"); + }); + + test("uses default template when no codex indicator", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + issueContext, + logger, + ); + await handler.run(); + + const templateCall = coder.getTemplateByOrganizationAndName.mock + .calls[0] as unknown as [string, string]; + expect(templateCall[1]).toBe("task-template"); + }); + + test("codex match in title is case insensitive", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + const ctx = { + ...issueContext, + issueTitle: "Use CODEX for processing", + }; + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + ctx, + logger, + ); + await handler.run(); + + const templateCall = coder.getTemplateByOrganizationAndName.mock + .calls[0] as unknown as [string, string]; + expect(templateCall[1]).toBe("task-template-codex"); + }); + test("logs task name via injected logger", async () => { github.checkActorPermission.mockResolvedValue(true); coder.getTask.mockResolvedValue(null); diff --git a/src/handlers/create-task.ts b/src/handlers/create-task.ts index a622fa3..dac738f 100644 --- a/src/handlers/create-task.ts +++ b/src/handlers/create-task.ts @@ -10,6 +10,8 @@ export interface IssueContext { repo: string; issueNumber: number; issueUrl: string; + issueTitle: string; + issueLabels: string[]; senderLogin: string; } @@ -88,9 +90,10 @@ export class CreateTaskHandler { : this.context.issueUrl; // 5. Get template and create task + const templateName = this.resolveTemplateName(); const template = await this.coder.getTemplateByOrganizationAndName( this.inputs.coderOrganization, - this.inputs.coderTemplateName, + templateName, ); const presets = await this.coder.getTemplateVersionPresets( @@ -136,6 +139,20 @@ export class CreateTaskHandler { }; } + private resolveTemplateName(): string { + const titleHasCodex = /codex/i.test(this.context.issueTitle); + const labelsHaveCodex = this.context.issueLabels.some( + (label) => label.toLowerCase() === "codex", + ); + if (titleHasCodex || labelsHaveCodex) { + this.logger.info( + `Using codex template: ${this.inputs.coderTemplateNameCodex}`, + ); + return this.inputs.coderTemplateNameCodex; + } + return this.inputs.coderTemplateName; + } + private generateTaskUrl(coderUsername: string, taskId: string): string { const baseURL = this.inputs.coderURL.replace(/\/$/, ""); return `${baseURL}/tasks/${coderUsername}/${taskId}`; diff --git a/src/handlers/failed-check.test.ts b/src/handlers/failed-check.test.ts index 6541cd6..0484898 100644 --- a/src/handlers/failed-check.test.ts +++ b/src/handlers/failed-check.test.ts @@ -15,6 +15,7 @@ const baseInputs: HandlerConfig = { coderUsername: "coder-agent", coderTaskNamePrefix: "gh", coderTemplateName: "task-template", + coderTemplateNameCodex: "task-template-codex", coderOrganization: "default", agentGithubUsername: "xmtp-coder-agent", }; diff --git a/src/handlers/issue-comment.test.ts b/src/handlers/issue-comment.test.ts index dc80107..553a2ee 100644 --- a/src/handlers/issue-comment.test.ts +++ b/src/handlers/issue-comment.test.ts @@ -16,6 +16,7 @@ const baseInputs: HandlerConfig = { coderUsername: "coder-agent", coderTaskNamePrefix: "gh", coderTemplateName: "task-template", + coderTemplateNameCodex: "task-template-codex", coderOrganization: "default", agentGithubUsername: "xmtp-coder-agent", }; diff --git a/src/handlers/pr-comment.test.ts b/src/handlers/pr-comment.test.ts index 4fb9393..f15df6f 100644 --- a/src/handlers/pr-comment.test.ts +++ b/src/handlers/pr-comment.test.ts @@ -16,6 +16,7 @@ const baseInputs: HandlerConfig = { coderUsername: "coder-agent", coderTaskNamePrefix: "gh", coderTemplateName: "task-template", + coderTemplateNameCodex: "task-template-codex", coderOrganization: "default", agentGithubUsername: "xmtp-coder-agent", }; diff --git a/src/schemas.ts b/src/schemas.ts index 3408c96..cbe0223 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -7,6 +7,7 @@ export interface HandlerConfig { coderToken: string; coderTaskNamePrefix: string; coderTemplateName: string; + coderTemplateNameCodex: string; coderTemplatePreset?: string; coderOrganization: string; agentGithubUsername: string; // replaces coderGithubUsername diff --git a/src/webhook-router.ts b/src/webhook-router.ts index eb1d026..0844357 100644 --- a/src/webhook-router.ts +++ b/src/webhook-router.ts @@ -22,6 +22,8 @@ export type HandlerType = export type CreateTaskContext = { issueNumber: number; issueUrl: string; + issueTitle: string; + issueLabels: string[]; repoName: string; repoOwner: string; senderLogin: string; @@ -243,6 +245,10 @@ export class WebhookRouter { context: { issueNumber: payload.issue.number, issueUrl: payload.issue.html_url, + issueTitle: payload.issue.title, + issueLabels: (payload.issue.labels ?? []).map((l) => + typeof l === "string" ? l : (l.name ?? ""), + ), repoName: payload.repository.name, repoOwner: payload.repository.owner.login, senderLogin: payload.sender.login,