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
5 changes: 5 additions & 0 deletions .changeset/patch-add-history-link-add-comment-issue-pr.md

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

5 changes: 5 additions & 0 deletions .changeset/patch-add-history-link-footers.md

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

17 changes: 16 additions & 1 deletion actions/setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const { logStagedPreviewInfo } = require("./staged_preview.cjs");
const { ERR_NOT_FOUND } = require("./error_codes.cjs");
const { isPayloadUserBot } = require("./resolve_mentions.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { generateHistoryUrl } = require("./generate_history_link.cjs");

/** @type {string} Safe output type handled by this module */
const HANDLER_TYPE = "add_comment";
Expand Down Expand Up @@ -335,6 +336,7 @@ async function main(config = {}) {

// Get workflow ID for hiding older comments
const workflowId = process.env.GH_AW_WORKFLOW_ID || "";
Copy link
Contributor

Choose a reason for hiding this comment

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

The callerWorkflowId variable is correctly scoped here alongside workflowId. One suggestion: consider adding a brief JSDoc comment explaining the distinction between workflowId (the stable identifier) and callerWorkflowId (the caller-qualified ID used for history URL generation).

const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || "";

/**
* Message handler function
Expand Down Expand Up @@ -525,9 +527,22 @@ async function main(config = {}) {
const triggeringPRNumber = context.payload.pull_request?.number;
const triggeringDiscussionNumber = context.payload.discussion?.number;

// Generate history URL: use in:comments for issue/PR comments; skip for discussion comments
// (GitHub search does not support in:comments for discussions)
const historyUrl = !isDiscussion
? generateHistoryUrl({
owner: repoParts.owner,
repo: repoParts.repo,
itemType: "comment",
workflowCallId: callerWorkflowId,
workflowId,
serverUrl: context.serverUrl,
Copy link
Contributor

Choose a reason for hiding this comment

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

The historyUrl is correctly skipped for discussions since GitHub search doesn't support in:comments for discussions. The comment explains this well.

}) || undefined
: undefined;

if (includeFooter) {
// When footer is enabled, add full footer with attribution and XML markers
processedBody += generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd();
processedBody += generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber, historyUrl).trimEnd();
} else {
// When footer is disabled, only add XML marker for searchability (no visible attribution text)
processedBody += "\n\n" + generateXMLMarker(workflowName, runUrl);
Expand Down
24 changes: 6 additions & 18 deletions actions/setup/js/generate_history_link.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
*/

/**
* @typedef {"issue" | "pull_request" | "discussion"} ItemType
* @typedef {"issue" | "pull_request" | "discussion" | "comment"} ItemType
*/

/**
* @typedef {Object} HistoryLinkParams
* @property {string} owner - Repository owner
* @property {string} repo - Repository name
* @property {ItemType} itemType - Type of GitHub item: "issue", "pull_request", or "discussion"
* @property {ItemType} itemType - Type of GitHub item: "issue", "pull_request", "discussion", or "comment"
* @property {string} [workflowCallId] - Caller workflow ID (e.g. "owner/repo/WorkflowName"). Takes precedence over workflowId.
* @property {string} [workflowId] - Workflow identifier. Used when workflowCallId is not available.
* @property {string} [serverUrl] - GitHub server URL for enterprise deployments (e.g. "https://github.example.com"). Defaults to "https://github.com".
Expand Down Expand Up @@ -51,32 +51,20 @@ function generateHistoryUrl({ owner, repo, itemType, workflowCallId, workflowId,
// Build the search query parts
const queryParts = [`repo:${owner}/${repo}`];

// Add item type qualifier (issues and PRs use is: qualifiers; discussions use type= param only)
// Add item type qualifier (issues and PRs use is: qualifiers; discussions and comments do not)
if (itemType === "issue") {
queryParts.push("is:issue");
} else if (itemType === "pull_request") {
queryParts.push("is:pr");
}

// Search for the XML marker in the body
// Search for the XML marker in the appropriate field
// Comments use in:comments (searches comment bodies); all others use in:body
queryParts.push(`"${markerId}"`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Good addition of the "comment" item type. The new in:comments search qualifier correctly targets comment bodies rather than item bodies for searchability.

queryParts.push("in:body");

// Determine the search result type parameter
let typeParam;
if (itemType === "issue") {
typeParam = "issues";
} else if (itemType === "pull_request") {
typeParam = "pullrequests";
} else if (itemType === "discussion") {
typeParam = "discussions";
}
queryParts.push(itemType === "comment" ? "in:comments" : "in:body");

const url = new URL(`${server}/search`);
url.searchParams.set("q", queryParts.join(" "));
if (typeParam) {
url.searchParams.set("type", typeParam);
}

return url.toString();
}
Expand Down
20 changes: 10 additions & 10 deletions actions/setup/js/generate_history_link.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("generate_history_link.cjs", () => {
});

expect(url).toContain("is%3Aissue");
expect(url).toContain("type=issues");
expect(url).not.toContain("type=");
});

it("should include is:pr qualifier for pull_request type", () => {
Expand All @@ -58,7 +58,7 @@ describe("generate_history_link.cjs", () => {
});

expect(url).toContain("is%3Apr");
expect(url).toContain("type=pullrequests");
expect(url).not.toContain("type=");
});

it("should NOT include is: qualifier for discussion type", () => {
Expand All @@ -71,7 +71,7 @@ describe("generate_history_link.cjs", () => {
});

expect(url).not.toContain("is%3A");
expect(url).toContain("type=discussions");
expect(url).not.toContain("type=");
});
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

A new ItemType of "comment" was added to generate_history_link.cjs, but the test suite doesn’t appear to cover this new mode (no assertions for itemType: "comment", in:comments, or the absence of is: qualifiers). Adding a focused test would help prevent regressions in how comment history URLs are constructed.

Suggested change
});
});
it("should filter by comments without is: qualifiers for comment type", () => {
const url = generateHistoryUrl({
owner: "testowner",
repo: "testrepo",
itemType: "comment",
workflowId: "my-workflow",
serverUrl: "https://github.com",
});
expect(url).toContain("in%3Acomments");
expect(url).not.toContain("is%3A");
expect(url).not.toContain("type=");
});

Copilot uses AI. Check for mistakes.
});

Expand Down Expand Up @@ -295,7 +295,7 @@ describe("generate_history_link.cjs", () => {
serverUrl: "https://github.com",
});

expect(url).toContain("type=");
expect(url).not.toContain("type=");
});

it("should generate a complete issue search URL", () => {
Expand All @@ -310,7 +310,7 @@ describe("generate_history_link.cjs", () => {
const parsed = new URL(url);
expect(parsed.hostname).toBe("github.com");
expect(parsed.pathname).toBe("/search");
expect(parsed.searchParams.get("type")).toBe("issues");
expect(parsed.searchParams.get("type")).toBeNull();

const query = parsed.searchParams.get("q");
expect(query).toContain("repo:myowner/myrepo");
Expand All @@ -329,7 +329,7 @@ describe("generate_history_link.cjs", () => {
});

const parsed = new URL(url);
expect(parsed.searchParams.get("type")).toBe("pullrequests");
expect(parsed.searchParams.get("type")).toBeNull();

const query = parsed.searchParams.get("q");
expect(query).toContain("is:pr");
Expand All @@ -346,7 +346,7 @@ describe("generate_history_link.cjs", () => {
});

const parsed = new URL(url);
expect(parsed.searchParams.get("type")).toBe("discussions");
expect(parsed.searchParams.get("type")).toBeNull();

const query = parsed.searchParams.get("q");
expect(query).not.toContain("is:issue");
Expand Down Expand Up @@ -430,7 +430,7 @@ describe("generate_history_link.cjs", () => {
serverUrl: "https://github.com",
});

expect(link).toContain("type=issues");
expect(link).not.toContain("type=");
expect(link).toContain("is%3Aissue");
});

Expand All @@ -443,7 +443,7 @@ describe("generate_history_link.cjs", () => {
serverUrl: "https://github.com",
});

expect(link).toContain("type=pullrequests");
expect(link).not.toContain("type=");
});

it("should generate link with correct search URL for discussion", () => {
Expand All @@ -455,7 +455,7 @@ describe("generate_history_link.cjs", () => {
serverUrl: "https://github.com",
});

expect(link).toContain("type=discussions");
expect(link).not.toContain("type=");
});

it("should support enterprise URLs in the history link", () => {
Expand Down
13 changes: 13 additions & 0 deletions actions/setup/js/update_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs");
const { ERR_VALIDATION } = require("./error_codes.cjs");
const { parseBoolTemplatable } = require("./templatable.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { generateHistoryUrl } = require("./generate_history_link.cjs");

/**
* Maximum limits for issue update parameters to prevent resource exhaustion.
Expand Down Expand Up @@ -80,9 +81,20 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) {
// context may be effectiveContext with repo overridden to a cross-repo target.
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "GitHub Agentic Workflow";
const workflowId = process.env.GH_AW_WORKFLOW_ID || "";
const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || "";
const workflowRepo = _workflowRepo || context.repo;
const runUrl = buildWorkflowRunUrl(context, workflowRepo);

const historyUrl =
generateHistoryUrl({
owner: context.repo.owner,
repo: context.repo.repo,
itemType: "issue",
workflowCallId: callerWorkflowId,
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

historyUrl is generated using workflowCallId (lines 88-96), but updateBody() doesn’t add the corresponding <!-- gh-aw-workflow-call-id: ... --> marker (and may not add <!-- gh-aw-workflow-id: ... --> either when includeFooter is true). This can produce a ◷ link that doesn’t include the updated issue in search results. Consider generating history URLs against markers that are guaranteed to be present in updated bodies, or ensure the update path appends/preserves the workflow-id/call-id markers that the search relies on.

Suggested change
workflowCallId: callerWorkflowId,

Copilot uses AI. Check for mistakes.
workflowId,
serverUrl: context.serverUrl,
}) || undefined;

// Use helper to update body (handles all operations including replace)
apiData.body = updateBody({
currentBody,
Expand All @@ -92,6 +104,7 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) {
runUrl,
workflowId,
includeFooter, // Pass footer flag to helper
historyUrl,
});

core.info(`Will update body (length: ${apiData.body.length})`);
Expand Down
10 changes: 6 additions & 4 deletions actions/setup/js/update_pr_description_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ const { sanitizeContent } = require("./sanitize_content.cjs");
* missing info sections, blocked domains, and XML metadata marker).
* @param {string} workflowName - Name of the workflow
* @param {string} runUrl - URL of the workflow run
* @param {string} [historyUrl] - GitHub search URL for items created by this workflow
* @returns {string} AI attribution footer
*/
function buildAIFooter(workflowName, runUrl) {
function buildAIFooter(workflowName, runUrl, historyUrl) {
const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE ?? "";
const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL ?? "";
// Use typeof guard since context is a global injected by the Actions Script runtime
const ctx = typeof context !== "undefined" ? context : null;
const triggeringIssueNumber = ctx?.payload?.issue?.number;
const triggeringPRNumber = ctx?.payload?.pull_request?.number;
const triggeringDiscussionNumber = ctx?.payload?.discussion?.number;
return generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd();
return generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber, historyUrl || undefined).trimEnd();
}

/**
Expand Down Expand Up @@ -82,13 +83,14 @@ function findIsland(body, workflowId) {
* @param {string} params.runUrl - URL of the workflow run
* @param {string} params.workflowId - Workflow ID (stable identifier across runs)
* @param {boolean} [params.includeFooter=true] - Whether to include AI-generated footer (default: true)
* @param {string} [params.historyUrl] - GitHub search URL for items created by this workflow
* @returns {string} Updated body content
*/
function updateBody(params) {
const { currentBody, newContent, operation, workflowName, runUrl, workflowId, includeFooter = true } = params;
const { currentBody, newContent, operation, workflowName, runUrl, workflowId, includeFooter = true, historyUrl } = params;
// When footer is enabled use the full footer (includes install instructions, XML marker, etc.)
// When footer is disabled still add standalone workflow-id marker for searchability
const aiFooter = includeFooter ? buildAIFooter(workflowName, runUrl) : "";
const aiFooter = includeFooter ? buildAIFooter(workflowName, runUrl, historyUrl) : "";
const workflowIdMarker = !includeFooter && workflowId ? `\n\n${generateWorkflowIdMarker(workflowId)}` : "";

// Sanitize new content to prevent injection attacks
Expand Down
13 changes: 13 additions & 0 deletions actions/setup/js/update_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const { createUpdateHandlerFactory, createStandardResolveNumber, createStandardF
const { sanitizeTitle } = require("./sanitize_title.cjs");
const { parseBoolTemplatable } = require("./templatable.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { generateHistoryUrl } = require("./generate_history_link.cjs");

/**
* Execute the pull request update API call
Expand Down Expand Up @@ -47,9 +48,20 @@ async function executePRUpdate(github, context, prNumber, updateData) {
// context may be effectiveContext with repo overridden to a cross-repo target.
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "GitHub Agentic Workflow";
const workflowId = process.env.GH_AW_WORKFLOW_ID || "";
const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || "";
const workflowRepo = _workflowRepo || context.repo;
const runUrl = buildWorkflowRunUrl(context, workflowRepo);

const historyUrl =
generateHistoryUrl({
owner: context.repo.owner,
repo: context.repo.repo,
itemType: "pull_request",
workflowCallId: callerWorkflowId,
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

update_pull_request defaults to operation = "replace" (line 29), and updateBody() does not append standalone gh-aw-workflow-id/gh-aw-workflow-call-id markers when includeFooter is true. Combined with generating historyUrl using workflowCallId (lines 55-63), the ◷ link can point to a search that won’t include the updated PR (because the marker being searched for may not exist in the new body). Either ensure the update path preserves/appends the markers that generateHistoryUrl searches for, or avoid using workflowCallId here unless you also emit its marker.

Suggested change
workflowCallId: callerWorkflowId,

Copilot uses AI. Check for mistakes.
workflowId,
serverUrl: context.serverUrl,
}) || undefined;

// Use helper to update body (handles all operations including replace)
apiData.body = updateBody({
currentBody,
Expand All @@ -59,6 +71,7 @@ async function executePRUpdate(github, context, prNumber, updateData) {
runUrl,
workflowId,
includeFooter, // Pass footer flag to helper
historyUrl,
});

core.info(`Will update body (length: ${apiData.body.length})`);
Expand Down
Loading