Skip to content
Draft
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,7 @@ The following sets of tools are available:
- `item_owner`: The owner (user or organization) of the repository containing the issue or pull request. Required for 'add_project_item' method. Also accepted by 'update_project_item' when resolving the item by issue number. (string, optional)
- `item_repo`: The name of the repository containing the issue or pull request. Required for 'add_project_item' method. Also accepted by 'update_project_item' when resolving the item by issue number. (string, optional)
- `item_type`: The item's type, either issue or pull_request. Required for 'add_project_item' method. (string, optional)
- `items`: The set of items to update. Required for 'update_project_items'. Each entry references an existing item by exactly one of: 'node_id' (the item's GraphQL node ID, e.g. as returned by 'list_project_items' or 'add_project_item'), 'item_id' (the numeric project item ID), or (item_owner + item_repo + issue_number). Each entry may optionally include its own 'updated_field'; if omitted, the top-level 'updated_field' is applied. Limit: 100 items per call. (object[], optional)
- `iteration_duration`: Duration in days for iterations of the field (e.g. 7 for weekly, 14 for bi-weekly). Required for 'create_iteration_field' method. (number, optional)
- `iterations`: Custom iterations for 'create_iteration_field' method. Only set this when you need iterations with varying durations, breaks between them, or specific titles. Otherwise omit it: GitHub auto-creates three iterations of 'iteration_duration' days starting on 'start_date', which is the right choice for most cases. (object[], optional)
- `method`: The method to execute (string, required)
Expand All @@ -1130,7 +1131,7 @@ The following sets of tools are available:
- `status`: The status of the project. Used for 'create_project_status_update' method. (string, optional)
- `target_date`: The target date of the status update in YYYY-MM-DD format. Used for 'create_project_status_update' method. (string, optional)
- `title`: The project title. Required for 'create_project' method. (string, optional)
- `updated_field`: Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {"id": 123456, "value": "..."}; (2) by name — {"name": "Status", "value": "In Progress"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field. (object, optional)
- `updated_field`: Object describing the field to update and its new value. Required for 'update_project_item'. For 'update_project_items', provide a shared shape at the top level or override it per item. Two shapes are accepted: (1) by ID — {"id": 123456, "value": "..."}; (2) by name — {"name": "Status", "value": "In Progress"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field. (object, optional)

</details>

Expand Down
37 changes: 36 additions & 1 deletion pkg/github/__toolsnaps__/projects_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,40 @@
],
"type": "string"
},
"items": {
"description": "The set of items to update. Required for 'update_project_items'. Each entry references an existing item by exactly one of: 'node_id' (the item's GraphQL node ID, e.g. as returned by 'list_project_items' or 'add_project_item'), 'item_id' (the numeric project item ID), or (item_owner + item_repo + issue_number). Each entry may optionally include its own 'updated_field'; if omitted, the top-level 'updated_field' is applied. Limit: 100 items per call.",
"items": {
"additionalProperties": false,
"properties": {
"issue_number": {
"description": "Issue number used to resolve the project item together with item_owner and item_repo.",
"type": "integer"
},
"item_id": {
"description": "The project item ID. Provide exactly one of node_id, item_id, or (item_owner + item_repo + issue_number).",
"type": "integer"
},
"item_owner": {
"description": "Owner of the repository containing the issue. Combine with item_repo and issue_number to resolve the item.",
"type": "string"
},
"item_repo": {
"description": "Repository containing the issue. Combine with item_owner and issue_number to resolve the item.",
"type": "string"
},
"node_id": {
"description": "The project item's GraphQL node ID. Provide exactly one of node_id, item_id, or (item_owner + item_repo + issue_number).",
"type": "string"
},
"updated_field": {
"description": "Per-item field update. Overrides the top-level 'updated_field'. Same shape as the top-level 'updated_field'.",
"type": "object"
}
},
"type": "object"
},
"type": "array"
},
"iteration_duration": {
"description": "Duration in days for iterations of the field (e.g. 7 for weekly, 14 for bi-weekly). Required for 'create_iteration_field' method.",
"type": "number"
Expand Down Expand Up @@ -76,6 +110,7 @@
"enum": [
"add_project_item",
"update_project_item",
"update_project_items",
"delete_project_item",
"create_project_status_update",
"create_project",
Expand Down Expand Up @@ -127,7 +162,7 @@
"type": "string"
},
"updated_field": {
"description": "Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field.",
"description": "Object describing the field to update and its new value. Required for 'update_project_item'. For 'update_project_items', provide a shared shape at the top level or override it per item. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field.",
"type": "object"
}
},
Expand Down
57 changes: 55 additions & 2 deletions pkg/github/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const (
ProjectStatusUpdateCreateFailedError = "failed to create project status update"
ProjectResolveIDFailedError = "failed to resolve project ID"
MaxProjectsPerPage = 50
// MaxProjectItemsPerBatch caps the number of items processed in a single
// update_project_items call. This keeps a batch bounded, prevents runaway
// resolution costs, and matches the practical scale of typical triage sweeps.
MaxProjectItemsPerBatch = 100
)

// Method constants for consolidated project tools
Expand All @@ -44,6 +48,7 @@ const (
projectsMethodGetProjectItem = "get_project_item"
projectsMethodAddProjectItem = "add_project_item"
projectsMethodUpdateProjectItem = "update_project_item"
projectsMethodUpdateProjectItems = "update_project_items"
projectsMethodDeleteProjectItem = "delete_project_item"
projectsMethodListProjectStatusUpdates = "list_project_status_updates"
projectsMethodGetProjectStatusUpdate = "get_project_status_update"
Expand Down Expand Up @@ -511,6 +516,7 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
Enum: []any{
projectsMethodAddProjectItem,
projectsMethodUpdateProjectItem,
projectsMethodUpdateProjectItems,
projectsMethodDeleteProjectItem,
projectsMethodCreateProjectStatusUpdate,
projectsMethodCreateProject,
Expand Down Expand Up @@ -561,7 +567,41 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
},
"updated_field": {
Type: "object",
Description: "Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field.",
Description: "Object describing the field to update and its new value. Required for 'update_project_item'. For 'update_project_items', provide a shared shape at the top level or override it per item. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field.",
},
"items": {
Type: "array",
Description: "The set of items to update. Required for 'update_project_items'. Each entry references an existing item by exactly one of: 'node_id' (the item's GraphQL node ID, e.g. as returned by 'list_project_items' or 'add_project_item'), 'item_id' (the numeric project item ID), or (item_owner + item_repo + issue_number). Each entry may optionally include its own 'updated_field'; if omitted, the top-level 'updated_field' is applied. Limit: " + strconv.Itoa(MaxProjectItemsPerBatch) + " items per call.",
Items: &jsonschema.Schema{
Type: "object",
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
Properties: map[string]*jsonschema.Schema{
"node_id": {
Type: "string",
Description: "The project item's GraphQL node ID. Provide exactly one of node_id, item_id, or (item_owner + item_repo + issue_number).",
},
"item_id": {
Type: "integer",
Description: "The project item ID. Provide exactly one of node_id, item_id, or (item_owner + item_repo + issue_number).",
},
"item_owner": {
Type: "string",
Description: "Owner of the repository containing the issue. Combine with item_repo and issue_number to resolve the item.",
},
"item_repo": {
Type: "string",
Description: "Repository containing the issue. Combine with item_owner and issue_number to resolve the item.",
},
"issue_number": {
Type: "integer",
Description: "Issue number used to resolve the project item together with item_owner and item_repo.",
},
"updated_field": {
Type: "object",
Description: "Per-item field update. Overrides the top-level 'updated_field'. Same shape as the top-level 'updated_field'.",
},
},
},
},
"body": {
Type: "string",
Expand Down Expand Up @@ -722,6 +762,8 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
return utils.NewToolResultError("updated_field must be an object"), nil, nil
}
return updateProjectItem(ctx, client, gqlClient, owner, ownerType, projectNumber, itemID, fieldValue)
case projectsMethodUpdateProjectItems:
return updateProjectItemsBatch(ctx, client, gqlClient, owner, ownerType, projectNumber, args)
case projectsMethodDeleteProjectItem:
itemID, err := RequiredBigInt(args, "item_id")
if err != nil {
Expand Down Expand Up @@ -1532,6 +1574,13 @@ func validateAndConvertToInt64(value any) (int64, error) {

// buildUpdateProjectItem builds UpdateProjectItemOptions, resolving field names and SINGLE_SELECT option names server-side.
func buildUpdateProjectItem(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, input map[string]any) (*github.UpdateProjectItemOptions, error) {
return buildUpdateProjectItemWithCache(ctx, gqlClient, nil, owner, ownerType, projectNumber, input)
}

// buildUpdateProjectItemWithCache is the cache-aware variant of buildUpdateProjectItem.
// When cache is non-nil, name-based field lookups reuse the cache's in-memory index; when
// nil, each name lookup performs its own GraphQL round trip (single-item behaviour).
func buildUpdateProjectItemWithCache(ctx context.Context, gqlClient *githubv4.Client, cache *projectFieldsCache, owner, ownerType string, projectNumber int, input map[string]any) (*github.UpdateProjectItemOptions, error) {
if input == nil {
return nil, fmt.Errorf("updated_field must be an object")
}
Expand Down Expand Up @@ -1571,7 +1620,11 @@ func buildUpdateProjectItem(ctx context.Context, gqlClient *githubv4.Client, own
return nil, fmt.Errorf("internal error: gqlClient is required to resolve updated_field.name")
}
var err error
resolved, err = resolveProjectFieldByName(ctx, gqlClient, owner, ownerType, projectNumber, fieldName, "")
if cache != nil {
resolved, err = cache.resolveFieldByName(ctx, gqlClient, owner, ownerType, projectNumber, fieldName)
} else {
resolved, err = resolveProjectFieldByName(ctx, gqlClient, owner, ownerType, projectNumber, fieldName, "")
}
if err != nil {
return nil, err
}
Expand Down
Loading
Loading