Skip to content
Merged
6 changes: 4 additions & 2 deletions actions/setup/js/add_labels.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const { attachExecutionState, fetchIssueState, normalizeLabelNames } = require("
const { MAX_LABELS } = require("./constants.cjs");
const { createCountGatedHandler } = require("./handler_scaffold.cjs");
const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs");
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");

/**
* Main handler factory for add_labels
Expand Down Expand Up @@ -78,15 +79,16 @@ const main = createCountGatedHandler({
// Accept common aliases: issue_number, pr_number, and pull_number are normalised to item_number
const targetResult = resolveSafeOutputIssueTarget({ message, resolvedTemporaryIds, repoParts, handlerType: HANDLER_TYPE });
if (!targetResult.success) return targetResult;
const itemNumber = targetResult.number ?? context.payload?.issue?.number ?? context.payload?.pull_request?.number;
const effectiveContext = resolveInvocationContext(context);
const itemNumber = targetResult.number ?? effectiveContext.eventPayload?.issue?.number ?? effectiveContext.eventPayload?.pull_request?.number;

if (!itemNumber || Number.isNaN(Number(itemNumber))) {
const error = "No issue/PR number available";
core.warning(error);
return { success: false, error };
}

const contextType = context.payload?.pull_request ? "pull request" : "issue";
const contextType = effectiveContext.eventPayload?.pull_request ? "pull request" : "issue";
const requestedLabels = message.labels ?? [];
core.info(`Requested labels: ${JSON.stringify(requestedLabels)}`);

Expand Down
34 changes: 34 additions & 0 deletions actions/setup/js/add_labels.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,40 @@ describe("add_labels", () => {
expect(result.contextType).toBe("issue");
});

it("should add labels from workflow_dispatch aw_context when issue payload is absent", async () => {
mockContext.eventName = "workflow_dispatch";
mockContext.payload = {
inputs: {
aw_context: JSON.stringify({
event_type: "issue_comment",
item_type: "issue",
item_number: 456,
repo: "test-owner/test-repo",
}),
},
};

const handler = await main({ max: 10 });
const addLabelsCalls = [];

mockGithub.rest.issues.addLabels = async params => {
addLabelsCalls.push(params);
return {};
};

const result = await handler(
{
labels: ["documentation"],
},
{}
);

expect(result.success).toBe(true);
expect(result.number).toBe(456);
expect(result.contextType).toBe("issue");
expect(addLabelsCalls[0].issue_number).toBe(456);
});

it("should add labels to a pull request from context", async () => {
mockContext.payload = {
pull_request: {
Expand Down
6 changes: 4 additions & 2 deletions actions/setup/js/remove_labels.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { logStagedPreviewInfo } = require("./staged_preview.cjs");
const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
const { resolveSafeOutputIssueTarget } = require("./temporary_id.cjs");
const { createCountGatedHandler } = require("./handler_scaffold.cjs");
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");

/**
* Main handler factory for remove_labels
Expand Down Expand Up @@ -69,15 +70,16 @@ const main = createCountGatedHandler({
// Accept common aliases: issue_number, pr_number, and pull_number are normalised to item_number
const targetResult = resolveSafeOutputIssueTarget({ message, resolvedTemporaryIds, repoParts, handlerType: HANDLER_TYPE });
if (!targetResult.success) return targetResult;
const itemNumber = targetResult.number ?? context.payload?.issue?.number ?? context.payload?.pull_request?.number;
const effectiveContext = resolveInvocationContext(context);
const itemNumber = targetResult.number ?? effectiveContext.eventPayload?.issue?.number ?? effectiveContext.eventPayload?.pull_request?.number;

if (!itemNumber || Number.isNaN(Number(itemNumber))) {
const error = "No issue/PR number available";
core.warning(error);
return { success: false, error };
}

const contextType = context.payload?.pull_request ? "pull request" : "issue";
const contextType = effectiveContext.eventPayload?.pull_request ? "pull request" : "issue";
const requestedLabels = message.labels ?? [];
core.info(`Requested labels to remove: ${JSON.stringify(requestedLabels)}`);

Expand Down
34 changes: 34 additions & 0 deletions actions/setup/js/remove_labels.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,40 @@ describe("remove_labels", () => {
expect(result.contextType).toBe("issue");
});

it("should remove labels from workflow_dispatch aw_context when issue payload is absent", async () => {
mockContext.eventName = "workflow_dispatch";
mockContext.payload = {
inputs: {
aw_context: JSON.stringify({
event_type: "issue_comment",
item_type: "issue",
item_number: 456,
repo: "test-owner/test-repo",
}),
},
};

const handler = await main({ max: 10 });
const removeLabelCalls = [];

mockGithub.rest.issues.removeLabel = async params => {
removeLabelCalls.push(params);
return {};
};

const result = await handler(
{
labels: ["documentation"],
},
{}
);

expect(result.success).toBe(true);
expect(result.number).toBe(456);
expect(result.contextType).toBe("issue");
expect(removeLabelCalls[0].issue_number).toBe(456);
});

it("should remove labels from a pull request from context", async () => {
mockContext.payload = {
pull_request: {
Expand Down
29 changes: 21 additions & 8 deletions actions/setup/js/safe_output_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
const { getErrorMessage } = require("./error_helpers.cjs");
const { matchesSimpleGlob } = require("./glob_pattern_helpers.cjs");
const { logStagedPreviewInfo } = require("./staged_preview.cjs");
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");

/**
* Parse a comma-separated list of allowed items from environment variable
Expand Down Expand Up @@ -70,12 +71,24 @@ function parseMaxCount(envValue, defaultValue = 3) {
*/
function resolveTarget(params) {
const { targetConfig, item, context, itemType, supportsPR = false, supportsIssue = false } = params;
let invocationContext;
try {
invocationContext = resolveInvocationContext(context);
} catch (err) {
return {
success: false,
error: `Failed to resolve invocation context for ${itemType}: ${getErrorMessage(err)}`,
shouldFail: true,
};
}
const effectiveEventName = invocationContext?.eventName || context.eventName;
const effectivePayload = invocationContext?.eventPayload || context.payload;

// Check context type
const prEventNames = new Set(["pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment"]);
const isIssueCommentOnPR = context.eventName === "issue_comment" && Boolean(context.payload?.issue?.pull_request);
const isIssueContext = context.eventName === "issues" || (context.eventName === "issue_comment" && !isIssueCommentOnPR);
const isPRContext = prEventNames.has(context.eventName) || isIssueCommentOnPR;
const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request);
const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR);
const isPRContext = prEventNames.has(effectiveEventName) || isIssueCommentOnPR;

