Skip to content

Commit a2f4758

Browse files
feat: add optional coder-username input for automated workflows (#7)
* 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 * chore: rebuild dist * chore: rebuild dist with latest bun version * chore: add test for when coder username and github id are not provided * fix: error when both coder-username and github-user-id are provided * chore: rebuild dist * fix: update error message test * refactor: simplify user identification logic in CoderTaskAction by using union instead * docs: update error responses
1 parent 45aabf0 commit a2f4758

8 files changed

Lines changed: 187 additions & 47 deletions

File tree

action.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ inputs:
3333
required: true
3434

3535
github-user-id:
36-
description: "GitHub user ID to create task for"
37-
required: true
36+
description: "GitHub user ID to create task for. If provided, `coder-username` must not be set."
37+
required: false
38+
39+
coder-username:
40+
description: "Coder username to create task for. If provided, github-user-id must not be set. Useful for automated workflows without a triggering user."
41+
required: false
3842

3943
# Optional inputs
4044
coder-organization:

dist/index.js

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26918,13 +26918,20 @@ class CoderTaskAction {
2691826918
}
2691926919
}
2692026920
async run() {
26921-
core.info(`GitHub user ID: ${this.inputs.githubUserID}`);
26922-
const coderUser = await this.coder.getCoderUserByGitHubId(this.inputs.githubUserID);
26921+
let coderUsername;
26922+
if (this.inputs.coderUsername) {
26923+
core.info(`Using provided Coder username: ${this.inputs.coderUsername}`);
26924+
coderUsername = this.inputs.coderUsername;
26925+
} else {
26926+
core.info(`Looking up Coder user by GitHub user ID: ${this.inputs.githubUserID}`);
26927+
const coderUser = await this.coder.getCoderUserByGitHubId(this.inputs.githubUserID);
26928+
coderUsername = coderUser.username;
26929+
}
2692326930
const { githubOrg, githubRepo, githubIssueNumber } = this.parseGithubIssueURL();
2692426931
core.info(`GitHub owner: ${githubOrg}`);
2692526932
core.info(`GitHub repo: ${githubRepo}`);
2692626933
core.info(`GitHub issue number: ${githubIssueNumber}`);
26927-
core.info(`Coder username: ${coderUser.username}`);
26934+
core.info(`Coder username: ${coderUsername}`);
2692826935
if (!this.inputs.coderTaskNamePrefix || !this.inputs.githubIssueURL) {
2692926936
throw new Error("either taskName or both taskNamePrefix and issueURL must be provided");
2693026937
}
@@ -26956,20 +26963,20 @@ class CoderTaskAction {
2695626963
throw new Error(`Preset ${this.inputs.coderTemplatePreset} not found`);
2695726964
}
2695826965
core.info(`Coder Template: Preset ID: ${presetID}`);
26959-
const existingTask = await this.coder.getTask(coderUser.username, taskName);
26966+
const existingTask = await this.coder.getTask(coderUsername, taskName);
2696026967
if (existingTask) {
2696126968
core.info(`Coder Task: already exists: ${existingTask.name} (id: ${existingTask.id} status: ${existingTask.status})`);
2696226969
if (existingTask.status !== "active") {
2696326970
core.info(`Coder Task: waiting for task ${existingTask.name} to become active...`);
26964-
await this.coder.waitForTaskActive(coderUser.username, existingTask.id, core.debug, 1200000);
26971+
await this.coder.waitForTaskActive(coderUsername, existingTask.id, core.debug, 1200000);
2696526972
}
2696626973
core.info("Coder Task: Sending prompt to existing task...");
26967-
await this.coder.sendTaskInput(coderUser.username, existingTask.id, this.inputs.coderTaskPrompt);
26974+
await this.coder.sendTaskInput(coderUsername, existingTask.id, this.inputs.coderTaskPrompt);
2696826975
core.info("Coder Task: Prompt sent successfully");
2696926976
return {
26970-
coderUsername: coderUser.username,
26977+
coderUsername,
2697126978
taskName: existingTask.name,
26972-
taskUrl: this.generateTaskUrl(coderUser.username, existingTask.id),
26979+
taskUrl: this.generateTaskUrl(coderUsername, existingTask.id),
2697326980
taskCreated: false
2697426981
};
2697526982
}
@@ -26980,9 +26987,9 @@ class CoderTaskAction {
2698026987
template_version_preset_id: presetID,
2698126988
input: this.inputs.coderTaskPrompt
2698226989
};
26983-
const createdTask = await this.coder.createTask(coderUser.username, req);
26990+
const createdTask = await this.coder.createTask(coderUsername, req);
2698426991
core.info(`Coder Task: created successfully (status: ${createdTask.status})`);
26985-
const taskUrl = this.generateTaskUrl(coderUser.username, createdTask.id);
26992+
const taskUrl = this.generateTaskUrl(coderUsername, createdTask.id);
2698626993
core.info(`Coder Task: URL: ${taskUrl}`);
2698726994
if (this.inputs.commentOnIssue) {
2698826995
core.info(`Commenting on issue ${githubOrg}/${githubRepo}#${githubIssueNumber}`);
@@ -26992,7 +26999,7 @@ class CoderTaskAction {
2699226999
core.info(`Skipping comment on issue (commentOnIssue is false)`);
2699327000
}
2699427001
return {
26995-
coderUsername: coderUser.username,
27002+
coderUsername,
2699627003
taskName,
2699727004
taskUrl,
2699827005
taskCreated: true
@@ -27001,19 +27008,30 @@ class CoderTaskAction {
2700127008
}
2700227009

2700327010
// src/schemas.ts
27004-
var ActionInputsSchema = exports_external.object({
27011+
var BaseInputsSchema = exports_external.object({
2700527012
coderTaskPrompt: exports_external.string().min(1),
2700627013
coderToken: exports_external.string().min(1),
2700727014
coderURL: exports_external.string().url(),
2700827015
coderTemplateName: exports_external.string().min(1),
2700927016
githubIssueURL: exports_external.string().url(),
2701027017
githubToken: exports_external.string(),
27011-
githubUserID: exports_external.number().min(1),
2701227018
coderOrganization: exports_external.string().min(1).optional().default("default"),
2701327019
coderTaskNamePrefix: exports_external.string().min(1).optional().default("gh"),
2701427020
coderTemplatePreset: exports_external.string().optional(),
2701527021
commentOnIssue: exports_external.boolean().default(true)
2701627022
});
27023+
var WithGithubUserIDSchema = BaseInputsSchema.extend({
27024+
githubUserID: exports_external.number().min(1),
27025+
coderUsername: exports_external.undefined()
27026+
});
27027+
var WithCoderUsernameSchema = BaseInputsSchema.extend({
27028+
githubUserID: exports_external.undefined(),
27029+
coderUsername: exports_external.string().min(1)
27030+
});
27031+
var ActionInputsSchema = exports_external.union([
27032+
WithGithubUserIDSchema,
27033+
WithCoderUsernameSchema
27034+
]);
2701727035
var ActionOutputsSchema = exports_external.object({
2701827036
coderUsername: exports_external.string(),
2701927037
taskName: exports_external.string(),
@@ -27024,6 +27042,8 @@ var ActionOutputsSchema = exports_external.object({
2702427042
// src/index.ts
2702527043
async function main() {
2702627044
try {
27045+
const githubUserIdInput = core2.getInput("github-user-id");
27046+
const githubUserID = githubUserIdInput ? Number.parseInt(githubUserIdInput, 10) : undefined;
2702727047
const inputs = ActionInputsSchema.parse({
2702827048
coderURL: core2.getInput("coder-url", { required: true }),
2702927049
coderToken: core2.getInput("coder-token", { required: true }),
@@ -27039,7 +27059,8 @@ async function main() {
2703927059
}),
2704027060
githubIssueURL: core2.getInput("github-issue-url", { required: true }),
2704127061
githubToken: core2.getInput("github-token", { required: true }),
27042-
githubUserID: Number.parseInt(core2.getInput("github-user-id", { required: true }), 10),
27062+
githubUserID,
27063+
coderUsername: core2.getInput("coder-username") || undefined,
2704327064
coderTemplatePreset: core2.getInput("coder-template-preset") || undefined,
2704427065
commentOnIssue: core2.getBooleanInput("comment-on-issue")
2704527066
});

src/action.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,46 @@ describe("CoderTaskAction", () => {
378378
assertActionOutputs(parsedResult, true);
379379
});
380380

381+
test("creates new task using direct coder-username (without github-user-id)", async () => {
382+
// Setup - no user lookup needed when coder-username is provided directly
383+
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
384+
mockTemplate,
385+
);
386+
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
387+
coderClient.mockGetTask.mockResolvedValue(null);
388+
coderClient.mockCreateTask.mockResolvedValue(mockTask);
389+
coderClient.mockWaitForTaskActive.mockResolvedValue(undefined);
390+
391+
const inputs = createMockInputs({
392+
githubUserID: undefined,
393+
coderUsername: mockUser.username,
394+
});
395+
const action = new CoderTaskAction(
396+
coderClient,
397+
octokit as unknown as Octokit,
398+
inputs,
399+
);
400+
401+
// Execute
402+
const result = await action.run();
403+
404+
// Verify - should NOT call any user lookup API when username is provided directly
405+
expect(coderClient.mockGetCoderUserByGithubID).not.toHaveBeenCalled();
406+
expect(coderClient.mockGetTask).toHaveBeenCalledWith(
407+
mockUser.username,
408+
mockTask.name,
409+
);
410+
expect(coderClient.mockCreateTask).toHaveBeenCalledWith(mockUser.username, {
411+
name: mockTask.name,
412+
template_version_id: mockTemplate.active_version_id,
413+
template_version_preset_id: undefined,
414+
input: inputs.coderTaskPrompt,
415+
});
416+
417+
const parsedResult = ActionOutputsSchema.parse(result);
418+
assertActionOutputs(parsedResult, true);
419+
});
420+
381421
test("sends prompt to existing task", async () => {
382422
// Setup
383423
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);

src/action.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,25 @@ export class CoderTaskAction {
104104
* Main action execution
105105
*/
106106
async run(): Promise<ActionOutputs> {
107-
core.info(`GitHub user ID: ${this.inputs.githubUserID}`);
108-
const coderUser = await this.coder.getCoderUserByGitHubId(
109-
this.inputs.githubUserID,
110-
);
107+
let coderUsername: string;
108+
if (this.inputs.coderUsername) {
109+
core.info(`Using provided Coder username: ${this.inputs.coderUsername}`);
110+
coderUsername = this.inputs.coderUsername;
111+
} else {
112+
core.info(
113+
`Looking up Coder user by GitHub user ID: ${this.inputs.githubUserID}`,
114+
);
115+
const coderUser = await this.coder.getCoderUserByGitHubId(
116+
this.inputs.githubUserID,
117+
);
118+
coderUsername = coderUser.username;
119+
}
111120
const { githubOrg, githubRepo, githubIssueNumber } =
112121
this.parseGithubIssueURL();
113122
core.info(`GitHub owner: ${githubOrg}`);
114123
core.info(`GitHub repo: ${githubRepo}`);
115124
core.info(`GitHub issue number: ${githubIssueNumber}`);
116-
core.info(`Coder username: ${coderUser.username}`);
125+
core.info(`Coder username: ${coderUsername}`);
117126
if (!this.inputs.coderTaskNamePrefix || !this.inputs.githubIssueURL) {
118127
throw new Error(
119128
"either taskName or both taskNamePrefix and issueURL must be provided",
@@ -158,7 +167,7 @@ export class CoderTaskAction {
158167
}
159168
core.info(`Coder Template: Preset ID: ${presetID}`);
160169

161-
const existingTask = await this.coder.getTask(coderUser.username, taskName);
170+
const existingTask = await this.coder.getTask(coderUsername, taskName);
162171
if (existingTask) {
163172
core.info(
164173
`Coder Task: already exists: ${existingTask.name} (id: ${existingTask.id} status: ${existingTask.status})`,
@@ -170,7 +179,7 @@ export class CoderTaskAction {
170179
`Coder Task: waiting for task ${existingTask.name} to become active...`,
171180
);
172181
await this.coder.waitForTaskActive(
173-
coderUser.username,
182+
coderUsername,
174183
existingTask.id,
175184
core.debug,
176185
1_200_000,
@@ -180,15 +189,15 @@ export class CoderTaskAction {
180189
core.info("Coder Task: Sending prompt to existing task...");
181190
// Send prompt to existing task using the task ID (UUID)
182191
await this.coder.sendTaskInput(
183-
coderUser.username,
192+
coderUsername,
184193
existingTask.id,
185194
this.inputs.coderTaskPrompt,
186195
);
187196
core.info("Coder Task: Prompt sent successfully");
188197
return {
189-
coderUsername: coderUser.username,
198+
coderUsername,
190199
taskName: existingTask.name,
191-
taskUrl: this.generateTaskUrl(coderUser.username, existingTask.id),
200+
taskUrl: this.generateTaskUrl(coderUsername, existingTask.id),
192201
taskCreated: false,
193202
};
194203
}
@@ -201,13 +210,13 @@ export class CoderTaskAction {
201210
input: this.inputs.coderTaskPrompt,
202211
};
203212
// Create new task
204-
const createdTask = await this.coder.createTask(coderUser.username, req);
213+
const createdTask = await this.coder.createTask(coderUsername, req);
205214
core.info(
206215
`Coder Task: created successfully (status: ${createdTask.status})`,
207216
);
208217

209218
// 5. Generate task URL
210-
const taskUrl = this.generateTaskUrl(coderUser.username, createdTask.id);
219+
const taskUrl = this.generateTaskUrl(coderUsername, createdTask.id);
211220
core.info(`Coder Task: URL: ${taskUrl}`);
212221

213222
// 6. Comment on issue if requested
@@ -226,8 +235,8 @@ export class CoderTaskAction {
226235
core.info(`Skipping comment on issue (commentOnIssue is false)`);
227236
}
228237
return {
229-
coderUsername: coderUser.username,
230-
taskName: taskName,
238+
coderUsername,
239+
taskName,
231240
taskUrl,
232241
taskCreated: true,
233242
};

src/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { ActionInputsSchema } from "./schemas";
77
async function main() {
88
try {
99
// Parse and validate inputs
10+
const githubUserIdInput = core.getInput("github-user-id");
11+
const githubUserID = githubUserIdInput
12+
? Number.parseInt(githubUserIdInput, 10)
13+
: undefined;
14+
1015
const inputs = ActionInputsSchema.parse({
1116
coderURL: core.getInput("coder-url", { required: true }),
1217
coderToken: core.getInput("coder-token", { required: true }),
@@ -22,10 +27,8 @@ async function main() {
2227
}),
2328
githubIssueURL: core.getInput("github-issue-url", { required: true }),
2429
githubToken: core.getInput("github-token", { required: true }),
25-
githubUserID: Number.parseInt(
26-
core.getInput("github-user-id", { required: true }),
27-
10,
28-
),
30+
githubUserID,
31+
coderUsername: core.getInput("coder-username") || undefined,
2932
coderTemplatePreset: core.getInput("coder-template-preset") || undefined,
3033
commentOnIssue: core.getBooleanInput("comment-on-issue"),
3134
});

0 commit comments

Comments
 (0)