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
124 changes: 120 additions & 4 deletions .github/workflows/smoke-copilot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .github/workflows/smoke-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ safe-outputs:
expires: 2h
group: true
close-older-issues: true
create-pull-request-review-comment:
max: 5
submit-pull-request-review:
add-labels:
allowed: [smoke-copilot]
allowed-repos: ["github/gh-aw"]
Expand Down Expand Up @@ -121,6 +124,7 @@ strict: true
- Use the `add_comment` tool with `discussion_number: <extracted_number>` to add a fun, playful comment stating that the smoke test agent was here
8. **Build gh-aw**: Run `GOCACHE=/tmp/go-cache GOMODCACHE=/tmp/go-mod make build` to verify the agent can successfully build the gh-aw project (both caches must be set to /tmp because the default cache locations are not writable). If the command fails, mark this test as ❌ and report the failure.
9. **Workflow Dispatch Testing**: Use the `dispatch_workflow` safe output tool to trigger the `haiku-printer` workflow with a haiku as the message input. Create an original, creative haiku about software testing or automation.
10. **PR Review Testing**: Review the diff of the current pull request. Leave 1-2 inline `create_pull_request_review_comment` comments on specific lines, then call `submit_pull_request_review` with a brief body summarizing your review and event `COMMENT`.

## Output

Expand Down
126 changes: 67 additions & 59 deletions actions/setup/js/create_pr_review_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
*/

const { generateFooter } = require("./generate_footer.cjs");
const { getRepositoryUrl } = require("./get_repository_url.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs");

Expand All @@ -15,16 +13,27 @@ const HANDLER_TYPE = "create_pull_request_review_comment";

/**
* Main handler factory for create_pull_request_review_comment
* Returns a message handler function that processes individual review comment messages
* Returns a message handler function that validates and buffers individual review comments.
* Comments are buffered in the PR review buffer (passed via config._prReviewBuffer) and
* submitted as a single PR review after all messages have been processed.
*
* @type {HandlerFactoryFunction}
*/
async function main(config = {}) {
// Extract configuration
const defaultSide = config.side || "RIGHT";
const commentTarget = config.target || "triggering";
const maxCount = config.max || 10;
const buffer = config._prReviewBuffer;
const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config);

if (!buffer) {
core.warning("create_pull_request_review_comment: No PR review buffer provided in config");
return async function handleCreatePRReviewComment() {
return { success: false, error: "No PR review buffer available" };
};
}

core.info(`PR review comment target configuration: ${commentTarget}`);
core.info(`Default comment side configuration: ${defaultSide}`);
core.info(`Max count: ${maxCount}`);
Expand All @@ -36,16 +45,31 @@ async function main(config = {}) {
// Track how many items we've processed for max limit
let processedCount = 0;

// Track created comments for outputs
const createdComments = [];

// 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;

// Set footer context once for the review buffer
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow";
const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE || "";
const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL || "";
const runId = context.runId;
const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com";
const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;

buffer.setFooterContext({
workflowName,
runUrl,
workflowSource,
workflowSourceURL,
triggeringIssueNumber,
triggeringPRNumber,
triggeringDiscussionNumber,
});

/**
* Message handler function that processes a single create_pull_request_review_comment message
* Message handler function that validates and buffers a single create_pull_request_review_comment message
* @param {Object} message - The create_pull_request_review_comment message to process
* @param {Object} resolvedTemporaryIds - Map of temporary IDs to {repo, number}
* @returns {Promise<Object>} Result with success/error status and comment details
Expand Down Expand Up @@ -203,8 +227,6 @@ async function main(config = {}) {
};
}

core.info(`Creating review comment on PR #${pullRequestNumber}`);

// Parse line numbers
const line = parseInt(commentItem.line, 10);
if (isNaN(line) || line <= 0) {
Expand Down Expand Up @@ -237,61 +259,47 @@ async function main(config = {}) {
};
}

// Extract body from the JSON item
let body = commentItem.body.trim();

// Add AI disclaimer with workflow name and run url
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow";
const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE || "";
const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL || "";
const runId = context.runId;
const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com";
const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
body += generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);

core.info(`Creating review comment on PR #${pullRequestNumber} in ${itemRepo} at ${commentItem.path}:${line}${startLine ? ` (lines ${startLine}-${line})` : ""} [${side}]`);
core.info(`Comment content length: ${body.length}`);

try {
// Prepare the request parameters
/** @type {any} */
const requestParams = {
owner: repoParts.owner,
repo: repoParts.repo,
pull_number: pullRequestNumber,
body: body,
path: commentItem.path,
commit_id: pullRequest && pullRequest.head ? pullRequest.head.sha : "", // Required for creating review comments
line: line,
side: side,
// Set the review context (first comment sets it)
// Reject comments targeting a different repo/PR than the first comment
const existingCtx = buffer.getReviewContext();
if (existingCtx && (existingCtx.repo !== itemRepo || existingCtx.pullRequestNumber !== pullRequestNumber)) {
core.warning(`Skipping review comment: targets ${itemRepo}#${pullRequestNumber} but buffer is bound to ${existingCtx.repo}#${existingCtx.pullRequestNumber}. ` + "All review comments in a single review must target the same PR.");
return {
success: false,
error: `Review comments must target the same PR (buffer is bound to ${existingCtx.repo}#${existingCtx.pullRequestNumber})`,
};
}

// Add start_line for multi-line comments
if (startLine !== undefined) {
requestParams.start_line = startLine;
requestParams.start_side = side; // start_side should match side for consistency
}
buffer.setReviewContext({
repo: itemRepo,
repoParts: repoParts,
pullRequestNumber: pullRequestNumber,
pullRequest: pullRequest,
});

// Buffer the comment instead of posting it individually
/** @type {import('./pr_review_buffer.cjs').BufferedComment} */
const bufferedComment = {
path: commentItem.path,
line: line,
body: commentItem.body.trim(),
side: side,
};

if (startLine !== undefined) {
bufferedComment.start_line = startLine;
}

// Create the review comment using GitHub API
const { data: comment } = await github.rest.pulls.createReviewComment(requestParams);
buffer.addComment(bufferedComment);

core.info("Created review comment #" + comment.id + ": " + comment.html_url);
createdComments.push(comment);
core.info(`Buffered review comment on PR #${pullRequestNumber} in ${itemRepo} at ${commentItem.path}:${line}${startLine ? ` (lines ${startLine}-${line})` : ""} [${side}]`);

return {
success: true,
comment_id: comment.id,
comment_url: comment.html_url,
pull_request_number: pullRequestNumber,
repo: itemRepo,
};
} catch (error) {
core.error(`✗ Failed to create review comment: ${getErrorMessage(error)}`);
return {
success: false,
error: getErrorMessage(error),
};
}
return {
success: true,
buffered: true,
pull_request_number: pullRequestNumber,
repo: itemRepo,
};
};
}

Expand Down
Loading
Loading