From a3427f7d8caf58639693c918a71c30adcfecd9e7 Mon Sep 17 00:00:00 2001 From: mmelodyRTR Date: Fri, 24 Apr 2026 10:27:25 +0100 Subject: [PATCH 1/4] fix: handle string-typed reviewer ID in Ruleset API responses --- github/rules.go | 32 +++++++++++++++++ github/rules_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/github/rules.go b/github/rules.go index d4e94edc269..c6e115e59c2 100644 --- a/github/rules.go +++ b/github/rules.go @@ -8,6 +8,7 @@ package github import ( "encoding/json" "fmt" + "strconv" ) // RulesetTarget represents a GitHub ruleset target. @@ -486,6 +487,37 @@ type RulesetReviewer struct { Type *RulesetReviewerType `json:"type,omitempty"` } +// UnmarshalJSON implements the json.Unmarshaler interface. +func (r *RulesetReviewer) UnmarshalJSON(data []byte) error { + var aux struct { + ID any `json:"id,omitempty"` + Type *RulesetReviewerType `json:"type,omitempty"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + r.Type = aux.Type + + if aux.ID != nil { + switch id := aux.ID.(type) { + case float64: + r.ID = Ptr(int64(id)) + case string: + i, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return err + } + r.ID = &i + default: + return fmt.Errorf("unexpected type for reviewer.ID: %T", id) + } + } + + return nil +} + // RequiredStatusChecksRuleParameters represents the required status checks rule parameters. type RequiredStatusChecksRuleParameters struct { DoNotEnforceOnCreate *bool `json:"do_not_enforce_on_create,omitempty"` diff --git a/github/rules_test.go b/github/rules_test.go index 05b63fe358c..59435e9b414 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -847,6 +847,35 @@ func TestRepositoryRule(t *testing.T) { }, `{"type":"pull_request","parameters":{"allowed_merge_methods":["merge","squash","rebase"],"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_approving_review_count":0,"required_reviewers":[{"minimum_approvals":1,"file_patterns":["*"],"reviewer":{"id":123456,"type":"Team"}}],"required_review_thread_resolution":false}}`, }, + { + "pull_request_string_id", + &RepositoryRule{ + Type: RulesetRuleTypePullRequest, + Parameters: &PullRequestRuleParameters{ + AllowedMergeMethods: []PullRequestMergeMethod{ + PullRequestMergeMethodMerge, + PullRequestMergeMethodSquash, + PullRequestMergeMethodRebase, + }, + DismissStaleReviewsOnPush: false, + RequireCodeOwnerReview: false, + RequireLastPushApproval: false, + RequiredApprovingReviewCount: 0, + RequiredReviewThreadResolution: false, + RequiredReviewers: []*RulesetRequiredReviewer{ + { + MinimumApprovals: Ptr(1), + FilePatterns: []string{"*"}, + Reviewer: &RulesetReviewer{ + ID: Ptr(int64(123456)), + Type: Ptr(RulesetReviewerTypeTeam), + }, + }, + }, + }, + }, + `{"type":"pull_request","parameters":{"allowed_merge_methods":["merge","squash","rebase"],"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_approving_review_count":0,"required_reviewers":[{"minimum_approvals":1,"file_patterns":["*"],"reviewer":{"id":"123456","type":"Team"}}],"required_review_thread_resolution":false}}`, + }, { "required_status_checks", &RepositoryRule{ @@ -1172,3 +1201,59 @@ func TestRepositoryRule(t *testing.T) { } }) } + +func TestRulesetReviewer_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + wantID int64 + wantErr bool + }{ + { + name: "integer_id", + json: `{"id": 123456, "type": "Team"}`, + wantID: 123456, + wantErr: false, + }, + { + name: "string_id", + json: `{"id": "123456", "type": "Team"}`, + wantID: 123456, + wantErr: false, + }, + { + name: "invalid_string_id", + json: `{"id": "not-a-number", "type": "Team"}`, + wantErr: true, + }, + { + name: "invalid_type_id", + json: `{"id": {}, "type": "Team"}`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var got RulesetReviewer + err := json.Unmarshal([]byte(tt.json), &got) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if got.GetID() != tt.wantID { + t.Errorf("UnmarshalJSON() got ID = %v, want %v", got.GetID(), tt.wantID) + } + if got.GetType() == nil || *got.GetType() != RulesetReviewerTypeTeam { + t.Errorf("UnmarshalJSON() got Type = %v, want %v", got.GetType(), RulesetReviewerTypeTeam) + } + } + }) + } +} From 7170ea44ecbca3abd4970ff93ae6101b6c3c1edf Mon Sep 17 00:00:00 2001 From: mmelodyRTR Date: Fri, 24 Apr 2026 10:40:14 +0100 Subject: [PATCH 2/4] style: fix alignment in RulesetReviewer.UnmarshalJSON --- github/rules.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/rules.go b/github/rules.go index c6e115e59c2..5061b838133 100644 --- a/github/rules.go +++ b/github/rules.go @@ -490,7 +490,7 @@ type RulesetReviewer struct { // UnmarshalJSON implements the json.Unmarshaler interface. func (r *RulesetReviewer) UnmarshalJSON(data []byte) error { var aux struct { - ID any `json:"id,omitempty"` + ID any `json:"id,omitempty"` Type *RulesetReviewerType `json:"type,omitempty"` } From f91127695e7719a71670b1c3c816116120855d99 Mon Sep 17 00:00:00 2001 From: mmelodyRTR Date: Fri, 24 Apr 2026 10:41:19 +0100 Subject: [PATCH 3/4] docs: align UnmarshalJSON comment style in rules.go --- github/rules.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/rules.go b/github/rules.go index 5061b838133..3699359853a 100644 --- a/github/rules.go +++ b/github/rules.go @@ -487,7 +487,7 @@ type RulesetReviewer struct { Type *RulesetReviewerType `json:"type,omitempty"` } -// UnmarshalJSON implements the json.Unmarshaler interface. +// UnmarshalJSON is a custom JSON unmarshaler for RulesetReviewer. func (r *RulesetReviewer) UnmarshalJSON(data []byte) error { var aux struct { ID any `json:"id,omitempty"` From 3166e0659cbda13565ebd30586f0028e95acb958 Mon Sep 17 00:00:00 2001 From: mmelodyRTR Date: Fri, 24 Apr 2026 13:42:21 +0100 Subject: [PATCH 4/4] Add additional test for malformed json --- github/rules_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/github/rules_test.go b/github/rules_test.go index 59435e9b414..964ce3cc59b 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -1233,6 +1233,11 @@ func TestRulesetReviewer_UnmarshalJSON(t *testing.T) { json: `{"id": {}, "type": "Team"}`, wantErr: true, }, + { + name: "malformed_json", + json: `{"id":`, + wantErr: true, + }, } for _, tt := range tests {