From 321925544fc092417c2ad01a8e6f6e84b7103e6d Mon Sep 17 00:00:00 2001 From: Coder Date: Wed, 18 Mar 2026 22:02:32 +0000 Subject: [PATCH] feat: enrich outputs with full Chat API response Surface richer data from the Chat API in GHA outputs: Schema changes: - CoderChatSchema now parses the full Chat struct: parent_chat_id, root_chat_id, last_model_config_id, last_error, archived, and the full diff_status sub-object. - New ChatDiffStatusSchema captures PR/branch metadata: URL, state, title, draft, additions/deletions, changed files, branches, PR number, commits, approval, reviewer count. New GHA outputs: - chat-status: current chat lifecycle state - chat-title: auto-generated title - workspace-id: which workspace the chat is running in - pull-request-url, pull-request-state, pull-request-title, pull-request-number: PR metadata when the chat has tracked changes - additions, deletions, changed-files: diff stats - head-branch, base-branch: branch names Behavioral change: - existing-chat path now calls getChat() after sending the message so outputs always contain the full chat state, not just the ID. Tests: 45 pass (+1 new diff metadata test) --- action.yaml | 38 ++++++++++++++- dist/index.js | 112 ++++++++++++++++++++++++++++++++++++++------ src/action.test.ts | 37 +++++++++++++++ src/action.ts | 59 +++++++++++++++++------ src/coder-client.ts | 32 ++++++++++++- src/index.ts | 37 +++++++++++++++ src/schemas.ts | 13 +++++ src/test-helpers.ts | 34 +++++++++++++- 8 files changed, 330 insertions(+), 32 deletions(-) diff --git a/action.yaml b/action.yaml index 20ef8cf..15cfb26 100644 --- a/action.yaml +++ b/action.yaml @@ -64,7 +64,7 @@ outputs: description: "The Coder username resolved from GitHub user" chat-id: - description: "The chat ID" + description: "The chat ID (UUID)" chat-url: description: "The URL to view the chat in Coder" @@ -72,6 +72,42 @@ outputs: chat-created: description: "Whether the chat was newly created (true) or a message was sent to an existing chat (false)" + chat-status: + description: "Current chat status (waiting, pending, running, paused, completed, error)" + + chat-title: + description: "The auto-generated chat title" + + workspace-id: + description: "The workspace ID the chat is running in (auto-provisioned or provided)" + + pull-request-url: + description: "URL of the pull request or branch page, if the chat has tracked changes" + + pull-request-state: + description: "PR state (open, closed, merged) when available" + + pull-request-title: + description: "Title of the pull request when available" + + pull-request-number: + description: "PR number when available" + + additions: + description: "Number of lines added in tracked changes" + + deletions: + description: "Number of lines deleted in tracked changes" + + changed-files: + description: "Number of files changed in tracked changes" + + head-branch: + description: "Head branch name when available" + + base-branch: + description: "Base branch name when available" + runs: using: "node20" main: "dist/index.js" diff --git a/dist/index.js b/dist/index.js index 66e0d14..12639fc 100644 --- a/dist/index.js +++ b/dist/index.js @@ -22769,6 +22769,27 @@ class CoderAgentChatAction { core.error(`Failed to comment on issue: ${error2}`); } } + buildOutputs(coderUsername, chat, chatCreated) { + const diff = chat.diff_status; + return { + coderUsername, + chatId: chat.id, + chatUrl: this.generateChatUrl(chat.id), + chatCreated, + chatStatus: chat.status, + chatTitle: chat.title, + workspaceId: chat.workspace_id ?? undefined, + pullRequestUrl: diff?.url ?? undefined, + pullRequestState: diff?.pull_request_state ?? undefined, + pullRequestTitle: diff?.pull_request_title || undefined, + pullRequestNumber: diff?.pr_number ?? undefined, + additions: diff?.additions, + deletions: diff?.deletions, + changedFiles: diff?.changed_files, + headBranch: diff?.head_branch ?? undefined, + baseBranch: diff?.base_branch ?? undefined + }; + } async run() { let coderUsername; if (this.inputs.coderUsername) { @@ -22792,17 +22813,14 @@ class CoderAgentChatAction { model_config_id: this.inputs.modelConfigId }); core.info("Message sent successfully"); + const chat = await this.coder.getChat(chatId); + core.info(`Chat status: ${chat.status}, title: ${chat.title}`); const chatUrl2 = this.generateChatUrl(chatId); if (this.inputs.commentOnIssue) { core.info(`Commenting on issue ${githubOrg}/${githubRepo}#${githubIssueNumber}`); await this.commentOnIssue(chatUrl2, githubOrg, githubRepo, githubIssueNumber); } - return { - coderUsername, - chatId: this.inputs.existingChatId, - chatUrl: chatUrl2, - chatCreated: false - }; + return this.buildOutputs(coderUsername, chat, false); } core.info("Creating new agent chat..."); const req = { @@ -22821,12 +22839,7 @@ class CoderAgentChatAction { } else { core.info("Skipping comment on issue (commentOnIssue is false)"); } - return { - coderUsername, - chatId: createdChat.id, - chatUrl, - chatCreated: true - }; + return this.buildOutputs(coderUsername, createdChat, true); } } @@ -26894,14 +26907,41 @@ var ChatStatusSchema = exports_external.enum([ "completed", "error" ]); +var ChatDiffStatusSchema = exports_external.object({ + chat_id: exports_external.string().uuid(), + url: exports_external.string().nullable().optional(), + pull_request_state: exports_external.string().nullable().optional(), + pull_request_title: exports_external.string().default(""), + pull_request_draft: exports_external.boolean().default(false), + changes_requested: exports_external.boolean().default(false), + additions: exports_external.number().default(0), + deletions: exports_external.number().default(0), + changed_files: exports_external.number().default(0), + author_login: exports_external.string().nullable().optional(), + author_avatar_url: exports_external.string().nullable().optional(), + base_branch: exports_external.string().nullable().optional(), + head_branch: exports_external.string().nullable().optional(), + pr_number: exports_external.number().nullable().optional(), + commits: exports_external.number().nullable().optional(), + approved: exports_external.boolean().nullable().optional(), + reviewer_count: exports_external.number().nullable().optional(), + refreshed_at: exports_external.string().nullable().optional(), + stale_at: exports_external.string().nullable().optional() +}); var CoderChatSchema = exports_external.object({ id: ChatIdSchema, owner_id: exports_external.string().uuid(), workspace_id: exports_external.string().uuid().nullable().optional(), + parent_chat_id: exports_external.string().uuid().nullable().optional(), + root_chat_id: exports_external.string().uuid().nullable().optional(), + last_model_config_id: exports_external.string().uuid().optional(), title: exports_external.string(), status: ChatStatusSchema, + last_error: exports_external.string().nullable().optional(), + diff_status: ChatDiffStatusSchema.nullable().optional(), created_at: exports_external.string(), - updated_at: exports_external.string() + updated_at: exports_external.string(), + archived: exports_external.boolean().optional() }); var CoderChatListResponseSchema = exports_external.array(CoderChatSchema); var ChatInputPartSchema = exports_external.object({ @@ -26961,7 +27001,19 @@ var ActionOutputsSchema = exports_external.object({ coderUsername: exports_external.string(), chatId: exports_external.string().uuid(), chatUrl: exports_external.string().url(), - chatCreated: exports_external.boolean() + chatCreated: exports_external.boolean(), + chatStatus: exports_external.string(), + chatTitle: exports_external.string(), + workspaceId: exports_external.string().uuid().optional(), + pullRequestUrl: exports_external.string().optional(), + pullRequestState: exports_external.string().optional(), + pullRequestTitle: exports_external.string().optional(), + pullRequestNumber: exports_external.number().optional(), + additions: exports_external.number().optional(), + deletions: exports_external.number().optional(), + changedFiles: exports_external.number().optional(), + headBranch: exports_external.string().optional(), + baseBranch: exports_external.string().optional() }); // src/index.ts @@ -26997,6 +27049,38 @@ async function main() { core2.setOutput("chat-id", outputs.chatId); core2.setOutput("chat-url", outputs.chatUrl); core2.setOutput("chat-created", outputs.chatCreated.toString()); + core2.setOutput("chat-status", outputs.chatStatus); + core2.setOutput("chat-title", outputs.chatTitle); + if (outputs.workspaceId) { + core2.setOutput("workspace-id", outputs.workspaceId); + } + if (outputs.pullRequestUrl) { + core2.setOutput("pull-request-url", outputs.pullRequestUrl); + } + if (outputs.pullRequestState) { + core2.setOutput("pull-request-state", outputs.pullRequestState); + } + if (outputs.pullRequestTitle) { + core2.setOutput("pull-request-title", outputs.pullRequestTitle); + } + if (outputs.pullRequestNumber !== undefined) { + core2.setOutput("pull-request-number", outputs.pullRequestNumber.toString()); + } + if (outputs.additions !== undefined) { + core2.setOutput("additions", outputs.additions.toString()); + } + if (outputs.deletions !== undefined) { + core2.setOutput("deletions", outputs.deletions.toString()); + } + if (outputs.changedFiles !== undefined) { + core2.setOutput("changed-files", outputs.changedFiles.toString()); + } + if (outputs.headBranch) { + core2.setOutput("head-branch", outputs.headBranch); + } + if (outputs.baseBranch) { + core2.setOutput("base-branch", outputs.baseBranch); + } core2.debug("Action completed successfully"); core2.debug(`Outputs: ${JSON.stringify(outputs, null, 2)}`); } catch (error2) { diff --git a/src/action.test.ts b/src/action.test.ts index 16607e4..a2fb837 100644 --- a/src/action.test.ts +++ b/src/action.test.ts @@ -8,6 +8,7 @@ import { createMockInputs, mockUser, mockChat, + mockChatWithDiff, mockChatMessageResponse, } from "./test-helpers"; @@ -213,6 +214,9 @@ describe("CoderAgentChatAction", () => { const parsedResult = ActionOutputsSchema.parse(result); expect(parsedResult.coderUsername).toBe(mockUser.username); expect(parsedResult.chatCreated).toBe(true); + expect(parsedResult.chatStatus).toBe("running"); + expect(parsedResult.chatTitle).toBe("Test chat"); + expect(parsedResult.workspaceId).toBe(mockChat.workspace_id ?? undefined); expect(parsedResult.chatUrl).toMatch( /^https:\/\/coder\.test\/chats\/[a-f0-9-]+$/, ); @@ -246,6 +250,7 @@ describe("CoderAgentChatAction", () => { coderClient.mockCreateChatMessage.mockResolvedValue( mockChatMessageResponse, ); + coderClient.mockGetChat.mockResolvedValue(mockChat); const existingChatId = "990e8400-e29b-41d4-a716-446655440000"; const inputs = createMockInputs({ @@ -268,10 +273,14 @@ describe("CoderAgentChatAction", () => { }, ); expect(coderClient.mockCreateChat).not.toHaveBeenCalled(); + // Should fetch full chat state after sending message. + expect(coderClient.mockGetChat).toHaveBeenCalledWith(existingChatId); const parsedResult = ActionOutputsSchema.parse(result); expect(parsedResult.chatCreated).toBe(false); expect(parsedResult.chatId).toBe(existingChatId); + expect(parsedResult.chatStatus).toBe("running"); + expect(parsedResult.chatTitle).toBe("Test chat"); }); test("creates chat with workspace-id", async () => { @@ -347,6 +356,34 @@ describe("CoderAgentChatAction", () => { }); }); + test("surfaces diff/PR metadata in outputs", async () => { + coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser); + coderClient.mockCreateChat.mockResolvedValue(mockChatWithDiff); + + const inputs = createMockInputs({ githubUserID: 12345 }); + const action = new CoderAgentChatAction( + coderClient, + octokit as unknown as Octokit, + inputs, + ); + + const result = await action.run(); + + const parsedResult = ActionOutputsSchema.parse(result); + expect(parsedResult.chatStatus).toBe("completed"); + expect(parsedResult.pullRequestUrl).toBe( + "https://github.com/test-org/test-repo/pull/42", + ); + expect(parsedResult.pullRequestState).toBe("open"); + expect(parsedResult.pullRequestTitle).toBe("Fix issue #123"); + expect(parsedResult.pullRequestNumber).toBe(42); + expect(parsedResult.additions).toBe(50); + expect(parsedResult.deletions).toBe(10); + expect(parsedResult.changedFiles).toBe(3); + expect(parsedResult.headBranch).toBe("fix/issue-123"); + expect(parsedResult.baseBranch).toBe("main"); + }); + describe("Error Scenarios", () => { test("throws error when Coder user not found", async () => { coderClient.mockGetCoderUserByGithubID.mockRejectedValue( diff --git a/src/action.ts b/src/action.ts index fe268e4..6518447 100644 --- a/src/action.ts +++ b/src/action.ts @@ -1,5 +1,10 @@ import * as core from "@actions/core"; -import type { CreateChatRequest, CoderClient, ChatId } from "./coder-client"; +import type { + CreateChatRequest, + CoderClient, + CoderChat, + ChatId, +} from "./coder-client"; import type { ActionInputs, ActionOutputs } from "./schemas"; import type { getOctokit } from "@actions/github"; @@ -89,6 +94,36 @@ export class CoderAgentChatAction { } } + /** + * Build a rich ActionOutputs from a Chat response. + */ + buildOutputs( + coderUsername: string, + chat: CoderChat, + chatCreated: boolean, + ): ActionOutputs { + const diff = chat.diff_status; + return { + coderUsername, + chatId: chat.id, + chatUrl: this.generateChatUrl(chat.id), + chatCreated, + chatStatus: chat.status, + chatTitle: chat.title, + workspaceId: chat.workspace_id ?? undefined, + // Diff / PR metadata + pullRequestUrl: diff?.url ?? undefined, + pullRequestState: diff?.pull_request_state ?? undefined, + pullRequestTitle: diff?.pull_request_title || undefined, + pullRequestNumber: diff?.pr_number ?? undefined, + additions: diff?.additions, + deletions: diff?.deletions, + changedFiles: diff?.changed_files, + headBranch: diff?.head_branch ?? undefined, + baseBranch: diff?.base_branch ?? undefined, + }; + } + /** * Main action execution */ @@ -114,7 +149,8 @@ export class CoderAgentChatAction { core.info(`GitHub issue number: ${githubIssueNumber}`); core.info(`Coder username: ${coderUsername}`); - // If an existing chat ID is provided, send a message to it + // If an existing chat ID is provided, send a message then fetch + // the full chat state so outputs are always rich. if (this.inputs.existingChatId) { core.info( `Sending message to existing chat: ${this.inputs.existingChatId}`, @@ -127,8 +163,11 @@ export class CoderAgentChatAction { }); core.info("Message sent successfully"); - const chatUrl = this.generateChatUrl(chatId); + // Fetch full chat so we surface status, title, diff info. + const chat = await this.coder.getChat(chatId); + core.info(`Chat status: ${chat.status}, title: ${chat.title}`); + const chatUrl = this.generateChatUrl(chatId); if (this.inputs.commentOnIssue) { core.info( `Commenting on issue ${githubOrg}/${githubRepo}#${githubIssueNumber}`, @@ -141,12 +180,7 @@ export class CoderAgentChatAction { ); } - return { - coderUsername, - chatId: this.inputs.existingChatId, - chatUrl, - chatCreated: false, - }; + return this.buildOutputs(coderUsername, chat, false); } // Create a new chat @@ -180,11 +214,6 @@ export class CoderAgentChatAction { core.info("Skipping comment on issue (commentOnIssue is false)"); } - return { - coderUsername, - chatId: createdChat.id, - chatUrl, - chatCreated: true, - }; + return this.buildOutputs(coderUsername, createdChat, true); } } diff --git a/src/coder-client.ts b/src/coder-client.ts index 0a6b949..6ef5d8e 100644 --- a/src/coder-client.ts +++ b/src/coder-client.ts @@ -153,15 +153,45 @@ export const ChatStatusSchema = z.enum([ ]); export type ChatStatus = z.infer; -// Chat schema +// ChatDiffStatus contains PR/branch info tracked by Agents. +export const ChatDiffStatusSchema = z.object({ + chat_id: z.string().uuid(), + url: z.string().nullable().optional(), + pull_request_state: z.string().nullable().optional(), + pull_request_title: z.string().default(""), + pull_request_draft: z.boolean().default(false), + changes_requested: z.boolean().default(false), + additions: z.number().default(0), + deletions: z.number().default(0), + changed_files: z.number().default(0), + author_login: z.string().nullable().optional(), + author_avatar_url: z.string().nullable().optional(), + base_branch: z.string().nullable().optional(), + head_branch: z.string().nullable().optional(), + pr_number: z.number().nullable().optional(), + commits: z.number().nullable().optional(), + approved: z.boolean().nullable().optional(), + reviewer_count: z.number().nullable().optional(), + refreshed_at: z.string().nullable().optional(), + stale_at: z.string().nullable().optional(), +}); +export type ChatDiffStatus = z.infer; + +// Chat schema — full representation returned by the API. export const CoderChatSchema = z.object({ id: ChatIdSchema, owner_id: z.string().uuid(), workspace_id: z.string().uuid().nullable().optional(), + parent_chat_id: z.string().uuid().nullable().optional(), + root_chat_id: z.string().uuid().nullable().optional(), + last_model_config_id: z.string().uuid().optional(), title: z.string(), status: ChatStatusSchema, + last_error: z.string().nullable().optional(), + diff_status: ChatDiffStatusSchema.nullable().optional(), created_at: z.string(), updated_at: z.string(), + archived: z.boolean().optional(), }); export type CoderChat = z.infer; diff --git a/src/index.ts b/src/index.ts index 6c490d8..f71e1c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,43 @@ async function main() { core.setOutput("chat-id", outputs.chatId); core.setOutput("chat-url", outputs.chatUrl); core.setOutput("chat-created", outputs.chatCreated.toString()); + core.setOutput("chat-status", outputs.chatStatus); + core.setOutput("chat-title", outputs.chatTitle); + if (outputs.workspaceId) { + core.setOutput("workspace-id", outputs.workspaceId); + } + // Diff / PR outputs — only set when present so downstream + // steps can test with a simple `if:` guard. + if (outputs.pullRequestUrl) { + core.setOutput("pull-request-url", outputs.pullRequestUrl); + } + if (outputs.pullRequestState) { + core.setOutput("pull-request-state", outputs.pullRequestState); + } + if (outputs.pullRequestTitle) { + core.setOutput("pull-request-title", outputs.pullRequestTitle); + } + if (outputs.pullRequestNumber !== undefined) { + core.setOutput( + "pull-request-number", + outputs.pullRequestNumber.toString(), + ); + } + if (outputs.additions !== undefined) { + core.setOutput("additions", outputs.additions.toString()); + } + if (outputs.deletions !== undefined) { + core.setOutput("deletions", outputs.deletions.toString()); + } + if (outputs.changedFiles !== undefined) { + core.setOutput("changed-files", outputs.changedFiles.toString()); + } + if (outputs.headBranch) { + core.setOutput("head-branch", outputs.headBranch); + } + if (outputs.baseBranch) { + core.setOutput("base-branch", outputs.baseBranch); + } core.debug("Action completed successfully"); core.debug(`Outputs: ${JSON.stringify(outputs, null, 2)}`); diff --git a/src/schemas.ts b/src/schemas.ts index bbefede..d0d346a 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -35,6 +35,19 @@ export const ActionOutputsSchema = z.object({ chatId: z.string().uuid(), chatUrl: z.string().url(), chatCreated: z.boolean(), + chatStatus: z.string(), + chatTitle: z.string(), + workspaceId: z.string().uuid().optional(), + // Diff/PR metadata — populated when the chat has tracked changes. + pullRequestUrl: z.string().optional(), + pullRequestState: z.string().optional(), + pullRequestTitle: z.string().optional(), + pullRequestNumber: z.number().optional(), + additions: z.number().optional(), + deletions: z.number().optional(), + changedFiles: z.number().optional(), + headBranch: z.string().optional(), + baseBranch: z.string().optional(), }); export type ActionOutputs = z.infer; diff --git a/src/test-helpers.ts b/src/test-helpers.ts index f5686fa..46d1573 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -44,11 +44,43 @@ export const mockUserListDuplicate: CoderSDKGetUsersResponse = { export const mockChat: CoderChat = { id: ChatIdSchema.parse("990e8400-e29b-41d4-a716-446655440000"), owner_id: "550e8400-e29b-41d4-a716-446655440000", - workspace_id: null, + workspace_id: "aa0e8400-e29b-41d4-a716-446655440000", + parent_chat_id: null, + root_chat_id: null, + last_model_config_id: "bb0e8400-e29b-41d4-a716-446655440000", title: "Test chat", status: "running", + last_error: null, + diff_status: null, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", + archived: false, +}; + +export const mockChatWithDiff: CoderChat = { + ...mockChat, + status: "completed", + diff_status: { + chat_id: "990e8400-e29b-41d4-a716-446655440000", + url: "https://github.com/test-org/test-repo/pull/42", + pull_request_state: "open", + pull_request_title: "Fix issue #123", + pull_request_draft: false, + changes_requested: false, + additions: 50, + deletions: 10, + changed_files: 3, + author_login: "testuser", + author_avatar_url: null, + base_branch: "main", + head_branch: "fix/issue-123", + pr_number: 42, + commits: 2, + approved: false, + reviewer_count: 0, + refreshed_at: "2024-01-01T01:00:00Z", + stale_at: null, + }, }; export const mockChatMessageResponse: CreateChatMessageResponse = {