From b32bff2b900a6a19a67c51fed1a23b3e32f04487 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:38:57 +0000 Subject: [PATCH 01/12] Initial plan From 8f54ed062d8e34889886153ed4b411d0befb3c88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:52:43 +0000 Subject: [PATCH 02/12] chore: outline fix plan Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/mcp-inspector.lock.yml | 2 +- .github/workflows/smoke-otel-backends.lock.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index a6c476a93e1..ad1d4a64f61 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -996,7 +996,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APP_KEY}", + "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ diff --git a/.github/workflows/smoke-otel-backends.lock.yml b/.github/workflows/smoke-otel-backends.lock.yml index 878ea3a0c79..59f8342c061 100644 --- a/.github/workflows/smoke-otel-backends.lock.yml +++ b/.github/workflows/smoke-otel-backends.lock.yml @@ -769,7 +769,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APP_KEY}", + "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ From bfb3b3b8aafc9ae55f1a298d5636a5ef58b77056 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:54:43 +0000 Subject: [PATCH 03/12] fix: resolve temporary IDs during signed bundle replay Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 8 ++ actions/setup/js/push_signed_commits.cjs | 82 ++++++++++++++++++- actions/setup/js/push_signed_commits.test.cjs | 32 ++++++++ .../setup/js/push_to_pull_request_branch.cjs | 2 + 4 files changed, 120 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 3807b012bd0..a1d0453844c 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1507,6 +1507,8 @@ async function main(config = {}) { baseRef: `origin/${baseBranch}`, cwd: process.cwd(), signedCommits, + resolvedTemporaryIds, + currentRepo: itemRepo, }); core.info("Changes pushed to branch (from bundle)"); @@ -1537,6 +1539,8 @@ async function main(config = {}) { baseRef: `origin/${baseBranch}`, cwd: process.cwd(), signedCommits, + resolvedTemporaryIds, + currentRepo: itemRepo, }); core.info("Changes pushed to branch after bundle rewrite retry"); @@ -1765,6 +1769,8 @@ gh pr create --title '${title}' --base ${baseBranch} --head ${branchName} --repo baseRef: `origin/${baseBranch}`, cwd: process.cwd(), signedCommits, + resolvedTemporaryIds, + currentRepo: itemRepo, }); core.info("Changes pushed to branch"); @@ -1911,6 +1917,8 @@ ${patchPreview}`; baseRef: `origin/${baseBranch}`, cwd: process.cwd(), signedCommits, + resolvedTemporaryIds, + currentRepo: itemRepo, }); core.info("Empty branch pushed successfully"); diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 31643b5d842..58f2a822f4c 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -8,6 +8,7 @@ */ const { ERR_API } = require("./error_codes.cjs"); +const { normalizeTemporaryId, replaceTemporaryIdReferencesInPatch } = require("./temporary_id.cjs"); /** Sentinel error class used to signal that the commit range contains a shape * that the GitHub GraphQL `createCommitOnBranch` mutation cannot represent @@ -124,6 +125,71 @@ async function readBlobAsBase64(blobHash, cwd) { return Buffer.concat(chunks).toString("base64"); } +/** + * Build a normalized temporary ID map from resolved temporary IDs. + * + * @param {Record | null | undefined} resolvedTemporaryIds + * @param {string} currentRepo + * @returns {Map} + */ +function buildTemporaryIdMap(resolvedTemporaryIds, currentRepo) { + /** @type {Map} */ + const map = new Map(); + if (!resolvedTemporaryIds || typeof resolvedTemporaryIds !== "object") { + return map; + } + for (const [key, value] of Object.entries(resolvedTemporaryIds)) { + const normalizedKey = normalizeTemporaryId(key); + if (typeof value === "number") { + map.set(normalizedKey, { repo: currentRepo, number: value }); + continue; + } + if (value && typeof value === "object" && "number" in value) { + map.set(normalizedKey, { + repo: String("repo" in value && value.repo ? value.repo : currentRepo), + number: Number(value.number), + }); + } + } + return map; +} + +/** + * Replace temporary ID references in base64-encoded UTF-8 text content. + * Leaves content unchanged for binary blobs, non-text blobs, or when there are no matches. + * + * @param {string} base64Content + * @param {Map} temporaryIdMap + * @param {string} currentRepo + * @param {string} filePath + * @returns {string} + */ +function maybeReplaceTemporaryIdsInBase64Content(base64Content, temporaryIdMap, currentRepo, filePath) { + if (!(temporaryIdMap instanceof Map) || temporaryIdMap.size === 0) { + return base64Content; + } + + const rawBytes = Buffer.from(base64Content, "base64"); + const utf8Text = rawBytes.toString("utf8"); + + // Treat only clean UTF-8 round-trippable content as text. + if (!Buffer.from(utf8Text, "utf8").equals(rawBytes)) { + return base64Content; + } + + if (!/#aw_[A-Za-z0-9_]{3,12}\b/i.test(utf8Text)) { + return base64Content; + } + + const replaced = replaceTemporaryIdReferencesInPatch(utf8Text, temporaryIdMap, currentRepo); + if (replaced === utf8Text) { + return base64Content; + } + + core.info(`pushSignedCommits: resolved temporary ID references in file content: ${filePath}`); + return Buffer.from(replaced, "utf8").toString("base64"); +} + /** * Push the local branch to origin using git directly and return the local HEAD * SHA after the push succeeds. @@ -167,9 +233,14 @@ async function resolveLocalHeadSha(cwd) { * @param {string} opts.cwd - Working directory of the local git checkout * @param {object} [opts.gitAuthEnv] - Environment variables for git push fallback auth * @param {boolean} [opts.signedCommits=true] - When false, skip GraphQL signed commits and use git push directly + * @param {Record} [opts.resolvedTemporaryIds] - Resolved temporary IDs map + * @param {string} [opts.currentRepo] - Repository slug used for same-repo temporary ID resolution * @returns {Promise} SHA of the commit that landed on the target branch */ -async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, cwd, gitAuthEnv, signedCommits = true }) { +async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, cwd, gitAuthEnv, signedCommits = true, resolvedTemporaryIds, currentRepo }) { + const effectiveCurrentRepo = currentRepo || `${owner}/${repo}`; + const temporaryIdMap = buildTemporaryIdMap(resolvedTemporaryIds, effectiveCurrentRepo); + // The default parameter value converts undefined to true; this check tests only the explicit false value. if (signedCommits === false) { core.info(`pushSignedCommits: signed-commits disabled (using direct git push) for branch ${branch}`); @@ -303,7 +374,8 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c if (dstMode === "100755") { core.warning(`pushSignedCommits: executable bit on ${renamedPath} will be lost in signed commit (GitHub GraphQL does not support mode 100755)`); } - additions.push({ path: renamedPath, contents: await readBlobAsBase64(dstHash, cwd) }); + const blobContents = await readBlobAsBase64(dstHash, cwd); + additions.push({ path: renamedPath, contents: maybeReplaceTemporaryIdsInBase64Content(blobContents, temporaryIdMap, effectiveCurrentRepo, renamedPath) }); } else if (status && status.startsWith("C")) { // Copy: source path is kept (no deletion), only the destination path is added const copiedPath = unquoteCPath(paths[1]); @@ -322,7 +394,8 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c if (dstMode === "100755") { core.warning(`pushSignedCommits: executable bit on ${copiedPath} will be lost in signed commit (GitHub GraphQL does not support mode 100755)`); } - additions.push({ path: copiedPath, contents: await readBlobAsBase64(dstHash, cwd) }); + const blobContents = await readBlobAsBase64(dstHash, cwd); + additions.push({ path: copiedPath, contents: maybeReplaceTemporaryIdsInBase64Content(blobContents, temporaryIdMap, effectiveCurrentRepo, copiedPath) }); } else { // Added or Modified if (dstMode === "160000") { @@ -336,7 +409,8 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c if (dstMode === "100755") { core.warning(`pushSignedCommits: executable bit on ${filePath} will be lost in signed commit (GitHub GraphQL does not support mode 100755)`); } - additions.push({ path: filePath, contents: await readBlobAsBase64(dstHash, cwd) }); + const blobContents = await readBlobAsBase64(dstHash, cwd); + additions.push({ path: filePath, contents: maybeReplaceTemporaryIdsInBase64Content(blobContents, temporaryIdMap, effectiveCurrentRepo, filePath) }); } } diff --git a/actions/setup/js/push_signed_commits.test.cjs b/actions/setup/js/push_signed_commits.test.cjs index f07d65bd27b..c2f70e1c7cd 100644 --- a/actions/setup/js/push_signed_commits.test.cjs +++ b/actions/setup/js/push_signed_commits.test.cjs @@ -307,6 +307,38 @@ describe("push_signed_commits integration tests", () => { expect(Buffer.from(variables.input.fileChanges.additions[0].contents, "base64").toString()).toBe("Hello World\n"); }); + it("should resolve temporary ID references in text file contents before GraphQL replay", async () => { + execGit(["checkout", "-b", "temp-id-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "quarantine.cs"), '[QuarantinedTest("https://github.com/test-owner/test-repo/issues/#aw_test1")]\n// linked: #aw_test1\n'); + execGit(["add", "quarantine.cs"], { cwd: workDir }); + execGit(["commit", "-m", "Add quarantine reference"], { cwd: workDir }); + execGit(["push", "-u", "origin", "temp-id-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "temp-id-branch", + baseRef: "origin/main", + cwd: workDir, + resolvedTemporaryIds: { + aw_test1: { repo: "test-owner/test-repo", number: 66708 }, + }, + currentRepo: "test-owner/test-repo", + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(1); + const additions = githubClient.graphql.mock.calls[0][1].input.fileChanges.additions; + expect(additions).toHaveLength(1); + const resolvedContent = Buffer.from(additions[0].contents, "base64").toString(); + expect(resolvedContent).toContain("https://github.com/test-owner/test-repo/issues/66708"); + expect(resolvedContent).toContain("#66708"); + expect(resolvedContent).not.toContain("#aw_test1"); + }); + it("should call GraphQL once per commit for multiple new commits", async () => { execGit(["checkout", "-b", "multi-commit-branch"], { cwd: workDir }); diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index c4a71de9aa0..af649afc76b 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -921,6 +921,8 @@ async function main(config = {}) { cwd: repoCwd || process.cwd(), gitAuthEnv, signedCommits, + resolvedTemporaryIds, + currentRepo: itemRepo, }); if (pushedSha) { pushedCommitSha = pushedSha; From 2dc59f3acea8f01640e77f0a42d488c254b69ec2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:58:26 +0000 Subject: [PATCH 04/12] chore: polish temporary ID replay handling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/push_signed_commits.cjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 58f2a822f4c..319d808dae3 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -9,6 +9,7 @@ const { ERR_API } = require("./error_codes.cjs"); const { normalizeTemporaryId, replaceTemporaryIdReferencesInPatch } = require("./temporary_id.cjs"); +const TEMPORARY_ID_REFERENCE_PATTERN = /#aw_[A-Za-z0-9_]{3,12}\b/i; /** Sentinel error class used to signal that the commit range contains a shape * that the GitHub GraphQL `createCommitOnBranch` mutation cannot represent @@ -135,7 +136,7 @@ async function readBlobAsBase64(blobHash, cwd) { function buildTemporaryIdMap(resolvedTemporaryIds, currentRepo) { /** @type {Map} */ const map = new Map(); - if (!resolvedTemporaryIds || typeof resolvedTemporaryIds !== "object") { + if (!resolvedTemporaryIds || typeof resolvedTemporaryIds !== "object" || Array.isArray(resolvedTemporaryIds)) { return map; } for (const [key, value] of Object.entries(resolvedTemporaryIds)) { @@ -156,7 +157,10 @@ function buildTemporaryIdMap(resolvedTemporaryIds, currentRepo) { /** * Replace temporary ID references in base64-encoded UTF-8 text content. - * Leaves content unchanged for binary blobs, non-text blobs, or when there are no matches. + * Returns original content unchanged for: + * - binary / non-UTF8 blobs + * - UTF-8 text with no temporary ID matches + * Returns rewritten base64 content when UTF-8 text contains resolvable temporary IDs. * * @param {string} base64Content * @param {Map} temporaryIdMap @@ -177,7 +181,7 @@ function maybeReplaceTemporaryIdsInBase64Content(base64Content, temporaryIdMap, return base64Content; } - if (!/#aw_[A-Za-z0-9_]{3,12}\b/i.test(utf8Text)) { + if (!TEMPORARY_ID_REFERENCE_PATTERN.test(utf8Text)) { return base64Content; } From 2b491aad25eeae06e1c1621b93af85804320f43c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 00:58:49 +0000 Subject: [PATCH 05/12] chore: revert unrelated workflow lock updates Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/mcp-inspector.lock.yml | 2 +- .github/workflows/smoke-otel-backends.lock.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index ad1d4a64f61..a6c476a93e1 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -996,7 +996,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", + "DD_APPLICATION_KEY": "\${DD_APP_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ diff --git a/.github/workflows/smoke-otel-backends.lock.yml b/.github/workflows/smoke-otel-backends.lock.yml index 59f8342c061..878ea3a0c79 100644 --- a/.github/workflows/smoke-otel-backends.lock.yml +++ b/.github/workflows/smoke-otel-backends.lock.yml @@ -769,7 +769,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", + "DD_APPLICATION_KEY": "\${DD_APP_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ From 78af32a281c5b27b2c3230489b4a898832a7d1a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 01:45:44 +0000 Subject: [PATCH 06/12] chore: outline review-followup plan Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/mcp-inspector.lock.yml | 2 +- .github/workflows/smoke-otel-backends.lock.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index a6c476a93e1..ad1d4a64f61 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -996,7 +996,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APP_KEY}", + "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ diff --git a/.github/workflows/smoke-otel-backends.lock.yml b/.github/workflows/smoke-otel-backends.lock.yml index 878ea3a0c79..59f8342c061 100644 --- a/.github/workflows/smoke-otel-backends.lock.yml +++ b/.github/workflows/smoke-otel-backends.lock.yml @@ -769,7 +769,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APP_KEY}", + "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ From 4c7dfa5fbd1e2bed9f6fb57a4f3737412718e2a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 01:48:26 +0000 Subject: [PATCH 07/12] fix: address signed replay temporary ID review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/push_signed_commits.cjs | 31 ++++++++-- actions/setup/js/push_signed_commits.test.cjs | 61 +++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 319d808dae3..2e791155012 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -9,7 +9,7 @@ const { ERR_API } = require("./error_codes.cjs"); const { normalizeTemporaryId, replaceTemporaryIdReferencesInPatch } = require("./temporary_id.cjs"); -const TEMPORARY_ID_REFERENCE_PATTERN = /#aw_[A-Za-z0-9_]{3,12}\b/i; +const TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN = /#aw_/i; /** Sentinel error class used to signal that the commit range contains a shape * that the GitHub GraphQL `createCommitOnBranch` mutation cannot represent @@ -139,16 +139,39 @@ function buildTemporaryIdMap(resolvedTemporaryIds, currentRepo) { if (!resolvedTemporaryIds || typeof resolvedTemporaryIds !== "object" || Array.isArray(resolvedTemporaryIds)) { return map; } + + /** + * @param {unknown} raw + * @returns {number | null} + */ + const toPositiveInteger = raw => { + const num = Number(raw); + if (!Number.isInteger(num) || num < 1) { + return null; + } + return num; + }; + for (const [key, value] of Object.entries(resolvedTemporaryIds)) { const normalizedKey = normalizeTemporaryId(key); if (typeof value === "number") { - map.set(normalizedKey, { repo: currentRepo, number: value }); + const number = toPositiveInteger(value); + if (number === null) { + core.warning(`pushSignedCommits: ignoring invalid resolved temporary ID number for '${normalizedKey}': ${String(value)}`); + continue; + } + map.set(normalizedKey, { repo: currentRepo, number }); continue; } if (value && typeof value === "object" && "number" in value) { + const number = toPositiveInteger(value.number); + if (number === null) { + core.warning(`pushSignedCommits: ignoring invalid resolved temporary ID number for '${normalizedKey}': ${String(value.number)}`); + continue; + } map.set(normalizedKey, { repo: String("repo" in value && value.repo ? value.repo : currentRepo), - number: Number(value.number), + number, }); } } @@ -181,7 +204,7 @@ function maybeReplaceTemporaryIdsInBase64Content(base64Content, temporaryIdMap, return base64Content; } - if (!TEMPORARY_ID_REFERENCE_PATTERN.test(utf8Text)) { + if (!TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN.test(utf8Text)) { return base64Content; } diff --git a/actions/setup/js/push_signed_commits.test.cjs b/actions/setup/js/push_signed_commits.test.cjs index c2f70e1c7cd..0edea1900c9 100644 --- a/actions/setup/js/push_signed_commits.test.cjs +++ b/actions/setup/js/push_signed_commits.test.cjs @@ -339,6 +339,67 @@ describe("push_signed_commits integration tests", () => { expect(resolvedContent).not.toContain("#aw_test1"); }); + it("should still run replacement logic for malformed temporary ID candidates and emit warning", async () => { + execGit(["checkout", "-b", "temp-id-malformed-candidate-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "quarantine.cs"), "// malformed link: #aw_test-id\n"); + execGit(["add", "quarantine.cs"], { cwd: workDir }); + execGit(["commit", "-m", "Add malformed temporary ID reference"], { cwd: workDir }); + execGit(["push", "-u", "origin", "temp-id-malformed-candidate-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "temp-id-malformed-candidate-branch", + baseRef: "origin/main", + cwd: workDir, + resolvedTemporaryIds: { + aw_test: { repo: "test-owner/test-repo", number: 66708 }, + }, + currentRepo: "test-owner/test-repo", + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(1); + const additions = githubClient.graphql.mock.calls[0][1].input.fileChanges.additions; + const replayedContent = Buffer.from(additions[0].contents, "base64").toString(); + expect(replayedContent).toContain("#66708-id"); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Malformed temporary ID reference '#aw_test-id'")); + }); + + it("should ignore invalid resolved temporary ID numbers instead of replacing with NaN", async () => { + execGit(["checkout", "-b", "temp-id-invalid-number-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "quarantine.cs"), "// linked: #aw_test2\n"); + execGit(["add", "quarantine.cs"], { cwd: workDir }); + execGit(["commit", "-m", "Add temporary ID with invalid map entry"], { cwd: workDir }); + execGit(["push", "-u", "origin", "temp-id-invalid-number-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "temp-id-invalid-number-branch", + baseRef: "origin/main", + cwd: workDir, + resolvedTemporaryIds: { + aw_test2: { repo: "test-owner/test-repo", number: "not-a-number" }, + }, + currentRepo: "test-owner/test-repo", + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(1); + const additions = githubClient.graphql.mock.calls[0][1].input.fileChanges.additions; + const replayedContent = Buffer.from(additions[0].contents, "base64").toString(); + expect(replayedContent).toContain("#aw_test2"); + expect(replayedContent).not.toContain("#NaN"); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("ignoring invalid resolved temporary ID number for 'aw_test2'")); + }); + it("should call GraphQL once per commit for multiple new commits", async () => { execGit(["checkout", "-b", "multi-commit-branch"], { cwd: workDir }); From 86b539e786a30b17c366b1241988999aed650d25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 02:33:20 +0000 Subject: [PATCH 08/12] refactor: share temp-id candidate precheck pattern Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/push_signed_commits.cjs | 3 +-- actions/setup/js/temporary_id.cjs | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 2e791155012..2da9e8e6a57 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -8,8 +8,7 @@ */ const { ERR_API } = require("./error_codes.cjs"); -const { normalizeTemporaryId, replaceTemporaryIdReferencesInPatch } = require("./temporary_id.cjs"); -const TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN = /#aw_/i; +const { normalizeTemporaryId, replaceTemporaryIdReferencesInPatch, TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN } = require("./temporary_id.cjs"); /** Sentinel error class used to signal that the commit range contains a shape * that the GitHub GraphQL `createCommitOnBranch` mutation cannot represent diff --git a/actions/setup/js/temporary_id.cjs b/actions/setup/js/temporary_id.cjs index 04a517093a7..18b36079348 100644 --- a/actions/setup/js/temporary_id.cjs +++ b/actions/setup/js/temporary_id.cjs @@ -37,6 +37,12 @@ const TEMPORARY_ID_PATTERN = /#(aw_[A-Za-z0-9_]{3,12})\b/gi; */ const TEMPORARY_ID_CANDIDATE_PATTERN = /#aw_([A-Za-z0-9_-]+)/gi; +/** + * Regex pattern for quick candidate detection of temporary ID references. + * Non-global on purpose so repeated `.test()` calls are state-free. + */ +const TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN = /#aw_/i; + /** * @typedef {Object} RepoIssuePair * @property {string} repo - Repository slug in "owner/repo" format @@ -673,6 +679,7 @@ function resolveNumberFromTemporaryId(value, resolvedTemporaryIds) { module.exports = { TEMPORARY_ID_PATTERN, TEMPORARY_ID_CANDIDATE_PATTERN, + TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN, generateTemporaryId, isTemporaryId, normalizeTemporaryId, From ccca10aac32fb375bc99bccea55df05ab371caf4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 02:58:31 +0000 Subject: [PATCH 09/12] refactor: reuse temporary ID map loader in signed replay Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/push_signed_commits.cjs | 62 +++--------------------- actions/setup/js/temporary_id.cjs | 45 +++++++++++++++-- 2 files changed, 48 insertions(+), 59 deletions(-) diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index 2da9e8e6a57..ed6168544a7 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -8,7 +8,7 @@ */ const { ERR_API } = require("./error_codes.cjs"); -const { normalizeTemporaryId, replaceTemporaryIdReferencesInPatch, TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN } = require("./temporary_id.cjs"); +const { loadTemporaryIdMapFromResolved, replaceTemporaryIdReferencesInPatch, TEMPORARY_ID_CANDIDATE_REFERENCE_PATTERN } = require("./temporary_id.cjs"); /** Sentinel error class used to signal that the commit range contains a shape * that the GitHub GraphQL `createCommitOnBranch` mutation cannot represent @@ -125,58 +125,6 @@ async function readBlobAsBase64(blobHash, cwd) { return Buffer.concat(chunks).toString("base64"); } -/** - * Build a normalized temporary ID map from resolved temporary IDs. - * - * @param {Record | null | undefined} resolvedTemporaryIds - * @param {string} currentRepo - * @returns {Map} - */ -function buildTemporaryIdMap(resolvedTemporaryIds, currentRepo) { - /** @type {Map} */ - const map = new Map(); - if (!resolvedTemporaryIds || typeof resolvedTemporaryIds !== "object" || Array.isArray(resolvedTemporaryIds)) { - return map; - } - - /** - * @param {unknown} raw - * @returns {number | null} - */ - const toPositiveInteger = raw => { - const num = Number(raw); - if (!Number.isInteger(num) || num < 1) { - return null; - } - return num; - }; - - for (const [key, value] of Object.entries(resolvedTemporaryIds)) { - const normalizedKey = normalizeTemporaryId(key); - if (typeof value === "number") { - const number = toPositiveInteger(value); - if (number === null) { - core.warning(`pushSignedCommits: ignoring invalid resolved temporary ID number for '${normalizedKey}': ${String(value)}`); - continue; - } - map.set(normalizedKey, { repo: currentRepo, number }); - continue; - } - if (value && typeof value === "object" && "number" in value) { - const number = toPositiveInteger(value.number); - if (number === null) { - core.warning(`pushSignedCommits: ignoring invalid resolved temporary ID number for '${normalizedKey}': ${String(value.number)}`); - continue; - } - map.set(normalizedKey, { - repo: String("repo" in value && value.repo ? value.repo : currentRepo), - number, - }); - } - } - return map; -} - /** * Replace temporary ID references in base64-encoded UTF-8 text content. * Returns original content unchanged for: @@ -265,7 +213,13 @@ async function resolveLocalHeadSha(cwd) { */ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, cwd, gitAuthEnv, signedCommits = true, resolvedTemporaryIds, currentRepo }) { const effectiveCurrentRepo = currentRepo || `${owner}/${repo}`; - const temporaryIdMap = buildTemporaryIdMap(resolvedTemporaryIds, effectiveCurrentRepo); + const temporaryIdMap = loadTemporaryIdMapFromResolved(resolvedTemporaryIds, { + defaultRepo: effectiveCurrentRepo, + validatePositiveIntegers: true, + onInvalidNumber: (normalizedKey, rawValue) => { + core.warning(`pushSignedCommits: ignoring invalid resolved temporary ID number for '${normalizedKey}': ${String(rawValue)}`); + }, + }); // The default parameter value converts undefined to true; this check tests only the explicit false value. if (signedCommits === false) { diff --git a/actions/setup/js/temporary_id.cjs b/actions/setup/js/temporary_id.cjs index 18b36079348..1e66210a0e9 100644 --- a/actions/setup/js/temporary_id.cjs +++ b/actions/setup/js/temporary_id.cjs @@ -262,9 +262,13 @@ function loadTemporaryIdMap() { * - { repo, number } * * @param {any} resolvedTemporaryIds - Object or Map of temporary IDs to resolved values + * @param {object} [options] + * @param {string} [options.defaultRepo] - Fallback repo to use for legacy number-only values + * @param {boolean} [options.validatePositiveIntegers] - When true, ignore non-positive-integer numbers + * @param {(normalizedKey: string, rawValue: unknown) => void} [options.onInvalidNumber] - Callback for invalid numbers when validation is enabled * @returns {Map} Map of normalized temporary_id to {repo, number} */ -function loadTemporaryIdMapFromResolved(resolvedTemporaryIds) { +function loadTemporaryIdMapFromResolved(resolvedTemporaryIds, options = {}) { /** @type {Map} */ const result = new Map(); @@ -272,22 +276,53 @@ function loadTemporaryIdMapFromResolved(resolvedTemporaryIds) { return result; } - const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : ""; + const contextRepo = options.defaultRepo ?? (typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : ""); + + /** + * @param {string} normalizedKey + * @param {unknown} rawValue + * @returns {number | null} + */ + const toNumber = (normalizedKey, rawValue) => { + const number = Number(rawValue); + if (!options.validatePositiveIntegers) { + return number; + } + if (!Number.isInteger(number) || number < 1) { + if (typeof options.onInvalidNumber === "function") { + options.onInvalidNumber(normalizedKey, rawValue); + } + return null; + } + return number; + }; const entries = resolvedTemporaryIds instanceof Map ? Array.from(resolvedTemporaryIds.entries()) : Object.entries(resolvedTemporaryIds); for (const [key, value] of entries) { const normalizedKey = normalizeTemporaryId(key); if (typeof value === "number") { - result.set(normalizedKey, { repo: contextRepo, number: value }); + const number = toNumber(normalizedKey, value); + if (number === null) { + continue; + } + result.set(normalizedKey, { repo: contextRepo, number }); continue; } if (typeof value === "object" && value !== null) { if ("repo" in value && "number" in value) { - result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) }); + const number = toNumber(normalizedKey, value.number); + if (number === null) { + continue; + } + result.set(normalizedKey, { repo: String(value.repo), number }); continue; } if ("number" in value) { - result.set(normalizedKey, { repo: contextRepo, number: Number(value.number) }); + const number = toNumber(normalizedKey, value.number); + if (number === null) { + continue; + } + result.set(normalizedKey, { repo: contextRepo, number }); continue; } } From 5b4db7679d698414631ffbe2eb830702c2d7a415 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 02:59:30 +0000 Subject: [PATCH 10/12] fix: harden shared temporary ID map number parsing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/temporary_id.cjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/temporary_id.cjs b/actions/setup/js/temporary_id.cjs index 1e66210a0e9..ae7392d799a 100644 --- a/actions/setup/js/temporary_id.cjs +++ b/actions/setup/js/temporary_id.cjs @@ -263,7 +263,7 @@ function loadTemporaryIdMap() { * * @param {any} resolvedTemporaryIds - Object or Map of temporary IDs to resolved values * @param {object} [options] - * @param {string} [options.defaultRepo] - Fallback repo to use for legacy number-only values + * @param {string} [options.defaultRepo] - Fallback repo for legacy number-only values; if omitted, uses GitHub Action context repo when available, else "" * @param {boolean} [options.validatePositiveIntegers] - When true, ignore non-positive-integer numbers * @param {(normalizedKey: string, rawValue: unknown) => void} [options.onInvalidNumber] - Callback for invalid numbers when validation is enabled * @returns {Map} Map of normalized temporary_id to {repo, number} @@ -285,6 +285,12 @@ function loadTemporaryIdMapFromResolved(resolvedTemporaryIds, options = {}) { */ const toNumber = (normalizedKey, rawValue) => { const number = Number(rawValue); + if (!Number.isFinite(number)) { + if (options.validatePositiveIntegers && typeof options.onInvalidNumber === "function") { + options.onInvalidNumber(normalizedKey, rawValue); + } + return null; + } if (!options.validatePositiveIntegers) { return number; } From a14129366dc83390b056e0de9ec083e1ddcd0aca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 03:00:16 +0000 Subject: [PATCH 11/12] docs: clarify invalid-number callback semantics Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/temporary_id.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/temporary_id.cjs b/actions/setup/js/temporary_id.cjs index ae7392d799a..4be06a392da 100644 --- a/actions/setup/js/temporary_id.cjs +++ b/actions/setup/js/temporary_id.cjs @@ -265,7 +265,7 @@ function loadTemporaryIdMap() { * @param {object} [options] * @param {string} [options.defaultRepo] - Fallback repo for legacy number-only values; if omitted, uses GitHub Action context repo when available, else "" * @param {boolean} [options.validatePositiveIntegers] - When true, ignore non-positive-integer numbers - * @param {(normalizedKey: string, rawValue: unknown) => void} [options.onInvalidNumber] - Callback for invalid numbers when validation is enabled + * @param {(normalizedKey: string, rawValue: unknown) => void} [options.onInvalidNumber] - Callback invoked when a value is skipped for invalid number parsing * @returns {Map} Map of normalized temporary_id to {repo, number} */ function loadTemporaryIdMapFromResolved(resolvedTemporaryIds, options = {}) { @@ -286,7 +286,7 @@ function loadTemporaryIdMapFromResolved(resolvedTemporaryIds, options = {}) { const toNumber = (normalizedKey, rawValue) => { const number = Number(rawValue); if (!Number.isFinite(number)) { - if (options.validatePositiveIntegers && typeof options.onInvalidNumber === "function") { + if (typeof options.onInvalidNumber === "function") { options.onInvalidNumber(normalizedKey, rawValue); } return null; From c0dd5d2d682266152784352d479086476ab60a79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 03:00:54 +0000 Subject: [PATCH 12/12] docs: clarify temporary ID loader option behavior Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/temporary_id.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/temporary_id.cjs b/actions/setup/js/temporary_id.cjs index 4be06a392da..4fb10083e67 100644 --- a/actions/setup/js/temporary_id.cjs +++ b/actions/setup/js/temporary_id.cjs @@ -263,9 +263,9 @@ function loadTemporaryIdMap() { * * @param {any} resolvedTemporaryIds - Object or Map of temporary IDs to resolved values * @param {object} [options] - * @param {string} [options.defaultRepo] - Fallback repo for legacy number-only values; if omitted, uses GitHub Action context repo when available, else "" + * @param {string} [options.defaultRepo] - Fallback repo for legacy number-only values; when null/undefined, uses GitHub Action context repo when available, else "" * @param {boolean} [options.validatePositiveIntegers] - When true, ignore non-positive-integer numbers - * @param {(normalizedKey: string, rawValue: unknown) => void} [options.onInvalidNumber] - Callback invoked when a value is skipped for invalid number parsing + * @param {(normalizedKey: string, rawValue: unknown) => void} [options.onInvalidNumber] - Callback invoked when a value is skipped for non-finite parsing, or for non-positive/non-integer values when `validatePositiveIntegers` is true * @returns {Map} Map of normalized temporary_id to {repo, number} */ function loadTemporaryIdMapFromResolved(resolvedTemporaryIds, options = {}) {