Skip to content
Open
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
1 change: 1 addition & 0 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const (
PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber = "DELETE /repos/{owner}/{repo}/issues/{issue_number}/sub_issue"
PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}/sub_issues/priority"
DeleteReposIssuesIssueFieldValueByOwnerByRepoByIssueNumber = "DELETE /repos/{owner}/{repo}/issues/{issue_number}/issue-field-values/{issue_field_id}"

// Pull request endpoints
GetReposPullsByOwnerByRepo = "GET /repos/{owner}/{repo}/pulls"
Expand Down
63 changes: 59 additions & 4 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -2179,10 +2179,13 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
issueRequest.Type = github.Ptr(issueType)
}

// Field IDs to clear via DELETE after the PATCH. See the post-PATCH loop
// for why we can't just rely on REST set semantics.
var fallbackDeleteFieldIDs []int64

if len(issueFieldValues) > 0 || len(fieldIDsToDelete) > 0 {
// The REST update endpoint uses "set" semantics — it overwrites all existing
// field values with whatever is sent. Fetch the current values first, merge in
// the new values, then remove any explicitly deleted fields.
// REST PATCH uses set semantics, so fetch existing values, merge in
// the new ones, then drop anything explicitly deleted.
existing, err := fetchExistingIssueFieldValues(ctx, gqlClient, owner, repo, issueNumber)
if err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to fetch existing issue field values", err), nil
Expand All @@ -2201,7 +2204,22 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
}
merged = kept
}
issueRequest.IssueFieldValues = merged
if len(merged) == 0 && len(fieldIDsToDelete) > 0 {
// Only queue DELETEs for fields actually present — the endpoint
// returns 404 otherwise, and "delete a field that isn't set" should
// stay a no-op (callers often invoke delete:true idempotently).
existingIDs := make(map[int64]bool, len(existing))
for _, e := range existing {
existingIDs[e.FieldID] = true
}
for _, id := range fieldIDsToDelete {
if existingIDs[id] {
fallbackDeleteFieldIDs = append(fallbackDeleteFieldIDs, id)
}
}
} else {
issueRequest.IssueFieldValues = merged
}
}

updatedIssue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest)
Expand All @@ -2222,6 +2240,43 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to update issue", resp, body), nil
}

// Per-field DELETE fallback. The PATCH can't clear field values when the
// merged set is empty — go-github's `omitempty` strips the empty slice
// and the dotcom REST handler skips its issue_field_values block when the
// key is absent. Errors are aggregated (not short-circuited) so callers
// can see which fields succeeded and which need retry.
if len(fallbackDeleteFieldIDs) > 0 {
var failedIDs, succeededIDs []int64
var firstFailureErr error
var firstFailureResp *github.Response
for _, fieldID := range fallbackDeleteFieldIDs {
path := fmt.Sprintf("repos/%s/%s/issues/%d/issue-field-values/%d", owner, repo, issueNumber, fieldID)
req, err := client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
failedIDs = append(failedIDs, fieldID)
if firstFailureErr == nil {
firstFailureErr = err
}
continue
}
delResp, err := client.Do(req, nil)
if err != nil {
failedIDs = append(failedIDs, fieldID)
if firstFailureErr == nil {
firstFailureErr = err
firstFailureResp = delResp
}
continue
}
succeededIDs = append(succeededIDs, fieldID)
_ = delResp.Body.Close()
}
if len(failedIDs) > 0 {
msg := fmt.Sprintf("failed to clear issue field values: failed=%v, cleared=%v", failedIDs, succeededIDs)
return ghErrors.NewGitHubAPIErrorResponse(ctx, msg, firstFailureResp, firstFailureErr), nil
}
}

// Use GraphQL API for state updates
if state != "" {
// Mandate specifying duplicateOf when trying to close as duplicate
Expand Down
Loading
Loading