Skip to content
Merged
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
42 changes: 29 additions & 13 deletions actions/setup/js/create_pr_review_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { isTemplatableTrue, isStagedMode, logStagedPreviewInfo, checkRequiredFilter } = require("./safe_output_helpers.cjs");
const { resolveAllowedMentionsFromPayload } = require("./resolve_mentions_from_payload.cjs");
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");
const { ERR_VALIDATION } = require("./error_codes.cjs");

/** @type {string} Safe output type handled by this module */
const HANDLER_TYPE = "create_pull_request_review_comment";
Expand All @@ -38,6 +40,20 @@ async function main(config = {}) {
const legacyBuffer = registry ? null : config._prReviewBuffer || null;
const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config);
const githubClient = await createAuthenticatedGitHubClient(config);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] effectivePayload is derived once at startup (line 41) before the per-message loop, but the invocation context is static anyway so this is fine. However, if resolveInvocationContext ever throws (e.g. malformed event_payload JSON), the entire main() will reject before processing any message. Consider wrapping the call in a try/catch and falling back to raw context, so a bad dispatch input degrades gracefully rather than taking down the whole handler.

💡 Defensive pattern
let invocationContext = {};
try {
  invocationContext = resolveInvocationContext(context);
} catch (e) {
  core.warning(`resolveInvocationContext failed, using raw context: ${e.message}`);
}
const effectiveEventName = invocationContext.eventName || context.eventName;
const effectivePayload   = invocationContext.eventPayload || context.payload;

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ec00158 by adding defensive handling around invocation-context resolution: non-validation failures now fall back to raw context, while ERR_VALIDATION failures are still re-thrown.

let invocationContext = {};
try {
invocationContext = resolveInvocationContext(context);
} catch (error) {
const typedError = /** @type {Error & { code?: string }} */ (error);
const isValidationError = typedError.code === ERR_VALIDATION;
if (isValidationError) {
throw error;
}
const errorMessage = getErrorMessage(error);
core.warning(`create_pull_request_review_comment: failed to resolve invocation context, using raw context: ${errorMessage}`);
}
const effectiveEventName = invocationContext.eventName || context.eventName;
const effectivePayload = invocationContext.eventPayload || context.payload;
const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : [];
const requiredTitlePrefix = config.required_title_prefix || "";
if (requiredLabels.length > 0) core.info(`Required labels (all): ${requiredLabels.join(", ")}`);
Expand Down Expand Up @@ -85,9 +101,9 @@ async function main(config = {}) {
}

// Extract triggering context for footer generation
const triggeringIssueNumber = context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined;
const triggeringPRNumber = context.payload?.pull_request?.number || (context.payload?.issue?.pull_request ? context.payload.issue.number : undefined);
const triggeringDiscussionNumber = context.payload?.discussion?.number;
const triggeringIssueNumber = effectivePayload?.issue?.number && !effectivePayload?.issue?.pull_request ? effectivePayload.issue.number : undefined;
const triggeringPRNumber = effectivePayload?.pull_request?.number || (effectivePayload?.issue?.pull_request ? effectivePayload.issue.number : undefined);
const triggeringDiscussionNumber = effectivePayload?.discussion?.number;

