From 15d92529ce22c3adea3cd22da19bbb6d7e4c3c59 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Thu, 15 Jan 2026 16:13:04 -0600 Subject: [PATCH 1/9] feat: add optional coder-username input for automated workflows Allow tasks to be created under a specific Coder user without requiring a GitHub user ID lookup. This enables automated workflows (e.g., CI bots, scheduled jobs) to run tasks under a service account. - Add coder-username input, make github-user-id optional - Skip API lookup when username is provided directly - Either coder-username or github-user-id must be provided --- action.yaml | 8 ++++-- src/action.test.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++ src/action.ts | 39 +++++++++++++++++--------- src/index.ts | 11 +++++--- src/schemas.ts | 4 ++- 5 files changed, 112 insertions(+), 20 deletions(-) diff --git a/action.yaml b/action.yaml index ce3b9dc..78a3564 100644 --- a/action.yaml +++ b/action.yaml @@ -33,8 +33,12 @@ inputs: required: true github-user-id: - description: "GitHub user ID to create task for" - required: true + description: "GitHub user ID to create task for. Required unless coder-username is provided." + required: false + + coder-username: + description: "Coder username to create task for. If provided, github-user-id is not required. Useful for automated workflows without a triggering user." + required: false # Optional inputs coder-organization: diff --git a/src/action.test.ts b/src/action.test.ts index 5c830d6..3aaa8c9 100644 --- a/src/action.test.ts +++ b/src/action.test.ts @@ -378,6 +378,76 @@ describe("CoderTaskAction", () => { assertActionOutputs(parsedResult, true); }); + test("creates new task using direct coder-username (without github-user-id)", async () => { + // Setup - no user lookup needed when coder-username is provided directly + coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue( + mockTemplate, + ); + coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]); + coderClient.mockGetTask.mockResolvedValue(null); + coderClient.mockCreateTask.mockResolvedValue(mockTask); + coderClient.mockWaitForTaskActive.mockResolvedValue(undefined); + + const inputs = createMockInputs({ + githubUserID: undefined, + coderUsername: mockUser.username, + }); + const action = new CoderTaskAction( + coderClient, + octokit as unknown as Octokit, + inputs, + ); + + // Execute + const result = await action.run(); + + // Verify - should NOT call any user lookup API when username is provided directly + expect(coderClient.mockGetCoderUserByGithubID).not.toHaveBeenCalled(); + expect(coderClient.mockGetTask).toHaveBeenCalledWith( + mockUser.username, + mockTask.name, + ); + expect(coderClient.mockCreateTask).toHaveBeenCalledWith(mockUser.username, { + name: mockTask.name, + template_version_id: mockTemplate.active_version_id, + template_version_preset_id: undefined, + input: inputs.coderTaskPrompt, + }); + + const parsedResult = ActionOutputsSchema.parse(result); + assertActionOutputs(parsedResult, true); + }); + + test("prefers coder-username over github-user-id when both provided", async () => { + // Setup - no user lookup needed when coder-username is provided directly + coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue( + mockTemplate, + ); + coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]); + coderClient.mockGetTask.mockResolvedValue(null); + coderClient.mockCreateTask.mockResolvedValue(mockTask); + coderClient.mockWaitForTaskActive.mockResolvedValue(undefined); + + const inputs = createMockInputs({ + githubUserID: 12345, + coderUsername: mockUser.username, + }); + const action = new CoderTaskAction( + coderClient, + octokit as unknown as Octokit, + inputs, + ); + + // Execute + const result = await action.run(); + + // Verify - should use coderUsername directly, skipping GitHub ID lookup + expect(coderClient.mockGetCoderUserByGithubID).not.toHaveBeenCalled(); + + const parsedResult = ActionOutputsSchema.parse(result); + assertActionOutputs(parsedResult, true); + }); + test("sends prompt to existing task", async () => { // Setup coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser); diff --git a/src/action.ts b/src/action.ts index 2eb0bfe..ce7b3f8 100644 --- a/src/action.ts +++ b/src/action.ts @@ -104,16 +104,29 @@ export class CoderTaskAction { * Main action execution */ async run(): Promise { - core.info(`GitHub user ID: ${this.inputs.githubUserID}`); - const coderUser = await this.coder.getCoderUserByGitHubId( - this.inputs.githubUserID, - ); + let coderUsername: string; + if (this.inputs.coderUsername) { + core.info(`Using provided Coder username: ${this.inputs.coderUsername}`); + coderUsername = this.inputs.coderUsername; + } else if (this.inputs.githubUserID) { + core.info( + `Looking up Coder user by GitHub user ID: ${this.inputs.githubUserID}`, + ); + const coderUser = await this.coder.getCoderUserByGitHubId( + this.inputs.githubUserID, + ); + coderUsername = coderUser.username; + } else { + throw new Error( + "Either coder-username or github-user-id must be provided", + ); + } const { githubOrg, githubRepo, githubIssueNumber } = this.parseGithubIssueURL(); core.info(`GitHub owner: ${githubOrg}`); core.info(`GitHub repo: ${githubRepo}`); core.info(`GitHub issue number: ${githubIssueNumber}`); - core.info(`Coder username: ${coderUser.username}`); + core.info(`Coder username: ${coderUsername}`); if (!this.inputs.coderTaskNamePrefix || !this.inputs.githubIssueURL) { throw new Error( "either taskName or both taskNamePrefix and issueURL must be provided", @@ -158,7 +171,7 @@ export class CoderTaskAction { } core.info(`Coder Template: Preset ID: ${presetID}`); - const existingTask = await this.coder.getTask(coderUser.username, taskName); + const existingTask = await this.coder.getTask(coderUsername, taskName); if (existingTask) { core.info( `Coder Task: already exists: ${existingTask.name} (id: ${existingTask.id} status: ${existingTask.status})`, @@ -170,7 +183,7 @@ export class CoderTaskAction { `Coder Task: waiting for task ${existingTask.name} to become active...`, ); await this.coder.waitForTaskActive( - coderUser.username, + coderUsername, existingTask.id, core.debug, 1_200_000, @@ -180,15 +193,15 @@ export class CoderTaskAction { core.info("Coder Task: Sending prompt to existing task..."); // Send prompt to existing task using the task ID (UUID) await this.coder.sendTaskInput( - coderUser.username, + coderUsername, existingTask.id, this.inputs.coderTaskPrompt, ); core.info("Coder Task: Prompt sent successfully"); return { - coderUsername: coderUser.username, + coderUsername: coderUsername, taskName: existingTask.name, - taskUrl: this.generateTaskUrl(coderUser.username, existingTask.id), + taskUrl: this.generateTaskUrl(coderUsername, existingTask.id), taskCreated: false, }; } @@ -201,13 +214,13 @@ export class CoderTaskAction { input: this.inputs.coderTaskPrompt, }; // Create new task - const createdTask = await this.coder.createTask(coderUser.username, req); + const createdTask = await this.coder.createTask(coderUsername, req); core.info( `Coder Task: created successfully (status: ${createdTask.status})`, ); // 5. Generate task URL - const taskUrl = this.generateTaskUrl(coderUser.username, createdTask.id); + const taskUrl = this.generateTaskUrl(coderUsername, createdTask.id); core.info(`Coder Task: URL: ${taskUrl}`); // 6. Comment on issue if requested @@ -226,7 +239,7 @@ export class CoderTaskAction { core.info(`Skipping comment on issue (commentOnIssue is false)`); } return { - coderUsername: coderUser.username, + coderUsername: coderUsername, taskName: taskName, taskUrl, taskCreated: true, diff --git a/src/index.ts b/src/index.ts index 956071d..6910ac0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,11 @@ import { ActionInputsSchema } from "./schemas"; async function main() { try { // Parse and validate inputs + const githubUserIdInput = core.getInput("github-user-id"); + const githubUserID = githubUserIdInput + ? Number.parseInt(githubUserIdInput, 10) + : undefined; + const inputs = ActionInputsSchema.parse({ coderURL: core.getInput("coder-url", { required: true }), coderToken: core.getInput("coder-token", { required: true }), @@ -22,10 +27,8 @@ async function main() { }), githubIssueURL: core.getInput("github-issue-url", { required: true }), githubToken: core.getInput("github-token", { required: true }), - githubUserID: Number.parseInt( - core.getInput("github-user-id", { required: true }), - 10, - ), + githubUserID, + coderUsername: core.getInput("coder-username") || undefined, coderTemplatePreset: core.getInput("coder-template-preset") || undefined, commentOnIssue: core.getBooleanInput("comment-on-issue"), }); diff --git a/src/schemas.ts b/src/schemas.ts index 295f854..6a37e76 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -10,7 +10,9 @@ export const ActionInputsSchema = z.object({ coderTemplateName: z.string().min(1), githubIssueURL: z.string().url(), githubToken: z.string(), - githubUserID: z.number().min(1), + // User identification - at least one must be provided (validated in action.ts) + githubUserID: z.number().min(1).optional(), + coderUsername: z.string().min(1).optional(), // Optional coderOrganization: z.string().min(1).optional().default("default"), coderTaskNamePrefix: z.string().min(1).optional().default("gh"), From ff33c556e09cc1add8b4f852a8d59d20bef5368c Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Thu, 15 Jan 2026 16:17:40 -0600 Subject: [PATCH 2/9] chore: rebuild dist --- dist/index.js | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/dist/index.js b/dist/index.js index 1ec215a..0ca5931 100644 --- a/dist/index.js +++ b/dist/index.js @@ -22708,11 +22708,11 @@ var require_github = __commonJS((exports2) => { }); // src/index.ts -var core2 = __toESM(require_core(), 1); -var github = __toESM(require_github(), 1); +var core2 = __toESM(require_core()); +var github = __toESM(require_github()); // src/action.ts -var core = __toESM(require_core(), 1); +var core = __toESM(require_core()); // node_modules/zod/v3/external.js var exports_external = {}; @@ -26918,13 +26918,22 @@ class CoderTaskAction { } } async run() { - core.info(`GitHub user ID: ${this.inputs.githubUserID}`); - const coderUser = await this.coder.getCoderUserByGitHubId(this.inputs.githubUserID); + let coderUsername; + if (this.inputs.coderUsername) { + core.info(`Using provided Coder username: ${this.inputs.coderUsername}`); + coderUsername = this.inputs.coderUsername; + } else if (this.inputs.githubUserID) { + core.info(`Looking up Coder user by GitHub user ID: ${this.inputs.githubUserID}`); + const coderUser = await this.coder.getCoderUserByGitHubId(this.inputs.githubUserID); + coderUsername = coderUser.username; + } else { + throw new Error("Either coder-username or github-user-id must be provided"); + } const { githubOrg, githubRepo, githubIssueNumber } = this.parseGithubIssueURL(); core.info(`GitHub owner: ${githubOrg}`); core.info(`GitHub repo: ${githubRepo}`); core.info(`GitHub issue number: ${githubIssueNumber}`); - core.info(`Coder username: ${coderUser.username}`); + core.info(`Coder username: ${coderUsername}`); if (!this.inputs.coderTaskNamePrefix || !this.inputs.githubIssueURL) { throw new Error("either taskName or both taskNamePrefix and issueURL must be provided"); } @@ -26956,20 +26965,20 @@ class CoderTaskAction { throw new Error(`Preset ${this.inputs.coderTemplatePreset} not found`); } core.info(`Coder Template: Preset ID: ${presetID}`); - const existingTask = await this.coder.getTask(coderUser.username, taskName); + const existingTask = await this.coder.getTask(coderUsername, taskName); if (existingTask) { core.info(`Coder Task: already exists: ${existingTask.name} (id: ${existingTask.id} status: ${existingTask.status})`); if (existingTask.status !== "active") { core.info(`Coder Task: waiting for task ${existingTask.name} to become active...`); - await this.coder.waitForTaskActive(coderUser.username, existingTask.id, core.debug, 1200000); + await this.coder.waitForTaskActive(coderUsername, existingTask.id, core.debug, 1200000); } core.info("Coder Task: Sending prompt to existing task..."); - await this.coder.sendTaskInput(coderUser.username, existingTask.id, this.inputs.coderTaskPrompt); + await this.coder.sendTaskInput(coderUsername, existingTask.id, this.inputs.coderTaskPrompt); core.info("Coder Task: Prompt sent successfully"); return { - coderUsername: coderUser.username, + coderUsername, taskName: existingTask.name, - taskUrl: this.generateTaskUrl(coderUser.username, existingTask.id), + taskUrl: this.generateTaskUrl(coderUsername, existingTask.id), taskCreated: false }; } @@ -26980,9 +26989,9 @@ class CoderTaskAction { template_version_preset_id: presetID, input: this.inputs.coderTaskPrompt }; - const createdTask = await this.coder.createTask(coderUser.username, req); + const createdTask = await this.coder.createTask(coderUsername, req); core.info(`Coder Task: created successfully (status: ${createdTask.status})`); - const taskUrl = this.generateTaskUrl(coderUser.username, createdTask.id); + const taskUrl = this.generateTaskUrl(coderUsername, createdTask.id); core.info(`Coder Task: URL: ${taskUrl}`); if (this.inputs.commentOnIssue) { core.info(`Commenting on issue ${githubOrg}/${githubRepo}#${githubIssueNumber}`); @@ -26992,7 +27001,7 @@ class CoderTaskAction { core.info(`Skipping comment on issue (commentOnIssue is false)`); } return { - coderUsername: coderUser.username, + coderUsername, taskName, taskUrl, taskCreated: true @@ -27008,7 +27017,8 @@ var ActionInputsSchema = exports_external.object({ coderTemplateName: exports_external.string().min(1), githubIssueURL: exports_external.string().url(), githubToken: exports_external.string(), - githubUserID: exports_external.number().min(1), + githubUserID: exports_external.number().min(1).optional(), + coderUsername: exports_external.string().min(1).optional(), coderOrganization: exports_external.string().min(1).optional().default("default"), coderTaskNamePrefix: exports_external.string().min(1).optional().default("gh"), coderTemplatePreset: exports_external.string().optional(), @@ -27024,6 +27034,8 @@ var ActionOutputsSchema = exports_external.object({ // src/index.ts async function main() { try { + const githubUserIdInput = core2.getInput("github-user-id"); + const githubUserID = githubUserIdInput ? Number.parseInt(githubUserIdInput, 10) : undefined; const inputs = ActionInputsSchema.parse({ coderURL: core2.getInput("coder-url", { required: true }), coderToken: core2.getInput("coder-token", { required: true }), @@ -27039,7 +27051,8 @@ async function main() { }), githubIssueURL: core2.getInput("github-issue-url", { required: true }), githubToken: core2.getInput("github-token", { required: true }), - githubUserID: Number.parseInt(core2.getInput("github-user-id", { required: true }), 10), + githubUserID, + coderUsername: core2.getInput("coder-username") || undefined, coderTemplatePreset: core2.getInput("coder-template-preset") || undefined, commentOnIssue: core2.getBooleanInput("comment-on-issue") }); From 461cc2abc65e681394574ad564426d0ba6465686 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Thu, 15 Jan 2026 16:19:51 -0600 Subject: [PATCH 3/9] chore: rebuild dist with latest bun version --- dist/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/index.js b/dist/index.js index 0ca5931..6468db1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -22708,11 +22708,11 @@ var require_github = __commonJS((exports2) => { }); // src/index.ts -var core2 = __toESM(require_core()); -var github = __toESM(require_github()); +var core2 = __toESM(require_core(), 1); +var github = __toESM(require_github(), 1); // src/action.ts -var core = __toESM(require_core()); +var core = __toESM(require_core(), 1); // node_modules/zod/v3/external.js var exports_external = {}; From 0a4d03dd63ae0926253556129f782c2d24e47298 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Thu, 15 Jan 2026 16:27:25 -0600 Subject: [PATCH 4/9] chore: add test for when coder username and github id are not provided --- src/action.test.ts | 17 +++++++++++++++++ src/action.ts | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/action.test.ts b/src/action.test.ts index 3aaa8c9..a4ff61f 100644 --- a/src/action.test.ts +++ b/src/action.test.ts @@ -448,6 +448,23 @@ describe("CoderTaskAction", () => { assertActionOutputs(parsedResult, true); }); + test("throws error when neither coder-username nor github-user-id is provided", async () => { + const inputs = createMockInputs({ + githubUserID: undefined, + coderUsername: undefined, + }); + const action = new CoderTaskAction( + coderClient, + octokit as unknown as Octokit, + inputs, + ); + + // Execute & Verify + expect(action.run()).rejects.toThrow( + "Either coder-username or github-user-id must be provided", + ); + }); + test("sends prompt to existing task", async () => { // Setup coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser); diff --git a/src/action.ts b/src/action.ts index ce7b3f8..1271b79 100644 --- a/src/action.ts +++ b/src/action.ts @@ -199,7 +199,7 @@ export class CoderTaskAction { ); core.info("Coder Task: Prompt sent successfully"); return { - coderUsername: coderUsername, + coderUsername, taskName: existingTask.name, taskUrl: this.generateTaskUrl(coderUsername, existingTask.id), taskCreated: false, @@ -239,8 +239,8 @@ export class CoderTaskAction { core.info(`Skipping comment on issue (commentOnIssue is false)`); } return { - coderUsername: coderUsername, - taskName: taskName, + coderUsername, + taskName, taskUrl, taskCreated: true, }; From d5679bb33479d44eb325993d1ba3bcbc200bd400 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 16 Jan 2026 08:09:25 -0600 Subject: [PATCH 5/9] fix: error when both coder-username and github-user-id are provided --- dist/index.js | 9 ++++++--- src/action.test.ts | 23 +++++------------------ src/action.ts | 7 +++++++ 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/dist/index.js b/dist/index.js index 6468db1..210c937 100644 --- a/dist/index.js +++ b/dist/index.js @@ -17177,10 +17177,10 @@ var require_oidc_utils = __commonJS((exports2) => { return __awaiter(this, undefined, undefined, function* () { const httpclient = OidcClient.createHttpClient(); const res = yield httpclient.getJson(id_token_url).catch((error) => { - throw new Error(`Failed to get ID Token. - + throw new Error(`Failed to get ID Token. + Error Code : ${error.statusCode} - + Error Message: ${error.message}`); }); const id_token = (_a = res.result) === null || _a === undefined ? undefined : _a.value; @@ -26918,6 +26918,9 @@ class CoderTaskAction { } } async run() { + if (this.inputs.coderUsername && this.inputs.githubUserID) { + throw new Error("Both coder-username and github-user-id were provided. Please provide only one as the intent is unclear."); + } let coderUsername; if (this.inputs.coderUsername) { core.info(`Using provided Coder username: ${this.inputs.coderUsername}`); diff --git a/src/action.test.ts b/src/action.test.ts index a4ff61f..d5934fe 100644 --- a/src/action.test.ts +++ b/src/action.test.ts @@ -418,16 +418,7 @@ describe("CoderTaskAction", () => { assertActionOutputs(parsedResult, true); }); - test("prefers coder-username over github-user-id when both provided", async () => { - // Setup - no user lookup needed when coder-username is provided directly - coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue( - mockTemplate, - ); - coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]); - coderClient.mockGetTask.mockResolvedValue(null); - coderClient.mockCreateTask.mockResolvedValue(mockTask); - coderClient.mockWaitForTaskActive.mockResolvedValue(undefined); - + test("errors when both coder-username and github-user-id are provided", async () => { const inputs = createMockInputs({ githubUserID: 12345, coderUsername: mockUser.username, @@ -438,14 +429,10 @@ describe("CoderTaskAction", () => { inputs, ); - // Execute - const result = await action.run(); - - // Verify - should use coderUsername directly, skipping GitHub ID lookup - expect(coderClient.mockGetCoderUserByGithubID).not.toHaveBeenCalled(); - - const parsedResult = ActionOutputsSchema.parse(result); - assertActionOutputs(parsedResult, true); + // Execute & Verify - should throw due to ambiguous input + expect(action.run()).rejects.toThrow( + "Both coder-username and github-user-id were provided. Please provide only one to avoid ambiguity.", + ); }); test("throws error when neither coder-username nor github-user-id is provided", async () => { diff --git a/src/action.ts b/src/action.ts index 1271b79..5dbdf60 100644 --- a/src/action.ts +++ b/src/action.ts @@ -104,6 +104,13 @@ export class CoderTaskAction { * Main action execution */ async run(): Promise { + // Validate that exactly one of coderUsername or githubUserID is provided + if (this.inputs.coderUsername && this.inputs.githubUserID) { + throw new Error( + "Both coder-username and github-user-id were provided. Please provide only one as the intent is unclear.", + ); + } + let coderUsername: string; if (this.inputs.coderUsername) { core.info(`Using provided Coder username: ${this.inputs.coderUsername}`); From 9e67378a775b99d42a110278ff1989bb8226ec05 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 16 Jan 2026 08:12:24 -0600 Subject: [PATCH 6/9] chore: rebuild dist --- dist/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/index.js b/dist/index.js index 210c937..4dac25f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -17177,10 +17177,10 @@ var require_oidc_utils = __commonJS((exports2) => { return __awaiter(this, undefined, undefined, function* () { const httpclient = OidcClient.createHttpClient(); const res = yield httpclient.getJson(id_token_url).catch((error) => { - throw new Error(`Failed to get ID Token. - + throw new Error(`Failed to get ID Token. + Error Code : ${error.statusCode} - + Error Message: ${error.message}`); }); const id_token = (_a = res.result) === null || _a === undefined ? undefined : _a.value; From 9848a81a4e03c7ec5b9a6413f8abd36f333aa2f5 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 16 Jan 2026 08:19:52 -0600 Subject: [PATCH 7/9] fix: update error message test --- src/action.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action.test.ts b/src/action.test.ts index d5934fe..66f98cb 100644 --- a/src/action.test.ts +++ b/src/action.test.ts @@ -431,7 +431,7 @@ describe("CoderTaskAction", () => { // Execute & Verify - should throw due to ambiguous input expect(action.run()).rejects.toThrow( - "Both coder-username and github-user-id were provided. Please provide only one to avoid ambiguity.", + "Both coder-username and github-user-id were provided. Please provide only one as the intent is unclear.", ); }); From fcde9a9c26393b3141ac8442d7a872b9542abcf0 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 16 Jan 2026 09:40:11 -0600 Subject: [PATCH 8/9] refactor: simplify user identification logic in CoderTaskAction by using union instead --- dist/index.js | 23 ++++++++++------- src/action.test.ts | 33 ------------------------ src/action.ts | 13 +--------- src/schemas.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++----- src/schemas.ts | 26 +++++++++++++------ src/test-helpers.ts | 2 +- 6 files changed, 91 insertions(+), 69 deletions(-) diff --git a/dist/index.js b/dist/index.js index 4dac25f..f32a1fb 100644 --- a/dist/index.js +++ b/dist/index.js @@ -26918,19 +26918,14 @@ class CoderTaskAction { } } async run() { - if (this.inputs.coderUsername && this.inputs.githubUserID) { - throw new Error("Both coder-username and github-user-id were provided. Please provide only one as the intent is unclear."); - } let coderUsername; if (this.inputs.coderUsername) { core.info(`Using provided Coder username: ${this.inputs.coderUsername}`); coderUsername = this.inputs.coderUsername; - } else if (this.inputs.githubUserID) { + } else { core.info(`Looking up Coder user by GitHub user ID: ${this.inputs.githubUserID}`); const coderUser = await this.coder.getCoderUserByGitHubId(this.inputs.githubUserID); coderUsername = coderUser.username; - } else { - throw new Error("Either coder-username or github-user-id must be provided"); } const { githubOrg, githubRepo, githubIssueNumber } = this.parseGithubIssueURL(); core.info(`GitHub owner: ${githubOrg}`); @@ -27013,20 +27008,30 @@ class CoderTaskAction { } // src/schemas.ts -var ActionInputsSchema = exports_external.object({ +var BaseInputsSchema = exports_external.object({ coderTaskPrompt: exports_external.string().min(1), coderToken: exports_external.string().min(1), coderURL: exports_external.string().url(), coderTemplateName: exports_external.string().min(1), githubIssueURL: exports_external.string().url(), githubToken: exports_external.string(), - githubUserID: exports_external.number().min(1).optional(), - coderUsername: exports_external.string().min(1).optional(), coderOrganization: exports_external.string().min(1).optional().default("default"), coderTaskNamePrefix: exports_external.string().min(1).optional().default("gh"), coderTemplatePreset: exports_external.string().optional(), commentOnIssue: exports_external.boolean().default(true) }); +var WithGithubUserIDSchema = BaseInputsSchema.extend({ + githubUserID: exports_external.number().min(1), + coderUsername: exports_external.undefined() +}); +var WithCoderUsernameSchema = BaseInputsSchema.extend({ + githubUserID: exports_external.undefined(), + coderUsername: exports_external.string().min(1) +}); +var ActionInputsSchema = exports_external.union([ + WithGithubUserIDSchema, + WithCoderUsernameSchema +]); var ActionOutputsSchema = exports_external.object({ coderUsername: exports_external.string(), taskName: exports_external.string(), diff --git a/src/action.test.ts b/src/action.test.ts index 66f98cb..3c89cf1 100644 --- a/src/action.test.ts +++ b/src/action.test.ts @@ -418,39 +418,6 @@ describe("CoderTaskAction", () => { assertActionOutputs(parsedResult, true); }); - test("errors when both coder-username and github-user-id are provided", async () => { - const inputs = createMockInputs({ - githubUserID: 12345, - coderUsername: mockUser.username, - }); - const action = new CoderTaskAction( - coderClient, - octokit as unknown as Octokit, - inputs, - ); - - // Execute & Verify - should throw due to ambiguous input - expect(action.run()).rejects.toThrow( - "Both coder-username and github-user-id were provided. Please provide only one as the intent is unclear.", - ); - }); - - test("throws error when neither coder-username nor github-user-id is provided", async () => { - const inputs = createMockInputs({ - githubUserID: undefined, - coderUsername: undefined, - }); - const action = new CoderTaskAction( - coderClient, - octokit as unknown as Octokit, - inputs, - ); - - // Execute & Verify - expect(action.run()).rejects.toThrow( - "Either coder-username or github-user-id must be provided", - ); - }); test("sends prompt to existing task", async () => { // Setup diff --git a/src/action.ts b/src/action.ts index 5dbdf60..248d065 100644 --- a/src/action.ts +++ b/src/action.ts @@ -104,18 +104,11 @@ export class CoderTaskAction { * Main action execution */ async run(): Promise { - // Validate that exactly one of coderUsername or githubUserID is provided - if (this.inputs.coderUsername && this.inputs.githubUserID) { - throw new Error( - "Both coder-username and github-user-id were provided. Please provide only one as the intent is unclear.", - ); - } - let coderUsername: string; if (this.inputs.coderUsername) { core.info(`Using provided Coder username: ${this.inputs.coderUsername}`); coderUsername = this.inputs.coderUsername; - } else if (this.inputs.githubUserID) { + } else { core.info( `Looking up Coder user by GitHub user ID: ${this.inputs.githubUserID}`, ); @@ -123,10 +116,6 @@ export class CoderTaskAction { this.inputs.githubUserID, ); coderUsername = coderUser.username; - } else { - throw new Error( - "Either coder-username or github-user-id must be provided", - ); } const { githubOrg, githubRepo, githubIssueNumber } = this.parseGithubIssueURL(); diff --git a/src/schemas.test.ts b/src/schemas.test.ts index ccd2ad8..06bb92f 100644 --- a/src/schemas.test.ts +++ b/src/schemas.test.ts @@ -34,7 +34,7 @@ describe("ActionInputsSchema", () => { }); test("accepts all optional inputs", () => { - const input: ActionInputs = { + const input = { ...actionInputValid, coderTemplatePreset: "custom", }; @@ -54,7 +54,7 @@ describe("ActionInputsSchema", () => { ]; for (const url of validUrls) { - const input: ActionInputs = { + const input = { ...actionInputValid, coderURL: url, }; @@ -66,12 +66,12 @@ describe("ActionInputsSchema", () => { describe("Invalid Input Cases", () => { test("rejects missing required fields", () => { - const input = {} as ActionInputs; + const input = {}; expect(() => ActionInputsSchema.parse(input)).toThrow(); }); test("rejects invalid URL format for coderUrl", () => { - const input: ActionInputs = { + const input = { ...actionInputValid, coderURL: "not-a-url", }; @@ -79,7 +79,7 @@ describe("ActionInputsSchema", () => { }); test("rejects invalid URL format for issueUrl", () => { - const input: ActionInputs = { + const input = { ...actionInputValid, githubIssueURL: "not-a-url", }; @@ -87,11 +87,62 @@ describe("ActionInputsSchema", () => { }); test("rejects empty strings for required fields", () => { - const input: ActionInputs = { + const input = { ...actionInputValid, coderToken: "", }; expect(() => ActionInputsSchema.parse(input)).toThrow(); }); }); + + describe("User Identification (Union Validation)", () => { + test("accepts input with only githubUserID", () => { + const result = ActionInputsSchema.parse(actionInputValid); + expect(result.githubUserID).toBe(12345); + expect(result.coderUsername).toBeUndefined(); + }); + + test("accepts input with only coderUsername", () => { + const { githubUserID: _, ...withoutGithubUserID } = actionInputValid; + const input = { ...withoutGithubUserID, coderUsername: "testuser" }; + const result = ActionInputsSchema.parse(input); + expect(result.coderUsername).toBe("testuser"); + expect(result.githubUserID).toBeUndefined(); + }); + + test("rejects input with both githubUserID and coderUsername", () => { + const input = { + ...actionInputValid, + coderUsername: "testuser", + }; + expect(() => ActionInputsSchema.parse(input)).toThrow(); + }); + + test("rejects input with neither githubUserID nor coderUsername", () => { + const { githubUserID: _, ...withoutGithubUserID } = actionInputValid; + expect(() => ActionInputsSchema.parse(withoutGithubUserID)).toThrow(); + }); + + test("rejects githubUserID of 0", () => { + const input = { + ...actionInputValid, + githubUserID: 0, + }; + expect(() => ActionInputsSchema.parse(input)).toThrow(); + }); + + test("rejects negative githubUserID", () => { + const input = { + ...actionInputValid, + githubUserID: -1, + }; + expect(() => ActionInputsSchema.parse(input)).toThrow(); + }); + + test("rejects empty coderUsername", () => { + const { githubUserID: _, ...withoutGithubUserID } = actionInputValid; + const input = { ...withoutGithubUserID, coderUsername: "" }; + expect(() => ActionInputsSchema.parse(input)).toThrow(); + }); + }); }); diff --git a/src/schemas.ts b/src/schemas.ts index 6a37e76..437e836 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,25 +1,35 @@ import { z } from "zod"; -export type ActionInputs = z.infer; - -export const ActionInputsSchema = z.object({ - // Required +const BaseInputsSchema = z.object({ coderTaskPrompt: z.string().min(1), coderToken: z.string().min(1), coderURL: z.string().url(), coderTemplateName: z.string().min(1), githubIssueURL: z.string().url(), githubToken: z.string(), - // User identification - at least one must be provided (validated in action.ts) - githubUserID: z.number().min(1).optional(), - coderUsername: z.string().min(1).optional(), - // Optional coderOrganization: z.string().min(1).optional().default("default"), coderTaskNamePrefix: z.string().min(1).optional().default("gh"), coderTemplatePreset: z.string().optional(), commentOnIssue: z.boolean().default(true), }); +const WithGithubUserIDSchema = BaseInputsSchema.extend({ + githubUserID: z.number().min(1), + coderUsername: z.undefined(), +}); + +const WithCoderUsernameSchema = BaseInputsSchema.extend({ + githubUserID: z.undefined(), + coderUsername: z.string().min(1), +}); + +export const ActionInputsSchema = z.union([ + WithGithubUserIDSchema, + WithCoderUsernameSchema, +]); + +export type ActionInputs = z.infer; + export const ActionOutputsSchema = z.object({ coderUsername: z.string(), taskName: z.string(), diff --git a/src/test-helpers.ts b/src/test-helpers.ts index 65e0e56..6c3cb1a 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -119,7 +119,7 @@ export function createMockInputs( githubUserID: 12345, commentOnIssue: true, // default value from schema ...overrides, - }; + } as ActionInputs; } /** From 9afea81fe1a5b69e06aeaa913fad5c320f80c5dc Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 16 Jan 2026 10:05:37 -0600 Subject: [PATCH 9/9] docs: update error responses --- action.yaml | 4 ++-- src/action.test.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/action.yaml b/action.yaml index 78a3564..646d48c 100644 --- a/action.yaml +++ b/action.yaml @@ -33,11 +33,11 @@ inputs: required: true github-user-id: - description: "GitHub user ID to create task for. Required unless coder-username is provided." + description: "GitHub user ID to create task for. If provided, `coder-username` must not be set." required: false coder-username: - description: "Coder username to create task for. If provided, github-user-id is not required. Useful for automated workflows without a triggering user." + description: "Coder username to create task for. If provided, github-user-id must not be set. Useful for automated workflows without a triggering user." required: false # Optional inputs diff --git a/src/action.test.ts b/src/action.test.ts index 3c89cf1..27b1960 100644 --- a/src/action.test.ts +++ b/src/action.test.ts @@ -418,7 +418,6 @@ describe("CoderTaskAction", () => { assertActionOutputs(parsedResult, true); }); - test("sends prompt to existing task", async () => { // Setup coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);