// Default target is "triggering"
const target = targetConfig || "triggering";
Expand Down Expand Up @@ -202,8 +215,8 @@ function resolveTarget(params) {
} else {
// Use triggering context
if (isIssueContext) {
if (context.payload.issue) {
itemNumber = context.payload.issue.number;
if (effectivePayload.issue) {
itemNumber = effectivePayload.issue.number;
contextType = "issue";
} else {
return {
Expand All @@ -213,11 +226,11 @@ function resolveTarget(params) {
};
}
} else if (isPRContext) {
if (context.payload.pull_request) {
itemNumber = context.payload.pull_request.number;
if (effectivePayload.pull_request) {
itemNumber = effectivePayload.pull_request.number;
contextType = "pull request";
} else if (isIssueCommentOnPR) {
itemNumber = context.payload.issue.number;
itemNumber = effectivePayload.issue.number;
contextType = "pull request";
} else {
return {
Expand Down
46 changes: 46 additions & 0 deletions actions/setup/js/safe_output_helpers.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@ describe("safe_output_helpers", () => {
expect(result.contextType).toBe("issue");
});

it("should resolve workflow_dispatch issue context from aw_context", () => {
const result = helpers.resolveTarget({
...baseParams,
context: {
eventName: "workflow_dispatch",
repo: { owner: "test-owner", repo: "test-repo" },
payload: {
inputs: {
aw_context: JSON.stringify({
event_type: "issue_comment",
item_type: "issue",
item_number: 321,
repo: "test-owner/test-repo",
}),
},
},
},
});
expect(result.success).toBe(true);
expect(result.number).toBe(321);
expect(result.contextType).toBe("issue");
});

it("should resolve triggering PR context", () => {
const result = helpers.resolveTarget({
...baseParams,
Expand Down Expand Up @@ -458,6 +481,29 @@ describe("safe_output_helpers", () => {
expect(result.contextType).toBe("issue");
});

it("should resolve workflow_dispatch issue context from aw_context", () => {
const result = helpers.resolveTarget({
...baseParams,
context: {
eventName: "workflow_dispatch",
repo: { owner: "test-owner", repo: "test-repo" },
payload: {
inputs: {
aw_context: JSON.stringify({
event_type: "issue_comment",
item_type: "issue",
item_number: 654,
repo: "test-owner/test-repo",
}),
},
},
},
});
expect(result.success).toBe(true);
expect(result.number).toBe(654);
expect(result.contextType).toBe("issue");
});

it("should fail when triggering and not in issue context", () => {
const result = helpers.resolveTarget({
...baseParams,
Expand Down
34 changes: 34 additions & 0 deletions actions/setup/js/update_issue.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,40 @@ describe("update_issue.cjs - cross-repo and operation integration", () => {
expect(capturedRepo).toBe("other-repo");
});

it("should resolve triggering issue from workflow_dispatch aw_context", async () => {
const savedEventName = mockContext.eventName;
const savedPayload = mockContext.payload;
mockContext.eventName = "workflow_dispatch";
mockContext.payload = {
inputs: {
aw_context: JSON.stringify({
event_type: "issue_comment",
item_type: "issue",
item_number: 123,
repo: "testowner/testrepo",
}),
},
};
let capturedIssueNumber;

mockGithub.rest.issues.get.mockResolvedValue({
data: { number: 123, title: "Test", body: "Original", html_url: "https://github.com/testowner/testrepo/issues/123" },
});
mockGithub.rest.issues.update.mockImplementation(async ({ issue_number, body }) => {
capturedIssueNumber = issue_number;
return { data: { number: issue_number, title: "Test", body, html_url: `https://github.com/testowner/testrepo/issues/${issue_number}` } };
});

const { main } = await import("./update_issue.cjs");
const handler = await main();
const result = await handler({ body: "Updated from centralized dispatch" }, {});

expect(result.success).toBe(true);
expect(capturedIssueNumber).toBe(123);
mockContext.eventName = savedEventName;
mockContext.payload = savedPayload;
});

it("should use current workflow repo in attribution URL for cross-repo updates", async () => {
let capturedBody;

Expand Down
Loading