Skip to content

Commit 4bbca15

Browse files
Copilotpelikhangithub-actions[bot]
authored
Use workflow-id instead of runId for stable island identification (#15207)
* Initial plan * Fix updateBody to use workflow-id instead of runId for stable island identification Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Add changeset [skip-ci] --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 52302d9 commit 4bbca15

8 files changed

Lines changed: 117 additions & 108 deletions

.changeset/patch-stable-island-markers.md

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/fuzz_update_body_harness.cjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ const { updateBody } = require("./update_pr_description_helpers.cjs");
2323
* @param {string} operation - Operation type: "append", "prepend", "replace", or "replace-island"
2424
* @param {string} workflowName - Name of the workflow
2525
* @param {string} runUrl - URL of the workflow run
26-
* @param {number} runId - Workflow run ID
26+
* @param {string} workflowId - Workflow ID (stable identifier across runs)
2727
* @returns {{result: string, error: string | null}} Result object
2828
*/
29-
function testUpdateBody(currentBody, newContent, operation, workflowName, runUrl, runId) {
29+
function testUpdateBody(currentBody, newContent, operation, workflowName, runUrl, workflowId) {
3030
try {
3131
const result = updateBody({
3232
currentBody,
3333
newContent,
3434
operation,
3535
workflowName,
3636
runUrl,
37-
runId,
37+
workflowId,
3838
});
3939
return { result, error: null };
4040
} catch (err) {
@@ -55,9 +55,9 @@ if (require.main === module) {
5555

5656
process.stdin.on("end", () => {
5757
try {
58-
// Parse input as JSON: { currentBody, newContent, operation, workflowName, runUrl, runId }
59-
const { currentBody, newContent, operation, workflowName, runUrl, runId } = JSON.parse(input);
60-
const result = testUpdateBody(currentBody || "", newContent || "", operation || "append", workflowName || "Test Workflow", runUrl || "https://github.com/test/actions/runs/123", runId || 123);
58+
// Parse input as JSON: { currentBody, newContent, operation, workflowName, runUrl, workflowId }
59+
const { currentBody, newContent, operation, workflowName, runUrl, workflowId } = JSON.parse(input);
60+
const result = testUpdateBody(currentBody || "", newContent || "", operation || "append", workflowName || "Test Workflow", runUrl || "https://github.com/test/actions/runs/123", workflowId || "test-workflow");
6161
process.stdout.write(JSON.stringify(result));
6262
process.exit(0);
6363
} catch (err) {

actions/setup/js/update_issue.cjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) {
5252

5353
// Get workflow run URL for AI attribution
5454
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "GitHub Agentic Workflow";
55+
const workflowId = process.env.GH_AW_WORKFLOW_ID || "";
5556
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
5657

5758
// Use helper to update body (handles all operations including replace)
@@ -61,7 +62,7 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) {
6162
operation,
6263
workflowName,
6364
runUrl,
64-
runId: context.runId,
65+
workflowId,
6566
includeFooter, // Pass footer flag to helper
6667
});
6768

actions/setup/js/update_issue.test.cjs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe("update_issue.cjs - footer support", () => {
5252

5353
// Reset environment variables
5454
process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
55+
process.env.GH_AW_WORKFLOW_ID = "test-workflow";
5556

5657
// Reset mock implementations
5758
mockGithub.rest.issues.get.mockResolvedValue({
@@ -209,17 +210,17 @@ describe("update_issue.cjs - footer support", () => {
209210
});
210211

211212
const newContent = "Island content";
212-
const expectedBody = `Original content\n\n---\n\n<!-- gh-aw-island-start:12345 -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:12345 -->`;
213+
const expectedBody = `Original content\n\n---\n\n<!-- gh-aw-island-start:test-workflow -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:test-workflow -->`;
213214

214215
expect(expectedBody).toContain("Original content");
215216
expect(expectedBody).toContain("Island content");
216-
expect(expectedBody).toContain("<!-- gh-aw-island-start:12345 -->");
217-
expect(expectedBody).toContain("<!-- gh-aw-island-end:12345 -->");
217+
expect(expectedBody).toContain("<!-- gh-aw-island-start:test-workflow -->");
218+
expect(expectedBody).toContain("<!-- gh-aw-island-end:test-workflow -->");
218219
expect(expectedBody).toContain("> AI generated by");
219220
});
220221

221222
it("should replace existing island content", async () => {
222-
const existingBody = "Before\n<!-- gh-aw-island-start:12345 -->\nOld island\n<!-- gh-aw-island-end:12345 -->\nAfter";
223+
const existingBody = "Before\n<!-- gh-aw-island-start:test-workflow -->\nOld island\n<!-- gh-aw-island-end:test-workflow -->\nAfter";
223224
mockGithub.rest.issues.get.mockResolvedValueOnce({
224225
data: {
225226
number: 100,
@@ -230,7 +231,7 @@ describe("update_issue.cjs - footer support", () => {
230231
});
231232

232233
const newContent = "New island";
233-
const expectedBody = `Before\n<!-- gh-aw-island-start:12345 -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:12345 -->\nAfter`;
234+
const expectedBody = `Before\n<!-- gh-aw-island-start:test-workflow -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:test-workflow -->\nAfter`;
234235

235236
expect(expectedBody).toContain("Before");
236237
expect(expectedBody).toContain("After");
@@ -456,17 +457,17 @@ describe("update_issue.cjs - footer support", () => {
456457
});
457458

458459
const newContent = "New island";
459-
// Should append because island with run ID 12345 doesn't exist
460-
const expectedBody = `${existingBody}\n\n---\n\n<!-- gh-aw-island-start:12345 -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:12345 -->`;
460+
// Should append because island with workflow test-workflow doesn't exist
461+
const expectedBody = `${existingBody}\n\n---\n\n<!-- gh-aw-island-start:test-workflow -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:test-workflow -->`;
461462

462463
expect(expectedBody).toContain("Other island");
463464
expect(expectedBody).toContain("New island");
464465
expect(expectedBody).toContain("<!-- gh-aw-island-start:99999 -->");
465-
expect(expectedBody).toContain("<!-- gh-aw-island-start:12345 -->");
466+
expect(expectedBody).toContain("<!-- gh-aw-island-start:test-workflow -->");
466467
});
467468

468469
it("should preserve content outside island when replacing", async () => {
469-
const existingBody = "# Title\n\n<!-- gh-aw-island-start:12345 -->\nOld\n<!-- gh-aw-island-end:12345 -->\n\n## Footer";
470+
const existingBody = "# Title\n\n<!-- gh-aw-island-start:test-workflow -->\nOld\n<!-- gh-aw-island-end:test-workflow -->\n\n## Footer";
470471
mockGithub.rest.issues.get.mockResolvedValueOnce({
471472
data: {
472473
number: 100,
@@ -477,7 +478,7 @@ describe("update_issue.cjs - footer support", () => {
477478
});
478479

479480
const newContent = "Updated content";
480-
const expectedBody = `# Title\n\n<!-- gh-aw-island-start:12345 -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:12345 -->\n\n## Footer`;
481+
const expectedBody = `# Title\n\n<!-- gh-aw-island-start:test-workflow -->\n${newContent}\n\n> AI generated by [Test Workflow](https://github.com/testowner/testrepo/actions/runs/12345)\n<!-- gh-aw-island-end:test-workflow -->\n\n## Footer`;
481482

482483
expect(expectedBody).toContain("# Title");
483484
expect(expectedBody).toContain("## Footer");

actions/setup/js/update_pr_description_helpers.cjs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,31 @@ function buildAIFooter(workflowName, runUrl) {
2222

2323
/**
2424
* Build the island start marker for replace-island mode
25-
* @param {number} runId - Workflow run ID
25+
* @param {string} workflowId - Workflow ID (stable identifier across runs)
2626
* @returns {string} Island start marker
2727
*/
28-
function buildIslandStartMarker(runId) {
29-
return `<!-- gh-aw-island-start:${runId} -->`;
28+
function buildIslandStartMarker(workflowId) {
29+
return `<!-- gh-aw-island-start:${workflowId} -->`;
3030
}
3131

3232
/**
3333
* Build the island end marker for replace-island mode
34-
* @param {number} runId - Workflow run ID
34+
* @param {string} workflowId - Workflow ID (stable identifier across runs)
3535
* @returns {string} Island end marker
3636
*/
37-
function buildIslandEndMarker(runId) {
38-
return `<!-- gh-aw-island-end:${runId} -->`;
37+
function buildIslandEndMarker(workflowId) {
38+
return `<!-- gh-aw-island-end:${workflowId} -->`;
3939
}
4040

4141
/**
4242
* Find and extract island content from body
4343
* @param {string} body - The body content to search
44-
* @param {number} runId - Workflow run ID
44+
* @param {string} workflowId - Workflow ID (stable identifier across runs)
4545
* @returns {{found: boolean, startIndex: number, endIndex: number}} Island location info
4646
*/
47-
function findIsland(body, runId) {
48-
const startMarker = buildIslandStartMarker(runId);
49-
const endMarker = buildIslandEndMarker(runId);
47+
function findIsland(body, workflowId) {
48+
const startMarker = buildIslandStartMarker(workflowId);
49+
const endMarker = buildIslandEndMarker(workflowId);
5050

5151
const startIndex = body.indexOf(startMarker);
5252
if (startIndex === -1) {
@@ -70,12 +70,12 @@ function findIsland(body, runId) {
7070
* @param {string} params.operation - Operation type: "append", "prepend", "replace", or "replace-island"
7171
* @param {string} params.workflowName - Name of the workflow
7272
* @param {string} params.runUrl - URL of the workflow run
73-
* @param {number} params.runId - Workflow run ID
73+
* @param {string} params.workflowId - Workflow ID (stable identifier across runs)
7474
* @param {boolean} [params.includeFooter=true] - Whether to include AI-generated footer (default: true)
7575
* @returns {string} Updated body content
7676
*/
7777
function updateBody(params) {
78-
const { currentBody, newContent, operation, workflowName, runUrl, runId, includeFooter = true } = params;
78+
const { currentBody, newContent, operation, workflowName, runUrl, workflowId, includeFooter = true } = params;
7979
const aiFooter = includeFooter ? buildAIFooter(workflowName, runUrl) : "";
8080

8181
if (operation === "replace") {
@@ -85,24 +85,24 @@ function updateBody(params) {
8585
}
8686

8787
if (operation === "replace-island") {
88-
// Try to find existing island for this run ID
89-
const island = findIsland(currentBody, runId);
88+
// Try to find existing island for this workflow ID
89+
const island = findIsland(currentBody, workflowId);
9090

9191
if (island.found) {
9292
// Replace the island content
93-
core.info(`Operation: replace-island (updating existing island for run ${runId})`);
94-
const startMarker = buildIslandStartMarker(runId);
95-
const endMarker = buildIslandEndMarker(runId);
93+
core.info(`Operation: replace-island (updating existing island for workflow ${workflowId})`);
94+
const startMarker = buildIslandStartMarker(workflowId);
95+
const endMarker = buildIslandEndMarker(workflowId);
9696
const islandContent = `${startMarker}\n${newContent}${aiFooter}\n${endMarker}`;
9797

9898
const before = currentBody.substring(0, island.startIndex);
9999
const after = currentBody.substring(island.endIndex);
100100
return before + islandContent + after;
101101
} else {
102102
// Island not found, fall back to append mode
103-
core.info(`Operation: replace-island (island not found for run ${runId}, falling back to append)`);
104-
const startMarker = buildIslandStartMarker(runId);
105-
const endMarker = buildIslandEndMarker(runId);
103+
core.info(`Operation: replace-island (island not found for workflow ${workflowId}, falling back to append)`);
104+
const startMarker = buildIslandStartMarker(workflowId);
105+
const endMarker = buildIslandEndMarker(workflowId);
106106
const islandContent = `${startMarker}\n${newContent}${aiFooter}\n${endMarker}`;
107107
const appendSection = `\n\n---\n\n${islandContent}`;
108108
return currentBody + appendSection;

0 commit comments

Comments
 (0)