diff --git a/.changeset/patch-add-history-link-add-comment-issue-pr.md b/.changeset/patch-add-history-link-add-comment-issue-pr.md new file mode 100644 index 00000000000..5cea3b558ad --- /dev/null +++ b/.changeset/patch-add-history-link-add-comment-issue-pr.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Added the clock history link (◷) to the add_comment, update_issue, and update_pull_request footers so generated updates match the other handlers that already include the history link. diff --git a/.changeset/patch-add-history-link-footers.md b/.changeset/patch-add-history-link-footers.md new file mode 100644 index 00000000000..df3bcd39359 --- /dev/null +++ b/.changeset/patch-add-history-link-footers.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Added the clock history link (◷) to the footers generated by the add_comment, update_issue, and update_pull_request handlers so the new helpers match existing issue/PR workflows. diff --git a/actions/setup/js/add_comment.cjs b/actions/setup/js/add_comment.cjs index ebf195949e8..8f24a8bef62 100644 --- a/actions/setup/js/add_comment.cjs +++ b/actions/setup/js/add_comment.cjs @@ -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"; @@ -335,6 +336,7 @@ async function main(config = {}) { // Get workflow ID for hiding older comments const workflowId = process.env.GH_AW_WORKFLOW_ID || ""; + const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || ""; /** * Message handler function @@ -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, + }) || 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); diff --git a/actions/setup/js/generate_history_link.cjs b/actions/setup/js/generate_history_link.cjs index 0e16630d25a..a42a3e729da 100644 --- a/actions/setup/js/generate_history_link.cjs +++ b/actions/setup/js/generate_history_link.cjs @@ -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". @@ -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}"`); - 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(); } diff --git a/actions/setup/js/generate_history_link.test.cjs b/actions/setup/js/generate_history_link.test.cjs index 9b1a5d73044..0614df0575c 100644 --- a/actions/setup/js/generate_history_link.test.cjs +++ b/actions/setup/js/generate_history_link.test.cjs @@ -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", () => { @@ -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", () => { @@ -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="); }); }); @@ -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", () => { @@ -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"); @@ -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"); @@ -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"); @@ -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"); }); @@ -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", () => { @@ -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", () => { diff --git a/actions/setup/js/update_issue.cjs b/actions/setup/js/update_issue.cjs index f79cc6e1705..dc5df34e092 100644 --- a/actions/setup/js/update_issue.cjs +++ b/actions/setup/js/update_issue.cjs @@ -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. @@ -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, + workflowId, + serverUrl: context.serverUrl, + }) || undefined; + // Use helper to update body (handles all operations including replace) apiData.body = updateBody({ currentBody, @@ -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})`); diff --git a/actions/setup/js/update_pr_description_helpers.cjs b/actions/setup/js/update_pr_description_helpers.cjs index 7d1f0df30c3..65807d5e403 100644 --- a/actions/setup/js/update_pr_description_helpers.cjs +++ b/actions/setup/js/update_pr_description_helpers.cjs @@ -17,9 +17,10 @@ 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 @@ -27,7 +28,7 @@ function buildAIFooter(workflowName, runUrl) { 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(); } /** @@ -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 diff --git a/actions/setup/js/update_pull_request.cjs b/actions/setup/js/update_pull_request.cjs index 729f3111967..9400f174490 100644 --- a/actions/setup/js/update_pull_request.cjs +++ b/actions/setup/js/update_pull_request.cjs @@ -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 @@ -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, + workflowId, + serverUrl: context.serverUrl, + }) || undefined; + // Use helper to update body (handles all operations including replace) apiData.body = updateBody({ currentBody, @@ -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})`);