-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrepository_policy.go
More file actions
241 lines (233 loc) · 11.7 KB
/
Copy pathrepository_policy.go
File metadata and controls
241 lines (233 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
// PermissionPolicy uses the read API's camelCase key; edits use "mode".
type PermissionPolicy struct {
MemberManagementMode *int `json:"memberMgntMode"`
}
type UpdatePermissionPolicyRequest struct {
Mode *int `json:"mode,omitempty"`
}
type UpdateCodeReviewPolicyRequest struct {
Assignees *string `json:"assignees,omitempty"`
Testers *string `json:"testers,omitempty"`
AssigneesNumber *int `json:"assignees_number,omitempty"`
TestersNumber *int `json:"testers_number,omitempty"`
}
type CodeReviewPolicyUpdateResponse struct {
ID *int64 `json:"id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type PermissionPolicyUpdateResponse struct {
Code *int `json:"code"`
Msg string `json:"msg"`
}
// CodeReviewPolicy is a read-only projection of pull_request_settings.
// There is no documented GET /reviewer. Reviewer identities are retained as
// JSON objects because the documented example specifies only empty arrays.
type CodeReviewPolicy struct {
ApprovalApprovers []json.RawMessage `json:"approval_approvers"`
ApprovalTesters []json.RawMessage `json:"approval_testers"`
ApprovalRequiredApprovers *int `json:"approval_required_approvers"`
ApprovalRequiredTesters *int `json:"approval_required_testers"`
}
type UpdatePullRequestPolicyRequest struct {
ApprovalRequiredReviewersEnable *bool `json:"approval_required_reviewers_enable,omitempty"`
ApprovalRequiredReviewers *int `json:"approval_required_reviewers,omitempty"`
OnlyAllowMergeIfAllDiscussionsAreResolved *bool `json:"only_allow_merge_if_all_discussions_are_resolved,omitempty"`
OnlyAllowMergeIfPipelineSucceeds *bool `json:"only_allow_merge_if_pipeline_succeeds,omitempty"`
DisableMergeBySelf *bool `json:"disable_merge_by_self,omitempty"`
CanForceMerge *bool `json:"can_force_merge,omitempty"`
AddNotesAfterMerged *bool `json:"add_notes_after_merged,omitempty"`
MarkAutoMergedMRAsClosed *bool `json:"mark_auto_merged_mr_as_closed,omitempty"`
CanReopen *bool `json:"can_reopen,omitempty"`
DeleteSourceBranchWhenMerged *bool `json:"delete_source_branch_when_merged,omitempty"`
DisableSquashMerge *bool `json:"disable_squash_merge,omitempty"`
AutoSquashMerge *bool `json:"auto_squash_merge,omitempty"`
MergeMethod *string `json:"merge_method,omitempty"`
SquashMergeWithNoMergeCommit *bool `json:"squash_merge_with_no_merge_commit,omitempty"`
MergedCommitAuthor *string `json:"merged_commit_author,omitempty"`
ApprovalRequiredApprovers *int `json:"approval_required_approvers,omitempty"`
ApprovalApproverIDs *string `json:"approval_approver_ids,omitempty"`
ApprovalTesterIDs *string `json:"approval_tester_ids,omitempty"`
ApprovalRequiredTesters *int `json:"approval_required_testers,omitempty"`
IsCheckCLA *bool `json:"is_check_cla,omitempty"`
IsAllowLiteMergeRequest *bool `json:"is_allow_lite_merge_request,omitempty"`
LiteMergeRequestPrefixTitle *string `json:"lite_merge_request_prefix_title,omitempty"`
CloseIssueWhenMRMerged *bool `json:"close_issue_when_mr_merged,omitempty"`
ForbiddenPRRelatedIssueClosed *bool `json:"forbidden_pr_related_issue_closed,omitempty"`
}
// Response switches vary between JSON booleans, 0/1 integers, and strings
// across the documented GET/PUT schemas. Normalize them to boolean semantics
// while preserving nil for absent or null settings; requests remain *bool.
type MergeRequestPolicySettings struct {
ApprovalRequiredReviewersEnable *FlexibleBool `json:"approval_required_reviewers_enable"`
ApprovalRequiredReviewers *int `json:"approval_required_reviewers"`
DisableMergeBySelf *FlexibleBool `json:"disable_merge_by_self"`
CanForceMerge *FlexibleBool `json:"can_force_merge"`
AddNotesAfterMerged *FlexibleBool `json:"add_notes_after_merged"`
MarkAutoMergedMRAsClosed *FlexibleBool `json:"mark_auto_merged_mr_as_closed"`
CanReopen *FlexibleBool `json:"can_reopen"`
DeleteSourceBranchWhenMerged *FlexibleBool `json:"delete_source_branch_when_merged"`
DisableSquashMerge *FlexibleBool `json:"disable_squash_merge"`
AutoSquashMerge *FlexibleBool `json:"auto_squash_merge"`
SquashMergeWithNoMergeCommit *FlexibleBool `json:"squash_merge_with_no_merge_commit"`
MergedCommitAuthor *string `json:"merged_commit_author"`
ApprovalRequiredApprovers *int `json:"approval_required_approvers"`
ApprovalRequiredTesters *int `json:"approval_required_testers"`
IsCheckCLA *FlexibleBool `json:"is_check_cla"`
IsAllowLiteMergeRequest *FlexibleBool `json:"is_allow_lite_merge_request"`
LiteMergeRequestPrefixTitle *string `json:"lite_merge_request_prefix_title"`
CloseIssueWhenMRMerged *FlexibleBool `json:"close_issue_when_mr_merged"`
ForbiddenPRRelatedIssueClosed *FlexibleBool `json:"forbidden_pr_related_issue_closed"`
ApprovalApprovers []json.RawMessage `json:"approval_approvers"`
ApprovalTesters []json.RawMessage `json:"approval_testers"`
}
type PullRequestPolicy struct {
MergeRequestSetting *MergeRequestPolicySettings `json:"merge_request_setting"`
OnlyAllowMergeIfAllDiscussionsAreResolved *FlexibleBool `json:"only_allow_merge_if_all_discussions_are_resolved"`
OnlyAllowMergeIfPipelineSucceeds *FlexibleBool `json:"only_allow_merge_if_pipeline_succeeds"`
MergeMethod *string `json:"merge_method"`
}
func policyPath(owner, repo, endpoint string) string {
return fmt.Sprintf("/repos/%s/%s/%s", url.PathEscape(owner), url.PathEscape(repo), endpoint)
}
// All policy endpoints document 200 JSON responses. Do not retry writes:
// even idempotent settings can race with another administrator's changes.
func policyRequest(client *Client, method, path string, request, result any) error {
var body *bytes.Reader
payload := []byte(nil)
if request != nil {
var err error
payload, err = json.Marshal(request)
if err != nil {
return fmt.Errorf("encode policy request: %w", err)
}
}
body = bytes.NewReader(payload)
return client.doJSONRequest(method, path, body, "application/json", "application/json",
RequestPolicy{AllowedStatuses: []int{http.StatusOK}, CanRetry: method == http.MethodGet}, result)
}
func GetPermissionPolicy(client *Client, owner, repo string) (PermissionPolicy, error) {
var result PermissionPolicy
err := policyRequest(client, http.MethodGet, policyPath(owner, repo, "transition"), nil, &result)
if err == nil && result.MemberManagementMode == nil {
err = fmt.Errorf("invalid permission policy response: missing memberMgntMode")
}
return result, err
}
func GetPullRequestPolicy(client *Client, owner, repo string) (PullRequestPolicy, error) {
var result PullRequestPolicy
err := policyRequest(client, http.MethodGet, policyPath(owner, repo, "pull_request_settings"), nil, &result)
if err == nil {
err = validatePullRequestPolicy(result)
}
return result, err
}
func validatePullRequestPolicy(result PullRequestPolicy) error {
if result.MergeRequestSetting == nil || result.MergeMethod == nil ||
result.OnlyAllowMergeIfAllDiscussionsAreResolved == nil || result.OnlyAllowMergeIfPipelineSucceeds == nil {
return fmt.Errorf("invalid pull-request policy response: missing settings")
}
return validatePolicyReviewers(result.MergeRequestSetting)
}
func validatePolicyReviewers(settings *MergeRequestPolicySettings) error {
for _, field := range []struct {
name string
entries []json.RawMessage
}{
{"approval_approvers", settings.ApprovalApprovers},
{"approval_testers", settings.ApprovalTesters},
} {
for i, entry := range field.entries {
var object map[string]json.RawMessage
if err := json.Unmarshal(entry, &object); err != nil || object == nil {
return fmt.Errorf("invalid policy response: %s[%d] must be an object", field.name, i)
}
}
}
return nil
}
func CodeReviewPolicyFrom(settings PullRequestPolicy) (CodeReviewPolicy, error) {
s := settings.MergeRequestSetting
if s == nil || s.ApprovalRequiredApprovers == nil || s.ApprovalRequiredTesters == nil ||
s.ApprovalApprovers == nil || s.ApprovalTesters == nil {
return CodeReviewPolicy{}, fmt.Errorf("invalid code-review policy response: missing approval settings")
}
if err := validatePolicyReviewers(s); err != nil {
return CodeReviewPolicy{}, err
}
return CodeReviewPolicy{s.ApprovalApprovers, s.ApprovalTesters, s.ApprovalRequiredApprovers, s.ApprovalRequiredTesters}, nil
}
func UpdatePermissionPolicy(client *Client, owner, repo string, request UpdatePermissionPolicyRequest) error {
var result PermissionPolicyUpdateResponse
if err := policyRequest(client, http.MethodPut, policyPath(owner, repo, "transition"), request, &result); err != nil {
return err
}
if result.Code == nil || *result.Code != 1 {
return fmt.Errorf("permission policy update was not acknowledged as successful")
}
return nil
}
func UpdateCodeReviewPolicy(client *Client, owner, repo string, request UpdateCodeReviewPolicyRequest) error {
var result CodeReviewPolicyUpdateResponse
if err := policyRequest(client, http.MethodPut, policyPath(owner, repo, "reviewer"), request, &result); err != nil {
return err
}
if result.ID == nil || *result.ID <= 0 {
return fmt.Errorf("invalid code-review update response: missing repository id")
}
return nil
}
func UpdatePullRequestPolicy(client *Client, owner, repo string, request UpdatePullRequestPolicyRequest) error {
var result PullRequestPolicy
if err := policyRequest(client, http.MethodPut, policyPath(owner, repo, "pull_request_settings"), request, &result); err != nil {
return err
}
if err := validatePullRequestPolicy(result); err != nil {
return err
}
return validatePolicyUpdate(request, result)
}
// Compare submitted scalars with fields actually returned by the server. Some
// settings are omitted by the API, and reviewer IDs have no documented mapping
// to the returned identity objects; neither can be verified from this response.
func validatePolicyUpdate(request UpdatePullRequestPolicyRequest, result PullRequestPolicy) error {
wantedJSON, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("encode requested policy: %w", err)
}
actualJSON, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("encode returned policy: %w", err)
}
var wanted, actual, nested map[string]json.RawMessage
if err := json.Unmarshal(wantedJSON, &wanted); err != nil {
return fmt.Errorf("decode requested policy: %w", err)
}
if err := json.Unmarshal(actualJSON, &actual); err != nil {
return fmt.Errorf("decode returned policy: %w", err)
}
if err := json.Unmarshal(actual["merge_request_setting"], &nested); err != nil {
return fmt.Errorf("decode returned policy settings: %w", err)
}
for field, value := range wanted {
returned, exists := actual[field]
if !exists {
returned = nested[field]
}
if len(returned) == 0 || bytes.Equal(returned, []byte("null")) {
continue
}
if !bytes.Equal(value, returned) {
return fmt.Errorf("policy update response differs from requested %s; changes may have been partially applied, inspect the policy before retrying", field)
}
}
return nil
}