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
63 changes: 38 additions & 25 deletions actions/setup/js/add_reaction_and_edit_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { sanitizeContent } = require("./sanitize_content.cjs");
const { ERR_API, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs");
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");
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.

Good refactor — importing resolveInvocationContext here centralizes context resolution. This makes the code more testable and consistent with how other setup scripts handle cross-workflow invocations.


/**
* Event type descriptions for comment messages
Expand Down Expand Up @@ -38,7 +39,8 @@ const DISCUSSION_REACTION_MAP = {
async function main() {
const reaction = process.env.GH_AW_REACTION || "eyes";
const command = process.env.GH_AW_COMMAND; // Only present for command workflows
const runUrl = buildWorkflowRunUrl(context, context.repo);
const invocationContext = resolveInvocationContext(context);
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.

Using invocationContext.workflowRepo instead of context.repo is a nice fix — ensures the run URL points to the workflow's repo rather than the event repo when running in cross-repo scenarios. 👍

const runUrl = buildWorkflowRunUrl(context, invocationContext.workflowRepo);

core.info(`Reaction type: ${reaction}`);
core.info(`Command name: ${command || "none"}`);
Expand All @@ -54,14 +56,15 @@ async function main() {

let reactionEndpoint;
let commentUpdateEndpoint;
const eventName = context.eventName;
const owner = context.repo.owner;
const repo = context.repo.repo;
const eventName = invocationContext.eventName;
const owner = invocationContext.eventRepo.owner;
const repo = invocationContext.eventRepo.repo;
const payload = invocationContext.eventPayload;

try {
switch (eventName) {
case "issues": {
const issueNumber = context.payload?.issue?.number;
const issueNumber = payload?.issue?.number;
if (!issueNumber) {
core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`);
return;
Expand All @@ -72,8 +75,8 @@ async function main() {
}

case "issue_comment": {
const commentId = context.payload?.comment?.id;
const issueNumberForComment = context.payload?.issue?.number;
const commentId = payload?.comment?.id;
const issueNumberForComment = payload?.issue?.number;
if (!commentId) {
core.setFailed(`${ERR_VALIDATION}: Comment ID not found in event payload`);
return;
Expand All @@ -89,7 +92,7 @@ async function main() {
}

case "pull_request": {
const prNumber = context.payload?.pull_request?.number;
const prNumber = payload?.pull_request?.number;
if (!prNumber) {
core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`);
return;
Expand All @@ -101,8 +104,8 @@ async function main() {
}

case "pull_request_review_comment": {
const reviewCommentId = context.payload?.comment?.id;
const prNumberForReviewComment = context.payload?.pull_request?.number;
const reviewCommentId = payload?.comment?.id;
const prNumberForReviewComment = payload?.pull_request?.number;
if (!reviewCommentId) {
core.setFailed(`${ERR_VALIDATION}: Review comment ID not found in event payload`);
return;
Expand All @@ -118,7 +121,7 @@ async function main() {
}

case "discussion": {
const discussionNumber = context.payload?.discussion?.number;
const discussionNumber = payload?.discussion?.number;
if (!discussionNumber) {
core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`);
return;
Expand All @@ -131,13 +134,13 @@ async function main() {
}

case "discussion_comment": {
const discussionCommentNumber = context.payload?.discussion?.number;
const discussionCommentId = context.payload?.comment?.id;
const discussionCommentNumber = payload?.discussion?.number;
const discussionCommentId = payload?.comment?.id;
if (!discussionCommentNumber || !discussionCommentId) {
core.setFailed(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`);
return;
}
const commentNodeId = context.payload?.comment?.node_id;
const commentNodeId = payload?.comment?.node_id;
if (!commentNodeId) {
core.setFailed(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`);
return;
Expand All @@ -163,7 +166,7 @@ async function main() {

if (commentUpdateEndpoint) {
core.info(`Comment endpoint: ${commentUpdateEndpoint}`);
await addCommentWithWorkflowLink(commentUpdateEndpoint, runUrl, eventName);
await addCommentWithWorkflowLink(commentUpdateEndpoint, runUrl, eventName, invocationContext);
}
} catch (error) {
const errorMessage = getErrorMessage(error);
Expand Down Expand Up @@ -270,24 +273,34 @@ async function getDiscussionId(owner, repo, discussionNumber) {
* Helper function to set comment outputs
* @param {string} commentId - The comment ID
* @param {string} commentUrl - The comment URL
* @param {{ owner: string, repo: string }} [eventRepo=context.repo] - Repository where the comment was created
*/
function setCommentOutputs(commentId, commentUrl) {
function setCommentOutputs(commentId, commentUrl, eventRepo = context.repo) {
core.info(`Successfully created comment with workflow link`);
core.info(`Comment ID: ${commentId}`);
core.info(`Comment URL: ${commentUrl}`);
core.info(`Comment Repo: ${context.repo.owner}/${context.repo.repo}`);
core.info(`Comment Repo: ${eventRepo.owner}/${eventRepo.repo}`);
core.setOutput("comment-id", commentId);
core.setOutput("comment-url", commentUrl);
core.setOutput("comment-repo", `${context.repo.owner}/${context.repo.repo}`);
core.setOutput("comment-repo", `${eventRepo.owner}/${eventRepo.repo}`);
}

/**
* Add a comment with a workflow run link
* @param {string} endpoint - The GitHub API endpoint to create the comment (or special format for discussions)
* @param {string} runUrl - The URL of the workflow run
* @param {string} eventName - The event type (to determine the comment text)
* @param {{
* source?: "native" | "workflow_dispatch" | "repository_dispatch",
* eventName?: string,
* eventPayload?: any,
* workflowRepo?: { owner: string, repo: string },
* eventRepo?: { owner: string, repo: string }
* } | null} [invocationContext=null] - Resolved invocation event context. When omitted, falls back to global context payload/repo.
*/
async function addCommentWithWorkflowLink(endpoint, runUrl, eventName) {
async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocationContext = null) {
const eventPayload = invocationContext?.eventPayload || context.payload;
const eventRepo = invocationContext?.eventRepo || context.repo;
try {
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow";
const eventTypeDescription = EVENT_TYPE_DESCRIPTIONS[eventName] ?? "event";
Expand Down Expand Up @@ -316,7 +329,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName) {
if (eventName === "discussion") {
// Parse discussion number from special format: "discussion:NUMBER"
const discussionNumber = parseInt(endpoint.split(":")[1], 10);
const { id: discussionId } = await getDiscussionId(context.repo.owner, context.repo.repo, discussionNumber);
const { id: discussionId } = await getDiscussionId(eventRepo.owner, eventRepo.repo, discussionNumber);

const result = await github.graphql(
`
Expand All @@ -332,17 +345,17 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName) {
);

const comment = result.addDiscussionComment.comment;
setCommentOutputs(comment.id, comment.url);
setCommentOutputs(comment.id, comment.url, eventRepo);
return;
} else if (eventName === "discussion_comment") {
// Parse discussion number from special format: "discussion_comment:NUMBER:COMMENT_ID"
const discussionNumber = parseInt(endpoint.split(":")[1], 10);
const { id: discussionId } = await getDiscussionId(context.repo.owner, context.repo.repo, discussionNumber);
const { id: discussionId } = await getDiscussionId(eventRepo.owner, eventRepo.repo, discussionNumber);

// Get the comment node ID to use as the parent for threading.
// GitHub Discussions only supports two nesting levels, so if the triggering comment is
// itself a reply, we resolve the top-level parent's node ID.
const commentNodeId = await resolveTopLevelDiscussionCommentId(github, context.payload?.comment?.node_id);
const commentNodeId = await resolveTopLevelDiscussionCommentId(github, eventPayload?.comment?.node_id);

const result = await github.graphql(
`
Expand All @@ -358,7 +371,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName) {
);

const comment = result.addDiscussionComment.comment;
setCommentOutputs(comment.id, comment.url);
setCommentOutputs(comment.id, comment.url, eventRepo);
return;
}

Expand All @@ -370,7 +383,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName) {
},
});

setCommentOutputs(createResponse.data.id.toString(), createResponse.data.html_url);
setCommentOutputs(createResponse.data.id.toString(), createResponse.data.html_url, eventRepo);
} catch (error) {
// Don't fail the entire job if comment creation fails - just log it
const errorMessage = getErrorMessage(error);
Expand Down
27 changes: 27 additions & 0 deletions actions/setup/js/add_reaction_and_edit_comment.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,33 @@ describe("add_reaction_and_edit_comment.cjs", () => {
});
});

