From 4b291f9cde34527cacff7b580d38255e5687b70f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:38:34 +0000 Subject: [PATCH 1/5] Initial plan From 0e0ed15616d4828e6360cf67c950132942e69dbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:02:08 +0000 Subject: [PATCH 2/5] fix: guard against string-typed fields in update_project handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When output.fields is a JSON-encoded string (agent double-encoding), the old code iterated over character indices and created bogus SINGLE_SELECT project fields named 0, 1, 2, … Add resolveFieldsObject() which: - Parses a string value via JSON.parse, warns and returns null on failure - Explicitly handles null after parsing (e.g. JSON 'null') - Rejects arrays and other non-object types with a warning Both call sites in updateProject now use this helper. Fixes #44845 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/update_project.cjs | 40 ++++++++++-- actions/setup/js/update_project.test.cjs | 80 ++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 0752f951fc1..56f47d5547c 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -441,6 +441,36 @@ function inferFieldDataType(fieldName, fieldValue, datePattern) { return "SINGLE_SELECT"; } +/** + * Coerce an agent-supplied `fields` value to a plain object, or return null. + * + * Agents occasionally double-encode the value as a JSON string. If that + * happens we parse it transparently. Arrays, null, and other non-object + * types are rejected with a warning so that callers can skip field updates + * without silently iterating over character indices or array elements. + * + * @param {unknown} fields - Raw value from the agent output + * @returns {Record|null} Plain object or null when unusable + */ +function resolveFieldsObject(fields) { + if (fields == null) return null; + if (typeof fields === "string") { + try { + fields = JSON.parse(fields); + } catch { + core.warning("update_project: `fields` was a string and could not be parsed as JSON; skipping field updates"); + return null; + } + // JSON.parse of "null", "1", or a quoted string yields a non-object + if (fields == null) return null; + } + if (Array.isArray(fields) || typeof fields !== "object") { + core.warning("update_project: `fields` must be a JSON object; skipping field updates"); + return null; + } + return /** @type {Record} */ fields; +} + /** * Apply field value updates to a project item, creating fields as needed. * @param {Object} github - GitHub client (Octokit instance) @@ -1105,8 +1135,9 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = } } - if (output.fields && Object.keys(output.fields).length > 0) { - await applyFieldUpdates(github, projectId, itemId, output.fields); + const resolvedFields1 = resolveFieldsObject(output.fields); + if (resolvedFields1 && Object.keys(resolvedFields1).length > 0) { + await applyFieldUpdates(github, projectId, itemId, resolvedFields1); } core.setOutput("item-id", itemId); @@ -1195,8 +1226,9 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = ).addProjectV2ItemById.item.id; } - if (output.fields && Object.keys(output.fields).length > 0) { - await applyFieldUpdates(github, projectId, itemId, output.fields); + const resolvedFields2 = resolveFieldsObject(output.fields); + if (resolvedFields2 && Object.keys(resolvedFields2).length > 0) { + await applyFieldUpdates(github, projectId, itemId, resolvedFields2); } core.setOutput("item-id", itemId); diff --git a/actions/setup/js/update_project.test.cjs b/actions/setup/js/update_project.test.cjs index 7c240a538d2..14607fa89a8 100644 --- a/actions/setup/js/update_project.test.cjs +++ b/actions/setup/js/update_project.test.cjs @@ -1230,6 +1230,86 @@ describe("updateProject", () => { expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to create field "NonExistentField"')); }); + it("warns and skips field updates when fields is a JSON-encoded string (double-encoded)", async () => { + const projectUrl = "https://github.com/orgs/testowner/projects/60"; + const output = { + type: "update_project", + project: projectUrl, + content_type: "issue", + content_number: 21, + // Agent double-encoded the fields object as a JSON string + fields: '{"Lifecycle":"Sandbox"}', + }; + + queueResponses([ + repoResponse(), + viewerResponse(), + orgProjectV2Response(projectUrl, 60, "project-test"), + issueResponse("issue-id-21"), + existingItemResponse("issue-id-21", "item-test"), + fieldsResponse([{ id: "field-lifecycle", name: "Lifecycle", options: [{ id: "opt-sandbox", name: "Sandbox" }] }]), + updateFieldValueResponse(), + ]); + + await updateProject(output); + + // Should NOT create bogus numeric fields (0, 1, 2, …) + const createFieldCall = mockGithub.graphql.mock.calls.find(([query]) => query.includes("createProjectV2Field")); + expect(createFieldCall).toBeUndefined(); + + // Should have applied the parsed field update + const updateCall = mockGithub.graphql.mock.calls.find(([query]) => query.includes("updateProjectV2ItemFieldValue")); + expect(updateCall).toBeDefined(); + }); + + it("warns and skips field updates when fields is an invalid JSON string", async () => { + const projectUrl = "https://github.com/orgs/testowner/projects/60"; + const output = { + type: "update_project", + project: projectUrl, + content_type: "issue", + content_number: 22, + fields: "not-valid-json", + }; + + queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project-test"), issueResponse("issue-id-22"), existingItemResponse("issue-id-22", "item-test")]); + + await updateProject(output); + + // Should warn about the bad string value + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("`fields` was a string and could not be parsed as JSON")); + + // Should NOT attempt any field GraphQL operations + const createFieldCall = mockGithub.graphql.mock.calls.find(([query]) => query.includes("createProjectV2Field")); + expect(createFieldCall).toBeUndefined(); + const updateCall = mockGithub.graphql.mock.calls.find(([query]) => query.includes("updateProjectV2ItemFieldValue")); + expect(updateCall).toBeUndefined(); + }); + + it("warns and skips field updates when fields is an array", async () => { + const projectUrl = "https://github.com/orgs/testowner/projects/60"; + const output = { + type: "update_project", + project: projectUrl, + content_type: "issue", + content_number: 23, + fields: ["Status", "In Progress"], + }; + + queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project-test"), issueResponse("issue-id-23"), existingItemResponse("issue-id-23", "item-test")]); + + await updateProject(output); + + // Should warn about the non-object value + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("`fields` must be a JSON object")); + + // Should NOT attempt any field GraphQL operations + const createFieldCall = mockGithub.graphql.mock.calls.find(([query]) => query.includes("createProjectV2Field")); + expect(createFieldCall).toBeUndefined(); + const updateCall = mockGithub.graphql.mock.calls.find(([query]) => query.includes("updateProjectV2ItemFieldValue")); + expect(updateCall).toBeUndefined(); + }); + it("rejects non-URL project identifier", async () => { const output = { type: "update_project", project: "Engineering Roadmap" }; await expect(updateProject(output)).rejects.toThrow(/full GitHub project URL/); From 742360c378ea5570e642f5a44ad3b19088ccccf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:38:48 +0000 Subject: [PATCH 3/5] fix(js): address typecheck and test-name feedback in update_project Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/update_project.cjs | 3 ++- actions/setup/js/update_project.test.cjs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 56f47d5547c..73633585a88 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -468,7 +468,8 @@ function resolveFieldsObject(fields) { core.warning("update_project: `fields` must be a JSON object; skipping field updates"); return null; } - return /** @type {Record} */ fields; + const objectFields = /** @type {Record} */ fields; + return objectFields; } /** diff --git a/actions/setup/js/update_project.test.cjs b/actions/setup/js/update_project.test.cjs index 14607fa89a8..146bcb080e4 100644 --- a/actions/setup/js/update_project.test.cjs +++ b/actions/setup/js/update_project.test.cjs @@ -1230,7 +1230,7 @@ describe("updateProject", () => { expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to create field "NonExistentField"')); }); - it("warns and skips field updates when fields is a JSON-encoded string (double-encoded)", async () => { + it("parses and applies field updates when fields is a JSON-encoded string (double-encoded)", async () => { const projectUrl = "https://github.com/orgs/testowner/projects/60"; const output = { type: "update_project", From a42b76138992a1cb6e20126e00a47eddabfc21f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 04:54:00 +0000 Subject: [PATCH 4/5] fix(update_project): avoid ts-check cast issue in resolveFieldsObject Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/update_project.cjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 73633585a88..4c913a3545c 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -468,8 +468,7 @@ function resolveFieldsObject(fields) { core.warning("update_project: `fields` must be a JSON object; skipping field updates"); return null; } - const objectFields = /** @type {Record} */ fields; - return objectFields; + return { ...fields }; } /** From b2851bbeee85f581e8b266e7cc5298449dd09dae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 04:55:25 +0000 Subject: [PATCH 5/5] fix(update_project): return normalized object for fields type narrowing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/update_project.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 4c913a3545c..784ba9e2e28 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -468,7 +468,7 @@ function resolveFieldsObject(fields) { core.warning("update_project: `fields` must be a JSON object; skipping field updates"); return null; } - return { ...fields }; + return Object.fromEntries(Object.entries(fields)); } /**