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
44 changes: 37 additions & 7 deletions actions/setup/js/route_slash_command.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,10 @@ async function addImmediateReaction(reaction) {
* @param {string} workflowId
* @param {string} ref
* @param {Record<string, string>} inputs
* @param {string} [fallbackRef]
* @returns {Promise<boolean>}
*/
async function dispatchWorkflow(workflowId, ref, inputs) {
async function dispatchWorkflow(workflowId, ref, inputs, fallbackRef) {
try {
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
Expand All @@ -294,6 +295,10 @@ async function dispatchWorkflow(workflowId, ref, inputs) {
core.info(`Skipping workflow '${workflowId}' because it is disabled.`);
return false;
}
if (isMissingWorkflowDispatchTriggerError(error) && fallbackRef && ref !== fallbackRef) {
core.info(`Workflow '${workflowId}' not found on ref '${ref}'; retrying on default branch '${fallbackRef}'.`);
return dispatchWorkflow(workflowId, fallbackRef, inputs, undefined);
}
throw new Error(`Failed to dispatch workflow '${workflowId}' on ref '${ref}': ${String(error)}`);
}
}
Expand Down Expand Up @@ -324,6 +329,20 @@ function isDisabledWorkflowDispatchError(error) {
return message.includes("workflow is disabled") || message.includes("workflow was disabled") || message.includes("disabled workflow");
}

function isMissingWorkflowDispatchTriggerError(error) {
const status = error?.status ?? error?.response?.status;
const message = [error?.message, error?.response?.data?.message]
.filter(value => typeof value === "string" && value.trim())
.join(" ")
.toLowerCase();

if (status !== 422 || !message) {
return false;
}

return message.includes("does not have 'workflow_dispatch' trigger");
}

/**
* @param {Record<string, Array<{workflow?: unknown, events?: unknown, ai_reaction?: unknown}>>} slashRouteMap
* @param {string} actualCommand
Expand Down Expand Up @@ -367,6 +386,7 @@ async function main() {
const identifier = eventIdentifier();
const { buildAwContext } = require("./aw_context.cjs");
const ref = await resolveDispatchRef();
const defaultBranch = normalizeDispatchRef(context.payload?.repository?.default_branch || "main");
if (isPRClosedAtStart()) {
core.info("Pull request is closed at workflow start; skipping centralized routing.");
return;
Expand Down Expand Up @@ -404,9 +424,14 @@ async function main() {
...(routeReaction ? { desired_ai_reaction: routeReaction } : {}),
};
core.info(`Dispatching workflow '${workflowID}' for label '${labelName}'.`);
const dispatched = await dispatchWorkflow(workflowID, ref, {
aw_context: JSON.stringify(awContext),
});
const dispatched = await dispatchWorkflow(
workflowID,
ref,
{
aw_context: JSON.stringify(awContext),
},
defaultBranch
);
if (dispatched) {
core.info(`Dispatched '${workflowID}' for label '${labelName}'`);
}
Expand Down Expand Up @@ -452,9 +477,14 @@ async function main() {
...(routeReaction ? { desired_ai_reaction: routeReaction } : {}),
};
core.info(`Dispatching workflow '${workflowID}' for '/${commandName}'.`);
const dispatched = await dispatchWorkflow(workflowID, ref, {
aw_context: JSON.stringify(awContext),
});
const dispatched = await dispatchWorkflow(
workflowID,
ref,
{
aw_context: JSON.stringify(awContext),
},
defaultBranch
);
if (dispatched) {
core.info(`Dispatched '${workflowID}' for '/${commandName}'`);
}
Expand Down
56 changes: 56 additions & 0 deletions actions/setup/js/route_slash_command.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,62 @@ describe("route_slash_command", () => {
expect(globals.core.info).toHaveBeenCalledWith(expect.stringContaining("Completed decentralized label routing for 'smoke'."));
});

it("falls back to default branch for slash commands when workflow_dispatch trigger is missing on PR branch", async () => {
globals.github.rest.actions.createWorkflowDispatch = vi.fn(async params => {
if (params.ref === "refs/heads/copilot/some-feature-branch") {
throw Object.assign(new Error("Workflow does not have 'workflow_dispatch' trigger"), {
status: 422,
response: { status: 422, data: { message: "Workflow does not have 'workflow_dispatch' trigger" } },
});
}
dispatchCalls.push(params);
});
globals.context.payload = {
issue: { number: 42, pull_request: {} },
comment: { id: 99, body: "/archie do the thing" },
};
globals.github.rest.pulls.get = vi.fn(async () => ({
data: { number: 42, head: { ref: "copilot/some-feature-branch" } },
}));
globals.context.payload.repository = { default_branch: "main" };

await main();

expect(dispatchCalls).toHaveLength(1);
expect(dispatchCalls[0].workflow_id).toBe("archie.lock.yml");
expect(dispatchCalls[0].ref).toBe("refs/heads/main");
expect(globals.core.info).toHaveBeenCalledWith(expect.stringContaining("Workflow 'archie.lock.yml' not found on ref 'refs/heads/copilot/some-feature-branch'; retrying on default branch 'refs/heads/main'."));
});

it("falls back to default branch for label routes when workflow_dispatch trigger is missing on PR branch", async () => {
globals.github.rest.actions.createWorkflowDispatch = vi.fn(async params => {
if (params.ref === "refs/heads/copilot/some-feature-branch") {
throw Object.assign(new Error("Workflow does not have 'workflow_dispatch' trigger"), {
status: 422,
response: { status: 422, data: { message: "Workflow does not have 'workflow_dispatch' trigger" } },
});
}
dispatchCalls.push(params);
});
globals.context.eventName = "pull_request";
globals.context.payload = {
action: "labeled",
label: { name: "ci-doctor" },
pull_request: { number: 42, head: { ref: "copilot/some-feature-branch" }, state: "open" },
repository: { default_branch: "main" },
};
process.env.GH_AW_LABEL_ROUTING = JSON.stringify({
"ci-doctor": [{ workflow: "ci-doctor", events: ["pull_request"], ai_reaction: "eyes" }],
});

await main();

expect(dispatchCalls).toHaveLength(1);
expect(dispatchCalls[0].workflow_id).toBe("ci-doctor.lock.yml");
expect(dispatchCalls[0].ref).toBe("refs/heads/main");
expect(globals.core.info).toHaveBeenCalledWith(expect.stringContaining("Workflow 'ci-doctor.lock.yml' not found on ref 'refs/heads/copilot/some-feature-branch'; retrying on default branch 'refs/heads/main'."));
});

it("skips centralized routing when PR is closed at workflow start", async () => {
globals.context.eventName = "pull_request";
globals.context.payload = { action: "ready_for_review", pull_request: { number: 12, state: "closed" } };
Expand Down