describe("repository_dispatch reactions", () => {
it("should use workflow repo for run URL and event repo for reaction/comment APIs", async () => {
process.env.GH_AW_REACTION = "eyes";
global.context = {
eventName: "repository_dispatch",
runId: 12345,
repo: { owner: "sideowner", repo: "siderepo" },
payload: {
action: "issue_comment",
client_payload: {
issue: { number: 123 },
comment: { id: 456 },
repository: { owner: { login: "targetowner" }, name: "targetrepo" },
},
},
};
mockGithub.request.mockResolvedValueOnce({ data: { id: 111 } }).mockResolvedValueOnce({ data: { id: 789, html_url: "https://github.com/targetowner/targetrepo/issues/123#issuecomment-789" } });

const { main } = await loadModule();
await main();

expect(mockGithub.request).toHaveBeenCalledWith("POST /repos/targetowner/targetrepo/issues/comments/456/reactions", expect.objectContaining({ content: "eyes" }));
expect(mockGithub.request).toHaveBeenCalledWith("POST /repos/targetowner/targetrepo/issues/123/comments", expect.objectContaining({ body: expect.stringContaining("https://github.com/sideowner/siderepo/actions/runs/12345") }));
expect(mockCore.setOutput).toHaveBeenCalledWith("comment-repo", "targetowner/targetrepo");
});
});

describe("Pull request review comment reactions", () => {
it("should create new comment for pull_request_review_comment event (not edit)", async () => {
process.env.GH_AW_REACTION = "rocket";
Expand Down
Loading
Loading