From ad3177f2525067de08040448e5e9f05b25a0af08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:56:13 +0000 Subject: [PATCH 1/3] Initial plan From 4f5bc8ed829f0e689fa155be546664152841ee46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:08:06 +0000 Subject: [PATCH 2/3] Implement clear field operation support - Add clearField function using ClearProjectV2ItemFieldValueInput GraphQL mutation - Update getInputs to accept "clear" as valid operation - Update run function to handle clear operation - Add comprehensive tests for clear functionality - Update action.yml and README.md documentation - Maintain backwards compatibility with existing operations Co-authored-by: benbalter <282759+benbalter@users.noreply.github.com> --- README.md | 14 ++++++++-- __test__/main.test.ts | 64 +++++++++++++++++++++++++++++++++++++++++++ action.yml | 2 +- dist/index.js | 44 ++++++++++++++++++++++++++--- src/update-project.ts | 44 +++++++++++++++++++++++++++-- 5 files changed, 159 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 23a3ad3..0d1fa04 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,16 @@ jobs: content_id: ${{ github.event.client_payload.command.resource.id }} field: Status value: ${{ github.event.client_payload.data.status }} + - name: Clear due date + id: clear_due_date + uses: github/update-project-action@v3 + with: + github_token: ${{ secrets.STATUS_UPDATE_TOKEN }} + organization: github + project_number: 1234 + content_id: ${{ github.event.client_payload.command.resource.id }} + field: "Due Date" + operation: clear ``` *Note: The above step can be repeated multiple times in a given job to update multiple fields on the same or different projects.* @@ -62,10 +72,10 @@ The Action is largely feature complete with regards to its initial goals. Find a * `content_id` - The global ID of the issue or pull request within the project * `field` - The field on the project to set the value of * `github_token` - A GitHub Token with access to both the source issue and the destination project (`repo` and `write:org` scopes) -* `operation` - Operation type (update or read) +* `operation` - Operation type (update, read, or clear) * `organization` - The organization that contains the project, defaults to the current repository owner * `project_number` - The project number from the project's URL -* `value` - The value to set the project field to. Only required for operation type read +* `value` - The value to set the project field to. Only required for operation type update ### Outputs diff --git a/__test__/main.test.ts b/__test__/main.test.ts index cdb0672..071ecc6 100644 --- a/__test__/main.test.ts +++ b/__test__/main.test.ts @@ -93,6 +93,12 @@ describe("with environmental variables", () => { expect(result.operation).toEqual("read"); }); + test("getInputs accepts clear", () => { + process.env = { ...process.env, ...{ INPUT_OPERATION: "clear" } }; + const result = updateProject.getInputs(); + expect(result.operation).toEqual("clear"); + }); + test("getInputs doesn't accept other operations", () => { process.env = { ...process.env, ...{ INPUT_OPERATION: "foo" } }; const result = updateProject.getInputs(); @@ -514,6 +520,41 @@ describe("with Octokit setup", () => { expect(mock.done()).toBe(true); }); + test("clearField clears a field", async () => { + const item = { project: { number: 1, owner: { login: "github" } } }; + mockContentMetadata("test", item); + + const field = { + id: 1, + name: "testField", + dataType: "date", + }; + mockProjectMetadata(1, field); + + const data = { data: { projectV2Item: { id: 1 } } }; + mockGraphQL(data, "clearField", "clearProjectV2ItemFieldValue"); + + const projectMetadata = await updateProject.fetchProjectMetadata( + "github", + 1, + "testField", + "", + "clear" + ); + const contentMetadata = await updateProject.fetchContentMetadata( + "1", + "test", + 1, + "github" + ); + const result = await updateProject.clearField( + projectMetadata, + contentMetadata + ); + expect(result).toEqual(data.data); + expect(mock.done()).toBe(true); + }); + test("run updates a field that was not empty", async () => { const item = { field: { value: "testValue" }, @@ -617,4 +658,27 @@ describe("with Octokit setup", () => { await updateProject.run(); expect(mock.done()).toBe(true); }); + + test("run clears a field", async () => { + process.env = { ...OLD_ENV, ...INPUTS, ...{ INPUT_OPERATION: "clear" } }; + + const item = { + field: { value: "2023-01-01" }, + project: { number: 1, owner: { login: "github" } }, + }; + mockContentMetadata("testField", item); + + const field = { + id: 1, + name: "testField", + dataType: "date", + }; + mockProjectMetadata(1, field); + + const data = { data: { projectV2Item: { id: 1 } } }; + mockGraphQL(data, "clearField", "clearProjectV2ItemFieldValue"); + + await updateProject.run(); + expect(mock.done()).toBe(true); + }); }); diff --git a/action.yml b/action.yml index 018051b..4ad786f 100644 --- a/action.yml +++ b/action.yml @@ -9,7 +9,7 @@ inputs: description: The project number from the project's URL required: true operation: - description: Operation type (update or read) + description: Operation type (update, read, or clear) default: update required: false content_id: diff --git a/dist/index.js b/dist/index.js index bf3b290..1f4fd55 100644 --- a/dist/index.js +++ b/dist/index.js @@ -9702,7 +9702,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.run = exports.setupOctokit = exports.getInputs = exports.updateField = exports.convertValueToFieldType = exports.valueGraphqlType = exports.ensureExists = exports.fetchProjectMetadata = exports.fetchContentMetadata = void 0; +exports.run = exports.setupOctokit = exports.getInputs = exports.clearField = exports.updateField = exports.convertValueToFieldType = exports.valueGraphqlType = exports.ensureExists = exports.fetchProjectMetadata = exports.fetchContentMetadata = void 0; const core_1 = __nccwpck_require__(2186); const github_1 = __nccwpck_require__(5438); let octokit; @@ -9887,7 +9887,7 @@ exports.valueGraphqlType = valueGraphqlType; * @returns {string | number} - the converted value */ function convertValueToFieldType(value, fieldType) { - if (fieldType === "number") { + if (fieldType === "NUMBER") { const numValue = parseFloat(value); if (isNaN(numValue)) { throw new Error(`Invalid number value: ${value}`); @@ -9942,6 +9942,37 @@ function updateField(projectMetadata, contentMetadata, value) { }); } exports.updateField = updateField; +/** + * Clears the field value for the content item + * @param {GraphQlQueryResponseData} projectMetadata - The project metadata returned from fetchProjectMetadata() + * @param {GraphQlQueryResponseData} contentMetadata - The content metadata returned from fetchContentMetadata() + * @return {Promise} - The updated content metadata + */ +function clearField(projectMetadata, contentMetadata) { + return __awaiter(this, void 0, void 0, function* () { + const result = yield octokit.graphql(` + mutation($project: ID!, $item: ID!, $field: ID!) { + clearProjectV2ItemFieldValue( + input: { + projectId: $project + itemId: $item + fieldId: $field + } + ) { + projectV2Item { + id + } + } + } + `, { + project: projectMetadata.projectId, + item: contentMetadata.id, + field: projectMetadata.field.fieldId, + }); + return result; + }); +} +exports.clearField = clearField; /** * Returns the validated and normalized inputs for the action * @@ -9951,8 +9982,8 @@ function getInputs() { let operation = (0, core_1.getInput)("operation"); if (operation === "") operation = "update"; - if (!["read", "update"].includes(operation)) { - (0, core_1.setFailed)(`Invalid value passed for the 'operation' parameter (passed: ${operation}, allowed: read, update)`); + if (!["read", "update", "clear"].includes(operation)) { + (0, core_1.setFailed)(`Invalid value passed for the 'operation' parameter (passed: ${operation}, allowed: read, update, clear)`); return {}; } const inputs = { @@ -9998,6 +10029,11 @@ function run() { (0, core_1.setOutput)("field_updated_value", inputs.value); (0, core_1.info)(`Updated field ${inputs.fieldName} on ${contentMetadata.title} to ${inputs.value}`); } + else if (inputs.operation === "clear") { + yield clearField(projectMetadata, contentMetadata); + (0, core_1.setOutput)("field_updated_value", null); + (0, core_1.info)(`Cleared field ${inputs.fieldName} on ${contentMetadata.title}`); + } else { (0, core_1.setOutput)("field_updated_value", (_b = contentMetadata.field) === null || _b === void 0 ? void 0 : _b.value); } diff --git a/src/update-project.ts b/src/update-project.ts index e10a7b1..cdd0d69 100644 --- a/src/update-project.ts +++ b/src/update-project.ts @@ -289,6 +289,42 @@ export async function updateField( return result; } +/** + * Clears the field value for the content item + * @param {GraphQlQueryResponseData} projectMetadata - The project metadata returned from fetchProjectMetadata() + * @param {GraphQlQueryResponseData} contentMetadata - The content metadata returned from fetchContentMetadata() + * @return {Promise} - The updated content metadata + */ +export async function clearField( + projectMetadata: GraphQlQueryResponseData, + contentMetadata: GraphQlQueryResponseData +): Promise { + const result: GraphQlQueryResponseData = await octokit.graphql( + ` + mutation($project: ID!, $item: ID!, $field: ID!) { + clearProjectV2ItemFieldValue( + input: { + projectId: $project + itemId: $item + fieldId: $field + } + ) { + projectV2Item { + id + } + } + } + `, + { + project: projectMetadata.projectId, + item: contentMetadata.id, + field: projectMetadata.field.fieldId, + } + ); + + return result; +} + /** * Returns the validated and normalized inputs for the action * @@ -298,9 +334,9 @@ export function getInputs(): { [key: string]: any } { let operation = getInput("operation"); if (operation === "") operation = "update"; - if (!["read", "update"].includes(operation)) { + if (!["read", "update", "clear"].includes(operation)) { setFailed( - `Invalid value passed for the 'operation' parameter (passed: ${operation}, allowed: read, update)` + `Invalid value passed for the 'operation' parameter (passed: ${operation}, allowed: read, update, clear)` ); return {}; @@ -361,6 +397,10 @@ export async function run(): Promise { info( `Updated field ${inputs.fieldName} on ${contentMetadata.title} to ${inputs.value}` ); + } else if (inputs.operation === "clear") { + await clearField(projectMetadata, contentMetadata); + setOutput("field_updated_value", null); + info(`Cleared field ${inputs.fieldName} on ${contentMetadata.title}`); } else { setOutput("field_updated_value", contentMetadata.field?.value); } From 47d88fd9e4345e82629fd2236712f90ff71ae128 Mon Sep 17 00:00:00 2001 From: Ben Balter Date: Fri, 15 Aug 2025 18:10:50 -0400 Subject: [PATCH 3/3] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d1fa04..77d5122 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ The Action is largely feature complete with regards to its initial goals. Find a * `operation` - Operation type (update, read, or clear) * `organization` - The organization that contains the project, defaults to the current repository owner * `project_number` - The project number from the project's URL -* `value` - The value to set the project field to. Only required for operation type update +* `value` - The value to set the project field to. Only required for operation type `update`; not required for `read` or `clear`. ### Outputs