Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 62 additions & 32 deletions actions/setup/js/update_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -385,11 +385,37 @@ async function updateProject(output) {
.join(" ");
let valueToSet,
field = projectFields.find(f => f.name.toLowerCase() === normalizedFieldName.toLowerCase());

// Detect expected field type based on field name and value heuristics
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
const isDateField = fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date");
const isTextField = "classification" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|"));
let expectedDataType;
if (isDateField && typeof fieldValue === "string" && datePattern.test(fieldValue)) {
expectedDataType = "DATE";
} else if (isTextField) {
expectedDataType = "TEXT";
} else {
expectedDataType = "SINGLE_SELECT";
}

// Check for type mismatch if field already exists
if (field && field.dataType && expectedDataType) {
const actualType = field.dataType;
if (actualType !== expectedDataType) {
core.warning(
`Field type mismatch for "${fieldName}": Expected ${expectedDataType} but found ${actualType}. ` +
`The field was likely created with the wrong type. To fix this, delete the field in the GitHub Projects UI and let it be recreated, ` +
`or manually change the field type if supported.`
);
// Continue anyway - we'll use the existing field type
}
}

if (!field)
if (fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date")) {
// Check if field name suggests it's a date field (e.g., start_date, end_date, due_date)
// Date field values must match ISO 8601 format (YYYY-MM-DD)
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
if (typeof fieldValue === "string" && datePattern.test(fieldValue)) {
try {
field = (
Expand Down Expand Up @@ -458,22 +484,11 @@ async function updateProject(output) {
valueToSet = { iterationId: iteration.id };
} else if (field.options) {
let option = field.options.find(o => o.name === fieldValue);
if (!option)
try {
const allOptions = [...field.options.map(o => ({ name: o.name, description: "", color: o.color || "GRAY" })), { name: String(fieldValue), description: "", color: "GRAY" }],
updatedField = (
await github.graphql(
"mutation($fieldId: ID!, $fieldName: String!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {\n updateProjectV2Field(input: {\n fieldId: $fieldId,\n name: $fieldName,\n singleSelectOptions: $options\n }) {\n projectV2Field {\n ... on ProjectV2SingleSelectField {\n id\n options {\n id\n name\n }\n }\n }\n }\n }",
{ fieldId: field.id, fieldName: field.name, options: allOptions }
)
).updateProjectV2Field.projectV2Field;
((option = updatedField.options.find(o => o.name === fieldValue)), (field = updatedField));
} catch (createError) {
core.warning(`Failed to create option "${fieldValue}": ${getErrorMessage(createError)}`);
continue;
}
if (!option) {
core.warning(`Could not get option ID for "${fieldValue}" in field "${fieldName}"`);
// GitHub's GraphQL API does not support adding new options to existing single-select fields
// The updateProjectV2Field mutation does not exist - users must add options manually via UI
const availableOptions = field.options.map(o => o.name).join(", ");
core.warning(`Option "${fieldValue}" not found in field "${fieldName}". Available options: ${availableOptions}. To add this option, please update the field manually in the GitHub Projects UI.`);
continue;
}
valueToSet = { singleSelectOptionId: option.id };
Expand Down Expand Up @@ -552,11 +567,37 @@ async function updateProject(output) {
.join(" ");
let valueToSet,
field = projectFields.find(f => f.name.toLowerCase() === normalizedFieldName.toLowerCase());

// Detect expected field type based on field name and value heuristics
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
const isDateField = fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date");
const isTextField = "classification" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|"));
let expectedDataType;
if (isDateField && typeof fieldValue === "string" && datePattern.test(fieldValue)) {
expectedDataType = "DATE";
} else if (isTextField) {
expectedDataType = "TEXT";
} else {
expectedDataType = "SINGLE_SELECT";
}

// Check for type mismatch if field already exists
if (field && field.dataType && expectedDataType) {
const actualType = field.dataType;
if (actualType !== expectedDataType) {
core.warning(
`Field type mismatch for "${fieldName}": Expected ${expectedDataType} but found ${actualType}. ` +
`The field was likely created with the wrong type. To fix this, delete the field in the GitHub Projects UI and let it be recreated, ` +
`or manually change the field type if supported.`
);
// Continue anyway - we'll use the existing field type
}
}

if (!field)
if (fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date")) {
// Check if field name suggests it's a date field (e.g., start_date, end_date, due_date)
// Date field values must match ISO 8601 format (YYYY-MM-DD)
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
if (typeof fieldValue === "string" && datePattern.test(fieldValue)) {
try {
field = (
Expand Down Expand Up @@ -631,22 +672,11 @@ async function updateProject(output) {
valueToSet = { iterationId: iteration.id };
} else if (field.options) {
let option = field.options.find(o => o.name === fieldValue);
if (!option)
try {
const allOptions = [...field.options.map(o => ({ name: o.name, description: "", color: o.color || "GRAY" })), { name: String(fieldValue), description: "", color: "GRAY" }],
updatedField = (
await github.graphql(
"mutation($fieldId: ID!, $fieldName: String!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {\n updateProjectV2Field(input: {\n fieldId: $fieldId,\n name: $fieldName,\n singleSelectOptions: $options\n }) {\n projectV2Field {\n ... on ProjectV2SingleSelectField {\n id\n options {\n id\n name\n }\n }\n }\n }\n }",
{ fieldId: field.id, fieldName: field.name, options: allOptions }
)
).updateProjectV2Field.projectV2Field;
((option = updatedField.options.find(o => o.name === fieldValue)), (field = updatedField));
} catch (createError) {
core.warning(`Failed to create option "${fieldValue}": ${getErrorMessage(createError)}`);
continue;
}
if (!option) {
core.warning(`Could not get option ID for "${fieldValue}" in field "${fieldName}"`);
// GitHub's GraphQL API does not support adding new options to existing single-select fields
// The updateProjectV2Field mutation does not exist - users must add options manually via UI
const availableOptions = field.options.map(o => o.name).join(", ");
core.warning(`Option "${fieldValue}" not found in field "${fieldName}". Available options: ${availableOptions}. To add this option, please update the field manually in the GitHub Projects UI.`);
continue;
}
valueToSet = { singleSelectOptionId: option.id };
Expand Down
39 changes: 8 additions & 31 deletions actions/setup/js/update_project.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ describe("updateProject", () => {
expect(updateCall).toBeDefined();
});

it("creates a new option in single select field with colors for existing options", async () => {
it("warns when attempting to add a new option to a single select field", async () => {
const projectUrl = "https://github.com/orgs/testowner/projects/60";
const output = {
type: "update_project",
Expand Down Expand Up @@ -472,42 +472,19 @@ describe("updateProject", () => {
],
},
]),
// Response for updateProjectV2Field mutation
{
updateProjectV2Field: {
projectV2Field: {
id: "field-status",
options: [
{ id: "opt-todo", name: "Todo" },
{ id: "opt-in-progress", name: "In Progress" },
{ id: "opt-done", name: "Done" },
{ id: "opt-closed", name: "Closed" },
{ id: "opt-closed-not-planned", name: "Closed - Not Planned" },
],
},
},
},
updateFieldValueResponse(),
]);

await updateProject(output);

// Find the updateProjectV2Field mutation call
// The updateProjectV2Field mutation does not exist in GitHub's API
// Verify that no attempt was made to call it
const updateFieldCall = mockGithub.graphql.mock.calls.find(([query]) => query.includes("updateProjectV2Field"));
expect(updateFieldCall).toBeDefined();

// Verify that the mutation includes color for all options
const options = updateFieldCall[1].options;
expect(options).toHaveLength(5); // 4 existing + 1 new

// Check that all existing options have their colors preserved
expect(options[0]).toEqual({ name: "Todo", description: "", color: "GRAY" });
expect(options[1]).toEqual({ name: "In Progress", description: "", color: "YELLOW" });
expect(options[2]).toEqual({ name: "Done", description: "", color: "GREEN" });
expect(options[3]).toEqual({ name: "Closed", description: "", color: "PURPLE" });
expect(updateFieldCall).toBeUndefined();

// Check that the new option has a default color
expect(options[4]).toEqual({ name: "Closed - Not Planned", description: "", color: "GRAY" });
// Verify that a warning was logged about the missing option
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('Option "Closed - Not Planned" not found in field "Status"'));
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Available options: Todo, In Progress, Done, Closed"));
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("please update the field manually in the GitHub Projects UI"));
});

it("warns when a field cannot be created", async () => {
Expand Down
Loading