const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow";
const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE || "";
Expand Down Expand Up @@ -154,11 +170,11 @@ async function main(config = {}) {

// Check if we're in a pull request context, or an issue comment context on a PR
const isPRContext =
context.eventName === "pull_request" ||
context.eventName === "pull_request_target" ||
context.eventName === "pull_request_review" ||
context.eventName === "pull_request_review_comment" ||
(context.eventName === "issue_comment" && context.payload.issue && context.payload.issue.pull_request);
effectiveEventName === "pull_request" ||
effectiveEventName === "pull_request_target" ||
effectiveEventName === "pull_request_review" ||
effectiveEventName === "pull_request_review_comment" ||
(effectiveEventName === "issue_comment" && effectivePayload.issue && effectivePayload.issue.pull_request);

// Validate context based on target configuration
if (commentTarget === "triggering" && !isPRContext) {
Expand Down Expand Up @@ -229,11 +245,11 @@ async function main(config = {}) {
}
} else {
// Default behavior: use triggering PR
if (context.payload.pull_request) {
pullRequestNumber = context.payload.pull_request.number;
pullRequest = context.payload.pull_request;
} else if (context.payload.issue && context.payload.issue.pull_request) {
pullRequestNumber = context.payload.issue.number;
if (effectivePayload.pull_request) {
pullRequestNumber = effectivePayload.pull_request.number;
pullRequest = effectivePayload.pull_request;
} else if (effectivePayload.issue && effectivePayload.issue.pull_request) {
pullRequestNumber = effectivePayload.issue.number;
} else {
core.warning("Pull request context detected but no pull request found in payload");
return {
Expand Down
93 changes: 93 additions & 0 deletions actions/setup/js/create_pr_review_comment.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,99 @@ describe("create_pr_review_comment.cjs", () => {
expect(buffer.getBufferedCount()).toBe(1);
});

it("should use the original PR context for centralized slash-command dispatches", async () => {
global.context = {
eventName: "workflow_dispatch",
runId: 12345,
repo: { owner: "testowner", repo: "testrepo" },
payload: {
inputs: {
event_name: "issue_comment",
event_payload: JSON.stringify({
issue: { number: 456, pull_request: {} },
repository: mockContext.payload.repository,
}),
},
},
};
mockGithub.rest.pulls.get.mockResolvedValue({
data: { number: 456, head: { sha: "dispatch123abc" } },
});
const handler = await createHandler({ target: "triggering" });
const message = {
type: "create_pull_request_review_comment",
pull_request_number: 456,
path: "src/main.js",
line: 5,
body: "Review comment from centralized slash command",
};

const result = await handler(message, {});

expect(result.success).toBe(true);
expect(result.buffered).toBe(true);
expect(result.pull_request_number).toBe(456);
expect(mockGithub.rest.pulls.get).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
pull_number: 456,
});
expect(buffer.getBufferedCount()).toBe(1);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The new test covers the workflow_dispatch happy path but not the fallback: if resolveInvocationContext returns {}, the code should fall back to raw context.eventName/context.payload. A test for that case would pin the || guard behaviour and prevent a silent regression if invocation_context_helpers changes its return shape.

💡 Suggested additional test skeleton
it("falls back to raw context when invocationContext is empty", async () => {
  // resolveInvocationContext returns no overrides
  jest.mock("./invocation_context_helpers.cjs", () => ({
    resolveInvocationContext: () => ({}),
  }));
  global.context = mockContext; // standard PR trigger
  const handler = await createHandler({ target: "triggering" });
  const result = await handler(prReviewMessage, {});
  expect(result.success).toBe(true);
});

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ec00158 by adding regression coverage for fallback behavior when invocation context resolution yields empty/partial context (raw context fallback path is now explicitly tested).

it("falls back to raw context when resolveInvocationContext returns empty object", async () => {
const invocationHelpersPath = path.join(__dirname, "invocation_context_helpers.cjs");
const invocationHelpers = require(invocationHelpersPath);
const resolveInvocationContextSpy = vi.spyOn(invocationHelpers, "resolveInvocationContext").mockReturnValue({});
try {
const handler = await createHandler({ target: "triggering" });
const message = {
type: "create_pull_request_review_comment",
path: "src/main.js",
line: 5,
body: "Review comment from fallback context",
};

const result = await handler(message, {});

expect(result.success).toBe(true);
expect(result.buffered).toBe(true);
expect(result.pull_request_number).toBe(123);
expect(buffer.getBufferedCount()).toBe(1);
expect(resolveInvocationContextSpy).toHaveBeenCalled();
} finally {
resolveInvocationContextSpy.mockRestore();
}
});

it("falls back to raw context when resolveInvocationContext throws a non-validation error", async () => {
const invocationHelpersPath = path.join(__dirname, "invocation_context_helpers.cjs");
const invocationHelpers = require(invocationHelpersPath);
const resolveInvocationContextSpy = vi.spyOn(invocationHelpers, "resolveInvocationContext").mockImplementation(() => {
throw new Error("boom");
});
try {
const handler = await createHandler({ target: "triggering" });
const message = {
type: "create_pull_request_review_comment",
path: "src/main.js",
line: 5,
body: "Review comment from thrown context resolver",
};

const result = await handler(message, {});

expect(result.success).toBe(true);
expect(result.buffered).toBe(true);
expect(result.pull_request_number).toBe(123);
expect(buffer.getBufferedCount()).toBe(1);
expect(resolveInvocationContextSpy).toHaveBeenCalled();
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("failed to resolve invocation context"));
} finally {
resolveInvocationContextSpy.mockRestore();
}
});

it("should reject comments targeting a different PR than the first comment", async () => {
// First comment sets context to PR #123
const handler = await createHandler();
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/invocation_context_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function checkAllowedRepo(workflowRepo, targetRepo) {

const validation = validateTargetRepo(targetRepoSlug, defaultRepo, allowedRepos);
if (!validation.valid) {
throw new Error(`${ERR_VALIDATION}: ${validation.error}`);
throw Object.assign(new Error(`${ERR_VALIDATION}: ${validation.error}`), { code: ERR_VALIDATION });
}
}

Expand Down
Loading