From 3ef165f867a950506561cba7d0d7912427016474 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:18:49 +0000
Subject: [PATCH 1/9] Initial plan
From 2c9bc8fa26776415c355b9f806f08945f7b5b7e2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:27:01 +0000
Subject: [PATCH 2/9] Preserve review supersession provenance
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 12 ++++++++++++
actions/setup/js/pr_review_buffer.cjs | 7 ++++---
actions/setup/js/pr_review_buffer.test.cjs | 14 ++++++++++----
3 files changed, 26 insertions(+), 7 deletions(-)
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index 7c1413d7c64..2804cf63b86 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -172,6 +172,17 @@ function generateWorkflowCallIdMarker(callerWorkflowId) {
return ``;
}
+/**
+ * Generates a non-rendered Markdown reference definition carrying a workflow-call ID.
+ * Unlike HTML comments, GitHub preserves this form in pull request review bodies.
+ *
+ * @param {string} callerWorkflowId - Calling workflow identifier
+ * @returns {string} Markdown reference definition for review provenance
+ */
+function generateWorkflowCallIdReviewMarker(callerWorkflowId) {
+ return `[gh-aw-workflow-call-id]: # "${encodeURIComponent(callerWorkflowId)}"`;
+}
+
/**
* Normalizes a user-supplied close-older-key to identifier style.
* Converts to lowercase, replaces runs of non-alphanumeric/dash/underscore characters
@@ -294,6 +305,7 @@ module.exports = {
generateXMLMarker,
generateWorkflowIdMarker,
generateWorkflowCallIdMarker,
+ generateWorkflowCallIdReviewMarker,
getWorkflowIdMarkerContent,
matchesWorkflowId,
isValidWorkflowId,
diff --git a/actions/setup/js/pr_review_buffer.cjs b/actions/setup/js/pr_review_buffer.cjs
index 3dfb6975fdb..3883c14499e 100644
--- a/actions/setup/js/pr_review_buffer.cjs
+++ b/actions/setup/js/pr_review_buffer.cjs
@@ -22,7 +22,7 @@
const { generateFooterWithMessages, getBodyFooterMessage, getDetectionCautionAlert } = require("./messages_footer.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { isStagedMode } = require("./safe_output_helpers.cjs");
-const { generateWorkflowCallIdMarker, matchesWorkflowId } = require("./generate_footer.cjs");
+const { generateWorkflowCallIdMarker, generateWorkflowCallIdReviewMarker, matchesWorkflowId } = require("./generate_footer.cjs");
const { attachExecutionState, fetchPullRequestReviewState } = require("./safe_output_execution_metadata.cjs");
const { withRetry, RATE_LIMIT_RETRY_CONFIG, isTransientError, sleep } = require("./error_recovery.cjs");
const { ERR_API } = require("./error_codes.cjs");
@@ -368,7 +368,7 @@ function createReviewBuffer() {
const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || "";
if (callerWorkflowId) {
- body += "\n" + generateWorkflowCallIdMarker(callerWorkflowId);
+ body += "\n" + generateWorkflowCallIdMarker(callerWorkflowId) + "\n" + generateWorkflowCallIdReviewMarker(callerWorkflowId);
}
}
if (footerContext) {
@@ -532,6 +532,7 @@ function createReviewBuffer() {
return;
}
const workflowCallMarker = workflowCallId ? generateWorkflowCallIdMarker(workflowCallId) : "";
+ const workflowCallReviewMarker = workflowCallId ? generateWorkflowCallIdReviewMarker(workflowCallId) : "";
try {
/** @type {any[]} */
const reviews = [];
@@ -564,7 +565,7 @@ function createReviewBuffer() {
if (review.state !== "CHANGES_REQUESTED") return false;
if (review.user?.type !== "Bot") return false;
if (workflowCallMarker) {
- return review.body?.includes(workflowCallMarker) || false;
+ return review.body?.includes(workflowCallMarker) || review.body?.includes(workflowCallReviewMarker) || false;
}
return matchesWorkflowId(review.body, workflowId);
});
diff --git a/actions/setup/js/pr_review_buffer.test.cjs b/actions/setup/js/pr_review_buffer.test.cjs
index 47fdc0fb4ed..019e08bc024 100644
--- a/actions/setup/js/pr_review_buffer.test.cjs
+++ b/actions/setup/js/pr_review_buffer.test.cjs
@@ -800,7 +800,7 @@ describe("pr_review_buffer (factory pattern)", () => {
expect(body.indexOf("Generated by")).toBeLessThan(body.indexOf("Policy for"));
});
- it("should append workflow-call-id marker to review body when available", async () => {
+ it("should append durable workflow-call-id provenance to review body when available", async () => {
const previousCallerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID;
process.env.GH_AW_CALLER_WORKFLOW_ID = "owner/repo/CallerA";
try {
@@ -831,6 +831,7 @@ describe("pr_review_buffer (factory pattern)", () => {
const callArgs = mockGithub.rest.pulls.createReview.mock.calls[0][0];
expect(callArgs.body).toContain("");
+ expect(callArgs.body).toContain('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerA"');
} finally {
if (previousCallerWorkflowId === undefined) {
delete process.env.GH_AW_CALLER_WORKFLOW_ID;
@@ -1240,7 +1241,7 @@ describe("pr_review_buffer (factory pattern)", () => {
}
});
- it("should dismiss older reviews matching workflow-call-id when supersede mode is enabled", async () => {
+ it("should dismiss only older blocking reviews with durable workflow-call-id provenance", async () => {
const previousWorkflowId = process.env.GH_AW_WORKFLOW_ID;
const previousCallerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID;
process.env.GH_AW_WORKFLOW_ID = "test-workflow";
@@ -1248,6 +1249,10 @@ describe("pr_review_buffer (factory pattern)", () => {
try {
buffer.setSupersedeOlderReviews(true);
buffer.setReviewMetadata("Updated review", "COMMENT");
+ buffer.setFooterContext({
+ workflowName: "test-workflow",
+ runUrl: "https://github.com/owner/repo/actions/runs/123",
+ });
buffer.setReviewContext({
repo: "owner/repo",
repoParts: { owner: "owner", repo: "repo" },
@@ -1263,10 +1268,10 @@ describe("pr_review_buffer (factory pattern)", () => {
});
mockGithub.rest.pulls.listReviews.mockResolvedValue({
data: [
- { id: 100, state: "CHANGES_REQUESTED", user: { login: "github-actions[bot]", type: "Bot" }, body: "\nOld blocking review" },
+ { id: 100, state: "CHANGES_REQUESTED", user: { login: "github-actions[bot]", type: "Bot" }, body: '[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerA"\nOld blocking review' },
{ id: 101, state: "CHANGES_REQUESTED", user: { login: "human-user", type: "User" }, body: "" },
{ id: 102, state: "APPROVED", user: { login: "github-actions[bot]", type: "Bot" }, body: "" },
- { id: 103, state: "CHANGES_REQUESTED", user: { login: "github-actions[bot]", type: "Bot" }, body: "" },
+ { id: 103, state: "CHANGES_REQUESTED", user: { login: "github-actions[bot]", type: "Bot" }, body: '[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerB"' },
{ id: 104, state: "CHANGES_REQUESTED", user: { login: "github-actions[bot]", type: "Bot" }, body: "" },
],
});
@@ -1275,6 +1280,7 @@ describe("pr_review_buffer (factory pattern)", () => {
const result = await buffer.submitReview();
expect(result.success).toBe(true);
+ expect(mockGithub.rest.pulls.createReview.mock.calls[0][0].body).toContain('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerA"');
// submitReview() reads reviews before superseding, during supersede
// candidate selection, and again after review creation for after-state.
expect(mockGithub.rest.pulls.listReviews).toHaveBeenCalledTimes(3);
From 01c17f48870333c0f066758192837fc8c4c02d73 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:28:33 +0000
Subject: [PATCH 3/9] Test review provenance encoding
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.test.cjs | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs
index 618ee17f1c2..87b58638386 100644
--- a/actions/setup/js/generate_footer.test.cjs
+++ b/actions/setup/js/generate_footer.test.cjs
@@ -37,6 +37,7 @@ describe("generate_footer.cjs", () => {
let generateXMLMarker;
let generateWorkflowIdMarker;
let generateWorkflowCallIdMarker;
+ let generateWorkflowCallIdReviewMarker;
let getWorkflowIdMarkerContent;
let normalizeCloseOlderKey;
@@ -58,6 +59,7 @@ describe("generate_footer.cjs", () => {
generateXMLMarker = module.generateXMLMarker;
generateWorkflowIdMarker = module.generateWorkflowIdMarker;
generateWorkflowCallIdMarker = module.generateWorkflowCallIdMarker;
+ generateWorkflowCallIdReviewMarker = module.generateWorkflowCallIdReviewMarker;
getWorkflowIdMarkerContent = module.getWorkflowIdMarkerContent;
normalizeCloseOlderKey = module.normalizeCloseOlderKey;
});
@@ -215,6 +217,14 @@ describe("generate_footer.cjs", () => {
expect(result).toBe("");
});
+ describe("generateWorkflowCallIdReviewMarker", () => {
+ it("should generate an encoded non-rendered marker", () => {
+ const result = generateWorkflowCallIdReviewMarker('owner/repo/Workflow "with" spaces');
+
+ expect(result).toBe('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FWorkflow%20%22with%22%20spaces"');
+ });
+ });
+
it("should handle caller IDs with slashes (repo/workflow format)", () => {
const result = generateWorkflowCallIdMarker("elastic/ai-github-actions/Explore: Live Elasticsearch");
From 5f819d8130f6fe06b4dfa809799f6760944511b2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:29:57 +0000
Subject: [PATCH 4/9] Match review provenance exactly
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 15 +++++++++++++++
actions/setup/js/generate_footer.test.cjs | 13 +++++++++++++
actions/setup/js/pr_review_buffer.cjs | 5 ++---
3 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index 2804cf63b86..ed4e6a852bc 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -183,6 +183,20 @@ function generateWorkflowCallIdReviewMarker(callerWorkflowId) {
return `[gh-aw-workflow-call-id]: # "${encodeURIComponent(callerWorkflowId)}"`;
}
+/**
+ * Check whether a review body has an exact workflow-call ID marker line.
+ * Supports the legacy HTML comment and the durable Markdown reference marker.
+ *
+ * @param {string|null|undefined} body - Review body
+ * @param {string} callerWorkflowId - Calling workflow identifier
+ * @returns {boolean} Whether the review belongs to the calling workflow
+ */
+function matchesWorkflowCallId(body, callerWorkflowId) {
+ if (!body) return false;
+ const markers = new Set([generateWorkflowCallIdMarker(callerWorkflowId), generateWorkflowCallIdReviewMarker(callerWorkflowId)]);
+ return body.split(/\r?\n/).some(line => markers.has(line.trim()));
+}
+
/**
* Normalizes a user-supplied close-older-key to identifier style.
* Converts to lowercase, replaces runs of non-alphanumeric/dash/underscore characters
@@ -306,6 +320,7 @@ module.exports = {
generateWorkflowIdMarker,
generateWorkflowCallIdMarker,
generateWorkflowCallIdReviewMarker,
+ matchesWorkflowCallId,
getWorkflowIdMarkerContent,
matchesWorkflowId,
isValidWorkflowId,
diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs
index 87b58638386..afc5f6f915d 100644
--- a/actions/setup/js/generate_footer.test.cjs
+++ b/actions/setup/js/generate_footer.test.cjs
@@ -38,6 +38,7 @@ describe("generate_footer.cjs", () => {
let generateWorkflowIdMarker;
let generateWorkflowCallIdMarker;
let generateWorkflowCallIdReviewMarker;
+ let matchesWorkflowCallId;
let getWorkflowIdMarkerContent;
let normalizeCloseOlderKey;
@@ -60,6 +61,7 @@ describe("generate_footer.cjs", () => {
generateWorkflowIdMarker = module.generateWorkflowIdMarker;
generateWorkflowCallIdMarker = module.generateWorkflowCallIdMarker;
generateWorkflowCallIdReviewMarker = module.generateWorkflowCallIdReviewMarker;
+ matchesWorkflowCallId = module.matchesWorkflowCallId;
getWorkflowIdMarkerContent = module.getWorkflowIdMarkerContent;
normalizeCloseOlderKey = module.normalizeCloseOlderKey;
});
@@ -223,6 +225,17 @@ describe("generate_footer.cjs", () => {
expect(result).toBe('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FWorkflow%20%22with%22%20spaces"');
});
+
+ describe("matchesWorkflowCallId", () => {
+ it("matches exact legacy and durable marker lines only", () => {
+ const callerWorkflowId = "owner/repo/Caller";
+
+ expect(matchesWorkflowCallId("Review\n", callerWorkflowId)).toBe(true);
+ expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"\nReview', callerWorkflowId)).toBe(true);
+ expect(matchesWorkflowCallId('Quoted [gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"', callerWorkflowId)).toBe(false);
+ expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerB"', callerWorkflowId)).toBe(false);
+ });
+ });
});
it("should handle caller IDs with slashes (repo/workflow format)", () => {
diff --git a/actions/setup/js/pr_review_buffer.cjs b/actions/setup/js/pr_review_buffer.cjs
index 3883c14499e..d05e6b498e1 100644
--- a/actions/setup/js/pr_review_buffer.cjs
+++ b/actions/setup/js/pr_review_buffer.cjs
@@ -22,7 +22,7 @@
const { generateFooterWithMessages, getBodyFooterMessage, getDetectionCautionAlert } = require("./messages_footer.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { isStagedMode } = require("./safe_output_helpers.cjs");
-const { generateWorkflowCallIdMarker, generateWorkflowCallIdReviewMarker, matchesWorkflowId } = require("./generate_footer.cjs");
+const { generateWorkflowCallIdMarker, generateWorkflowCallIdReviewMarker, matchesWorkflowCallId, matchesWorkflowId } = require("./generate_footer.cjs");
const { attachExecutionState, fetchPullRequestReviewState } = require("./safe_output_execution_metadata.cjs");
const { withRetry, RATE_LIMIT_RETRY_CONFIG, isTransientError, sleep } = require("./error_recovery.cjs");
const { ERR_API } = require("./error_codes.cjs");
@@ -532,7 +532,6 @@ function createReviewBuffer() {
return;
}
const workflowCallMarker = workflowCallId ? generateWorkflowCallIdMarker(workflowCallId) : "";
- const workflowCallReviewMarker = workflowCallId ? generateWorkflowCallIdReviewMarker(workflowCallId) : "";
try {
/** @type {any[]} */
const reviews = [];
@@ -565,7 +564,7 @@ function createReviewBuffer() {
if (review.state !== "CHANGES_REQUESTED") return false;
if (review.user?.type !== "Bot") return false;
if (workflowCallMarker) {
- return review.body?.includes(workflowCallMarker) || review.body?.includes(workflowCallReviewMarker) || false;
+ return matchesWorkflowCallId(review.body, workflowCallId);
}
return matchesWorkflowId(review.body, workflowId);
});
From 81b7e3a19df3f66c176326ac829729623ba1b694 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:31:15 +0000
Subject: [PATCH 5/9] Harden review provenance matching
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.test.cjs | 38 +++++++++++------------
actions/setup/js/pr_review_buffer.cjs | 3 +-
2 files changed, 20 insertions(+), 21 deletions(-)
diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs
index afc5f6f915d..865d0f12e78 100644
--- a/actions/setup/js/generate_footer.test.cjs
+++ b/actions/setup/js/generate_footer.test.cjs
@@ -219,25 +219,6 @@ describe("generate_footer.cjs", () => {
expect(result).toBe("");
});
- describe("generateWorkflowCallIdReviewMarker", () => {
- it("should generate an encoded non-rendered marker", () => {
- const result = generateWorkflowCallIdReviewMarker('owner/repo/Workflow "with" spaces');
-
- expect(result).toBe('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FWorkflow%20%22with%22%20spaces"');
- });
-
- describe("matchesWorkflowCallId", () => {
- it("matches exact legacy and durable marker lines only", () => {
- const callerWorkflowId = "owner/repo/Caller";
-
- expect(matchesWorkflowCallId("Review\n", callerWorkflowId)).toBe(true);
- expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"\nReview', callerWorkflowId)).toBe(true);
- expect(matchesWorkflowCallId('Quoted [gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"', callerWorkflowId)).toBe(false);
- expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerB"', callerWorkflowId)).toBe(false);
- });
- });
- });
-
it("should handle caller IDs with slashes (repo/workflow format)", () => {
const result = generateWorkflowCallIdMarker("elastic/ai-github-actions/Explore: Live Elasticsearch");
@@ -269,6 +250,25 @@ describe("generate_footer.cjs", () => {
});
});
+ describe("generateWorkflowCallIdReviewMarker", () => {
+ it("should generate an encoded non-rendered marker", () => {
+ const result = generateWorkflowCallIdReviewMarker('owner/repo/Workflow "with" spaces');
+
+ expect(result).toBe('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FWorkflow%20%22with%22%20spaces"');
+ });
+ });
+
+ describe("matchesWorkflowCallId", () => {
+ it("matches exact legacy and durable marker lines only", () => {
+ const callerWorkflowId = "owner/repo/Caller";
+
+ expect(matchesWorkflowCallId("Review\n", callerWorkflowId)).toBe(true);
+ expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"\nReview', callerWorkflowId)).toBe(true);
+ expect(matchesWorkflowCallId('Quoted [gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"', callerWorkflowId)).toBe(false);
+ expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerB"', callerWorkflowId)).toBe(false);
+ });
+ });
+
describe("getWorkflowIdMarkerContent", () => {
it("should return marker content without XML wrapper", () => {
const result = getWorkflowIdMarkerContent("test-workflow");
diff --git a/actions/setup/js/pr_review_buffer.cjs b/actions/setup/js/pr_review_buffer.cjs
index d05e6b498e1..497a29c0650 100644
--- a/actions/setup/js/pr_review_buffer.cjs
+++ b/actions/setup/js/pr_review_buffer.cjs
@@ -531,7 +531,6 @@ function createReviewBuffer() {
core.warning("supersede-older-reviews is enabled but neither GH_AW_WORKFLOW_ID nor GH_AW_CALLER_WORKFLOW_ID is set. Skipping stale review dismissal.");
return;
}
- const workflowCallMarker = workflowCallId ? generateWorkflowCallIdMarker(workflowCallId) : "";
try {
/** @type {any[]} */
const reviews = [];
@@ -563,7 +562,7 @@ function createReviewBuffer() {
if (!review || review.id === currentReviewId) return false;
if (review.state !== "CHANGES_REQUESTED") return false;
if (review.user?.type !== "Bot") return false;
- if (workflowCallMarker) {
+ if (workflowCallId) {
return matchesWorkflowCallId(review.body, workflowCallId);
}
return matchesWorkflowId(review.body, workflowId);
From fcf4e690fd0a9ec7fbfd47c38427ad6f2e286cd7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:32:35 +0000
Subject: [PATCH 6/9] Validate durable review provenance
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 16 +++++++++++++---
actions/setup/js/generate_footer.test.cjs | 2 ++
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index ed4e6a852bc..3b81a32e874 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -192,9 +192,19 @@ function generateWorkflowCallIdReviewMarker(callerWorkflowId) {
* @returns {boolean} Whether the review belongs to the calling workflow
*/
function matchesWorkflowCallId(body, callerWorkflowId) {
- if (!body) return false;
- const markers = new Set([generateWorkflowCallIdMarker(callerWorkflowId), generateWorkflowCallIdReviewMarker(callerWorkflowId)]);
- return body.split(/\r?\n/).some(line => markers.has(line.trim()));
+ if (!body || !callerWorkflowId) return false;
+ const legacyMarker = generateWorkflowCallIdMarker(callerWorkflowId);
+ return body.split(/\r?\n/).some(line => {
+ const trimmedLine = line.trim();
+ if (trimmedLine === legacyMarker) return true;
+ const durableMatch = trimmedLine.match(/^\[gh-aw-workflow-call-id\]: # "([^"]+)"$/);
+ if (!durableMatch) return false;
+ try {
+ return decodeURIComponent(durableMatch[1]) === callerWorkflowId;
+ } catch {
+ return false;
+ }
+ });
}
/**
diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs
index 865d0f12e78..0b01befbbc4 100644
--- a/actions/setup/js/generate_footer.test.cjs
+++ b/actions/setup/js/generate_footer.test.cjs
@@ -264,8 +264,10 @@ describe("generate_footer.cjs", () => {
expect(matchesWorkflowCallId("Review\n", callerWorkflowId)).toBe(true);
expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"\nReview', callerWorkflowId)).toBe(true);
+ expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2frepo%2fCaller"', callerWorkflowId)).toBe(true);
expect(matchesWorkflowCallId('Quoted [gh-aw-workflow-call-id]: # "owner%2Frepo%2FCaller"', callerWorkflowId)).toBe(false);
expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerB"', callerWorkflowId)).toBe(false);
+ expect(matchesWorkflowCallId('[gh-aw-workflow-call-id]: # ""', "")).toBe(false);
});
});
From 0e9a9ec0d2931bdea8a313e195fe896357c6724c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 26 Sep 2026 05:39:32 +0000
Subject: [PATCH 7/9] Emit review provenance independent of footer mode
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 30 +++++++++++++---
actions/setup/js/pr_review_buffer.cjs | 16 ++++++---
actions/setup/js/pr_review_buffer.test.cjs | 42 ++++++++++++++++++++++
3 files changed, 79 insertions(+), 9 deletions(-)
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index 3b81a32e874..016e7e2b3a8 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -183,6 +183,30 @@ function generateWorkflowCallIdReviewMarker(callerWorkflowId) {
return `[gh-aw-workflow-call-id]: # "${encodeURIComponent(callerWorkflowId)}"`;
}
+/**
+ * Check whether any trimmed line of a body satisfies a marker predicate.
+ *
+ * @param {string|null|undefined} body - Body to scan
+ * @param {(line: string) => boolean} predicate - Predicate applied to each trimmed line
+ * @returns {boolean} Whether any line matches
+ */
+function someMarkerLine(body, predicate) {
+ if (!body) return false;
+ return body.split(/\r?\n/).some(line => predicate(line.trim()));
+}
+
+/**
+ * Check whether a body contains a line that exactly equals the given marker.
+ *
+ * @param {string|null|undefined} body - Body to scan
+ * @param {string} marker - Complete marker line
+ * @returns {boolean} Whether an exact marker line is present
+ */
+function matchesExactMarkerLine(body, marker) {
+ if (!marker) return false;
+ return someMarkerLine(body, line => line === marker);
+}
+
/**
* Check whether a review body has an exact workflow-call ID marker line.
* Supports the legacy HTML comment and the durable Markdown reference marker.
@@ -193,10 +217,8 @@ function generateWorkflowCallIdReviewMarker(callerWorkflowId) {
*/
function matchesWorkflowCallId(body, callerWorkflowId) {
if (!body || !callerWorkflowId) return false;
- const legacyMarker = generateWorkflowCallIdMarker(callerWorkflowId);
- return body.split(/\r?\n/).some(line => {
- const trimmedLine = line.trim();
- if (trimmedLine === legacyMarker) return true;
+ if (matchesExactMarkerLine(body, generateWorkflowCallIdMarker(callerWorkflowId))) return true;
+ return someMarkerLine(body, trimmedLine => {
const durableMatch = trimmedLine.match(/^\[gh-aw-workflow-call-id\]: # "([^"]+)"$/);
if (!durableMatch) return false;
try {
diff --git a/actions/setup/js/pr_review_buffer.cjs b/actions/setup/js/pr_review_buffer.cjs
index 497a29c0650..bf2c3f6ef34 100644
--- a/actions/setup/js/pr_review_buffer.cjs
+++ b/actions/setup/js/pr_review_buffer.cjs
@@ -365,11 +365,6 @@ function createReviewBuffer() {
undefined,
{ skipDetectionCaution: true }
);
-
- const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || "";
- if (callerWorkflowId) {
- body += "\n" + generateWorkflowCallIdMarker(callerWorkflowId) + "\n" + generateWorkflowCallIdReviewMarker(callerWorkflowId);
- }
}
if (footerContext) {
const bodyFooter = getBodyFooterMessage(footerContext.bodyFooter, footerContext);
@@ -378,6 +373,17 @@ function createReviewBuffer() {
}
}
+ // Always embed caller provenance, even when the visible footer is disabled, so
+ // supersede-older-reviews can identify this review in later runs. The legacy HTML
+ // marker is kept for consistency with other safe outputs and existing readers;
+ // the Markdown reference marker is the durable form because GitHub strips HTML
+ // comments from submitted review bodies.
+ const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || "";
+ if (callerWorkflowId) {
+ const provenance = generateWorkflowCallIdMarker(callerWorkflowId) + "\n" + generateWorkflowCallIdReviewMarker(callerWorkflowId);
+ body = body.trim() ? body.trimEnd() + "\n\n" + provenance : provenance;
+ }
+
// Build comments array for the API
let comments = bufferedComments.map(comment => {
/** @type {any} */
diff --git a/actions/setup/js/pr_review_buffer.test.cjs b/actions/setup/js/pr_review_buffer.test.cjs
index 019e08bc024..c80e47659c1 100644
--- a/actions/setup/js/pr_review_buffer.test.cjs
+++ b/actions/setup/js/pr_review_buffer.test.cjs
@@ -841,6 +841,48 @@ describe("pr_review_buffer (factory pattern)", () => {
}
});
+ it("should append workflow-call-id provenance even when footer is disabled", async () => {
+ const previousCallerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID;
+ process.env.GH_AW_CALLER_WORKFLOW_ID = "owner/repo/CallerA";
+ try {
+ buffer.addComment({ path: "test.js", line: 1, body: "comment" });
+ buffer.setReviewMetadata("", "REQUEST_CHANGES");
+ buffer.setReviewContext({
+ repo: "owner/repo",
+ repoParts: { owner: "owner", repo: "repo" },
+ pullRequestNumber: 42,
+ pullRequest: { head: { sha: "abc123" } },
+ });
+ buffer.setFooterContext({
+ workflowName: "test-workflow",
+ runUrl: "https://github.com/owner/repo/actions/runs/123",
+ workflowSource: "owner/repo/workflows/test.md@v1",
+ workflowSourceURL: "https://github.com/owner/repo/blob/main/test.md",
+ });
+ buffer.setFooterMode("none");
+
+ mockGithub.rest.pulls.createReview.mockResolvedValue({
+ data: {
+ id: 406,
+ html_url: "https://github.com/owner/repo/pull/42#pullrequestreview-406",
+ },
+ });
+
+ const result = await buffer.submitReview();
+ expect(result.success).toBe(true);
+
+ const callArgs = mockGithub.rest.pulls.createReview.mock.calls[0][0];
+ expect(callArgs.body).not.toContain("test-workflow");
+ expect(callArgs.body).toBe('\n[gh-aw-workflow-call-id]: # "owner%2Frepo%2FCallerA"');
+ } finally {
+ if (previousCallerWorkflowId === undefined) {
+ delete process.env.GH_AW_CALLER_WORKFLOW_ID;
+ } else {
+ process.env.GH_AW_CALLER_WORKFLOW_ID = previousCallerWorkflowId;
+ }
+ }
+ });
+
it("should skip footer when setIncludeFooter('none') is called", async () => {
buffer.addComment({ path: "test.js", line: 1, body: "comment" });
buffer.setReviewMetadata("Review body", "COMMENT");
From 802a31f71e81db70f52e78be7e13cf06e7085ec8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 28 Sep 2026 16:58:06 +0000
Subject: [PATCH 8/9] Standardize generated footer provenance
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/messages.test.cjs | 34 ++++++++++++++------
actions/setup/js/messages_footer.cjs | 36 ++++++++++++----------
docs/src/content/docs/reference/footers.md | 4 +++
3 files changed, 47 insertions(+), 27 deletions(-)
diff --git a/actions/setup/js/messages.test.cjs b/actions/setup/js/messages.test.cjs
index 880c97bbf3c..0bc030f4028 100644
--- a/actions/setup/js/messages.test.cjs
+++ b/actions/setup/js/messages.test.cjs
@@ -36,6 +36,7 @@ describe("messages.cjs", () => {
delete process.env.GH_AW_TRACKER_ID;
delete process.env.GITHUB_RUN_ID;
delete process.env.GH_AW_WORKFLOW_ID;
+ delete process.env.GH_AW_CALLER_WORKFLOW_ID;
delete process.env.GH_AW_DEPRECATED_COST;
delete process.env.GH_AW_AIC;
delete process.env.GH_AW_AMBIENT_CONTEXT;
@@ -270,7 +271,7 @@ describe("messages.cjs", () => {
expect(result).toBe("> Test (Test)");
});
- it("should append history link when historyUrl is provided", async () => {
+ it("should append provenance on its own line when historyUrl is provided", async () => {
const { getFooterMessage } = await import("./messages.cjs");
const result = getFooterMessage({
@@ -279,7 +280,20 @@ describe("messages.cjs", () => {
historyUrl: "https://github.com/search?q=repo:test/repo+is:issue&type=issues",
});
- expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) · [◷](https://github.com/search?q=repo:test/repo+is:issue&type=issues)");
+ expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)\n> Provenance: [Test Workflow](https://github.com/search?q=repo:test/repo+is:issue&type=issues)");
+ });
+
+ it("should use the caller workflow ID as the provenance identifier", async () => {
+ process.env.GH_AW_CALLER_WORKFLOW_ID = "owner/repo/test-workflow";
+ const { getFooterMessage } = await import("./messages.cjs");
+
+ const result = getFooterMessage({
+ workflowName: "Test Workflow",
+ runUrl: "https://github.com/test/repo/actions/runs/123",
+ historyUrl: "https://github.com/search?q=repo:test/repo+is:issue&type=issues",
+ });
+
+ expect(result).toContain("> Provenance: [owner/repo/test-workflow](https://github.com/search?q=repo:test/repo+is:issue&type=issues)");
});
it("should include both triggering number and history link when both are provided", async () => {
@@ -292,7 +306,7 @@ describe("messages.cjs", () => {
historyUrl: "https://github.com/search?q=repo:test/repo+is:issue&type=issues",
});
- expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) for #42 · [◷](https://github.com/search?q=repo:test/repo+is:issue&type=issues)");
+ expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) for #42\n> Provenance: [Test Workflow](https://github.com/search?q=repo:test/repo+is:issue&type=issues)");
});
it("should not append history link when historyUrl is not provided", async () => {
@@ -321,7 +335,7 @@ describe("messages.cjs", () => {
historyUrl,
});
- expect(result).toBe(`> 🤖 *Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)* · [◷](${historyUrl})`);
+ expect(result).toBe(`> 🤖 *Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)* · [◷](${historyUrl})\n> Provenance: [Test Workflow](${historyUrl})`);
});
it("should render empty string for {history_link} when historyUrl is not provided", async () => {
@@ -410,7 +424,7 @@ describe("messages.cjs", () => {
historyUrl,
});
- expect(result).toBe(`> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) · [◷](${historyUrl})`);
+ expect(result).toBe(`> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)\n> Provenance: [Test Workflow](${historyUrl})`);
});
it("should include AI Credits without AI Credits when GH_AW_AIC is set", async () => {
@@ -666,7 +680,7 @@ describe("messages.cjs", () => {
slashCommand: "deploy",
});
- expect(result).toBe(`> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) · [◷](${historyUrl})\n> Comment /deploy to run again`);
+ expect(result).toBe(`> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)\n> Provenance: [Test Workflow](${historyUrl})\n> Comment /deploy to run again`);
});
it("should include slash command hint in custom footer templates", async () => {
@@ -1214,7 +1228,7 @@ describe("messages.cjs", () => {
expect(result).toBe("> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123)");
});
- it("should include history link without AI Credits in default footer", async () => {
+ it("should include provenance without AI Credits in default footer", async () => {
process.env.GH_AW_DEPRECATED_COST = "5000";
const historyUrl = "https://github.com/search?q=repo:test/repo+is:issue&type=issues";
@@ -1226,7 +1240,7 @@ describe("messages.cjs", () => {
historyUrl,
});
- expect(result).toBe(`> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123) · [◷](${historyUrl})`);
+ expect(result).toBe(`> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123)\n> Provenance: [Test Workflow](${historyUrl})`);
});
it("should include AIC and ambient context in default footer when available", async () => {
@@ -1316,7 +1330,7 @@ describe("messages.cjs", () => {
expect(result).toBe("> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123)");
});
- it("should include history link without AI Credits in default footer", async () => {
+ it("should include provenance without AI Credits in default footer", async () => {
process.env.GH_AW_DEPRECATED_COST = "5000";
const historyUrl = "https://github.com/search?q=repo:test/repo+is:issue&type=issues";
@@ -1328,7 +1342,7 @@ describe("messages.cjs", () => {
historyUrl,
});
- expect(result).toBe(`> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123) · [◷](${historyUrl})`);
+ expect(result).toBe(`> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123)\n> Provenance: [Test Workflow](${historyUrl})`);
});
it("should include explicit context AIC in the default footer", async () => {
diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs
index 8f6314855b8..49f378ee3ea 100644
--- a/actions/setup/js/messages_footer.cjs
+++ b/actions/setup/js/messages_footer.cjs
@@ -161,7 +161,7 @@ function getAICFromEnv() {
* @property {string} [workflowSourceUrl] - GitHub URL for the workflow source
* @property {number|string} [triggeringNumber] - Issue, PR, or discussion number that triggered this workflow
* @property {"issue"|"PR"|"discussion"} [triggeringType] - Triggering item type used in the default footer
- * @property {string} [historyUrl] - GitHub search URL for items created by this workflow (for the history link)
+ * @property {string} [historyUrl] - GitHub search URL for items created by this workflow
* @property {string} [historyLink] - Pre-formatted markdown history link (e.g. " · [◷](url)"), or "" if unavailable
* @property {number|string} [aiCredits] - Total AI Credits cost for the run (1 AIC == 0.01 USD)
* @property {string} [emoji] - Optional emoji representing the workflow (from frontmatter)
@@ -170,6 +170,18 @@ function getAICFromEnv() {
* @property {string} [labelCommand] - Label command name for the run-again hint, when applicable
*/
+/**
+ * Append a standardized provenance line when a history search is available.
+ * @param {string} footer - Rendered footer text
+ * @param {FooterContext} ctx - Context for footer generation
+ * @returns {string} Footer text with provenance
+ */
+function appendFooterProvenance(footer, ctx) {
+ if (!ctx.historyUrl) return footer;
+ const identifier = process.env.GH_AW_CALLER_WORKFLOW_ID || process.env.GH_AW_WORKFLOW_ID || ctx.workflowName;
+ return `${footer}\n> Provenance: [${identifier}](${ctx.historyUrl})`;
+}
+
/**
* Get the footer message, using custom template if configured.
* @param {FooterContext} ctx - Context for footer generation
@@ -265,7 +277,8 @@ function getFooterMessage(ctx) {
// Use custom footer template if configured
if (messages?.footer) {
const renderedCustomFooter = renderTemplate(messages.footer, templateContext);
- return renderedCustomFooter + getRunAgainHints(renderedCustomFooter);
+ const footer = appendFooterProvenance(renderedCustomFooter, ctx);
+ return footer + getRunAgainHints(footer);
}
// Default footer template - includes emoji prefix when available
@@ -285,12 +298,9 @@ function getFooterMessage(ctx) {
if (metricSuffixes.length > 0) {
defaultFooter += metricSuffixes.join("");
}
- // Append history link when available
- if (ctx.historyUrl) {
- defaultFooter += " · [◷]({history_url})";
- }
const renderedDefaultFooter = renderTemplate(defaultFooter, templateContext);
- return renderedDefaultFooter + getRunAgainHints(renderedDefaultFooter);
+ const footer = appendFooterProvenance(renderedDefaultFooter, ctx);
+ return footer + getRunAgainHints(footer);
}
/**
@@ -496,14 +506,10 @@ function getFooterAgentFailureIssueMessage(ctx) {
if (ambientContext) {
defaultFooter += ambientContextSuffix;
}
- // Append history link when available
- if (ctx.historyUrl) {
- defaultFooter += " · [◷]({history_url})";
- }
footer = renderTemplate(defaultFooter, templateContext);
}
- return footer;
+ return appendFooterProvenance(footer, ctx);
}
/**
@@ -588,14 +594,10 @@ function getFooterAgentFailureCommentMessage(ctx) {
if (ambientContext) {
defaultFooter += ambientContextSuffix;
}
- // Append history link when available
- if (ctx.historyUrl) {
- defaultFooter += " · [◷]({history_url})";
- }
footer = renderTemplate(defaultFooter, templateContext);
}
- return footer;
+ return appendFooterProvenance(footer, ctx);
}
/**
diff --git a/docs/src/content/docs/reference/footers.md b/docs/src/content/docs/reference/footers.md
index e0748186d61..72b17e7af57 100644
--- a/docs/src/content/docs/reference/footers.md
+++ b/docs/src/content/docs/reference/footers.md
@@ -75,6 +75,10 @@ safe-outputs:
The global body footer is added to any handler-specific `body-footer`; both are appended even when `footer: false` and remain separate from the generated attribution footer. Body footers from imported agentic workflows are additive, in import order. The template supports `{workflow_name}` and `{run_url}` placeholders.
+## Provenance
+
+When a generated footer has a history search, it includes a separate subscript provenance line. The caller workflow ID is the link text and links to the same search previously exposed by the clock icon.
+
## PR Review Footer Control
For PR reviews (`submit-pull-request-review`), the `footer` field supports conditional control over when the footer is added to the review body:
From 77e68f243de97193ba197b8cc09fb132434d0244 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 28 Sep 2026 22:40:01 +0000
Subject: [PATCH 9/9] Standardize legacy footer history links
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
actions/setup/js/messages.test.cjs | 5 +++--
actions/setup/js/messages_footer.cjs | 14 +++++++-------
docs/src/content/docs/reference/footers.md | 2 +-
3 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/actions/setup/js/messages.test.cjs b/actions/setup/js/messages.test.cjs
index 0bc030f4028..a8e35ab3fd0 100644
--- a/actions/setup/js/messages.test.cjs
+++ b/actions/setup/js/messages.test.cjs
@@ -321,7 +321,7 @@ describe("messages.cjs", () => {
expect(result).not.toContain("◷");
});
- it("should expose {history_link} placeholder in custom footer templates", async () => {
+ it("should replace the legacy {history_link} placeholder with standardized provenance", async () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
footer: "> 🤖 *Generated by [{workflow_name}]({run_url})*{history_link}",
});
@@ -335,7 +335,8 @@ describe("messages.cjs", () => {
historyUrl,
});
- expect(result).toBe(`> 🤖 *Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)* · [◷](${historyUrl})\n> Provenance: [Test Workflow](${historyUrl})`);
+ expect(result).toBe(`> 🤖 *Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)*\n> Provenance: [Test Workflow](${historyUrl})`);
+ expect(result).not.toContain("◷");
});
it("should render empty string for {history_link} when historyUrl is not provided", async () => {
diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs
index 49f378ee3ea..072af30b2c7 100644
--- a/actions/setup/js/messages_footer.cjs
+++ b/actions/setup/js/messages_footer.cjs
@@ -162,7 +162,7 @@ function getAICFromEnv() {
* @property {number|string} [triggeringNumber] - Issue, PR, or discussion number that triggered this workflow
* @property {"issue"|"PR"|"discussion"} [triggeringType] - Triggering item type used in the default footer
* @property {string} [historyUrl] - GitHub search URL for items created by this workflow
- * @property {string} [historyLink] - Pre-formatted markdown history link (e.g. " · [◷](url)"), or "" if unavailable
+ * @property {string} [historyLink] - Deprecated history link placeholder, always ""
* @property {number|string} [aiCredits] - Total AI Credits cost for the run (1 AIC == 0.01 USD)
* @property {string} [emoji] - Optional emoji representing the workflow (from frontmatter)
* @property {string} [slashCommand] - Slash command name (without leading slash) for the run-again hint, when applicable
@@ -215,8 +215,8 @@ function getFooterMessage(ctx) {
const detectionConclusion = process.env.GH_AW_DETECTION_CONCLUSION || undefined;
const detectionReason = process.env.GH_AW_DETECTION_REASON || undefined;
- // Pre-compute history_link as a ready-to-use markdown suffix (empty string when unavailable)
- const historyLink = ctx.historyUrl ? ` · [◷](${ctx.historyUrl})` : "";
+ // Keep the legacy placeholder empty; provenance is rendered on its own line below.
+ const historyLink = "";
// Pre-compute agentic_workflow_url as the direct link to the agentic workflow page
const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : "");
@@ -432,8 +432,8 @@ function getFooterWorkflowRecompileCommentMessage(ctx) {
function getFooterAgentFailureIssueMessage(ctx) {
const messages = getMessages();
- // Pre-compute history_link as a ready-to-use markdown suffix (empty string when unavailable)
- const historyLink = ctx.historyUrl ? ` · [◷](${ctx.historyUrl})` : "";
+ // Keep the legacy placeholder empty; provenance is rendered on its own line below.
+ const historyLink = "";
// Pre-compute agentic_workflow_url as the direct link to the agentic workflow page
const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : "");
@@ -520,8 +520,8 @@ function getFooterAgentFailureIssueMessage(ctx) {
function getFooterAgentFailureCommentMessage(ctx) {
const messages = getMessages();
- // Pre-compute history_link as a ready-to-use markdown suffix (empty string when unavailable)
- const historyLink = ctx.historyUrl ? ` · [◷](${ctx.historyUrl})` : "";
+ // Keep the legacy placeholder empty; provenance is rendered on its own line below.
+ const historyLink = "";
// Pre-compute agentic_workflow_url as the direct link to the agentic workflow page
const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : "");
diff --git a/docs/src/content/docs/reference/footers.md b/docs/src/content/docs/reference/footers.md
index 72b17e7af57..b84196809ce 100644
--- a/docs/src/content/docs/reference/footers.md
+++ b/docs/src/content/docs/reference/footers.md
@@ -108,7 +108,7 @@ safe-outputs:
title-prefix: "[bot] "
```
-The `messages.footer` template supports variables like `{workflow_name}`, `{agentic_workflow_url}`, `{run_url}`, `{triggering_number}`, `{triggering_type}`, `{ai_credits_suffix}`, and more. `{triggering_type}` is `issue`, `PR`, or `discussion`, matching the type of item that triggered the run. `{agentic_workflow_url}` links directly to the agentic workflow file view for the run (equivalent to `{run_url}/agentic_workflow`), while `{run_url}` links to the plain Actions run page. `{ai_credits_suffix}` is a pre-formatted, always-safe suffix (e.g. `" · sonnet46 12.4 AIC"` or `""`) that you can place directly before `{history_link}`. When the run's engine model is known, the suffix is prefixed with a deterministic compact model identifier (`sonnet46`, `gpt55`, `opus47`, `haiku45`, `gem25`, …), and direct short aliases like `opus`, `sonnet`, and `haiku` are preserved. Individual cost components are also exposed: `{ai_model}` (full model name, e.g. `claude-sonnet-4.6`), `{ai_model_short}` (compact identifier, e.g. `sonnet46`), `{ai_credits}` (raw numeric cost), `{ai_credits_formatted}` (formatted cost), `{ai_credits_unit}` (always `AIC`), `{agent_ai_credits_formatted}`, `{evals_ai_credits_formatted}`, `{threat_detection_ai_credits_formatted}`, `{detection_conclusion}`, and `{detection_reason}`. See [Custom Messages](/gh-aw/reference/safe-outputs/#custom-messages-messages) for complete documentation on message templates and available variables.
+The `messages.footer` template supports variables like `{workflow_name}`, `{agentic_workflow_url}`, `{run_url}`, `{triggering_number}`, `{triggering_type}`, `{ai_credits_suffix}`, and more. `{triggering_type}` is `issue`, `PR`, or `discussion`, matching the type of item that triggered the run. `{agentic_workflow_url}` links directly to the agentic workflow file view for the run (equivalent to `{run_url}/agentic_workflow`), while `{run_url}` links to the plain Actions run page. `{ai_credits_suffix}` is a pre-formatted, always-safe suffix (e.g. `" · sonnet46 12.4 AIC"` or `""`). The legacy `{history_link}` placeholder is retained for compatibility but now renders as an empty string; the standardized provenance line is appended automatically. When the run's engine model is known, the suffix is prefixed with a deterministic compact model identifier (`sonnet46`, `gpt55`, `opus47`, `haiku45`, `gem25`, …), and direct short aliases like `opus`, `sonnet`, and `haiku` are preserved. Individual cost components are also exposed: `{ai_model}` (full model name, e.g. `claude-sonnet-4.6`), `{ai_model_short}` (compact identifier, e.g. `sonnet46`), `{ai_credits}` (raw numeric cost), `{ai_credits_formatted}` (formatted cost), `{ai_credits_unit}` (always `AIC`), `{agent_ai_credits_formatted}`, `{evals_ai_credits_formatted}`, `{threat_detection_ai_credits_formatted}`, `{detection_conclusion}`, and `{detection_reason}`. See [Custom Messages](/gh-aw/reference/safe-outputs/#custom-messages-messages) for complete documentation on message templates and available variables.
## Learn More