Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,50 @@ 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"

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"
112 changes: 98 additions & 14 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 = {
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 37 additions & 0 deletions src/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createMockInputs,
mockUser,
mockChat,
mockChatWithDiff,
mockChatMessageResponse,
} from "./test-helpers";

Expand Down Expand Up @@ -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-]+$/,
);
Expand Down Expand Up @@ -246,6 +250,7 @@ describe("CoderAgentChatAction", () => {
coderClient.mockCreateChatMessage.mockResolvedValue(
mockChatMessageResponse,
);
coderClient.mockGetChat.mockResolvedValue(mockChat);

const existingChatId = "990e8400-e29b-41d4-a716-446655440000";
const inputs = createMockInputs({
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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(
Expand Down
59 changes: 44 additions & 15 deletions src/action.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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
*/
Expand All @@ -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}`,
Expand All @@ -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}`,
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}
Loading
Loading