Skip to content

Commit f975967

Browse files
committed
Support environmentfs
1 parent 83e794e commit f975967

14 files changed

Lines changed: 921 additions & 185 deletions

File tree

.schemas/repository-config.schema.json

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,74 @@
9191
"ref_name"
9292
]
9393
},
94+
"Environment": {
95+
"properties": {
96+
"environment": {
97+
"type": "string"
98+
},
99+
"wait_timer": {
100+
"type": "integer"
101+
},
102+
"can_admins_bypass": {
103+
"type": "boolean"
104+
},
105+
"prevent_self_review": {
106+
"type": "boolean"
107+
},
108+
"reviewers": {
109+
"$ref": "#/$defs/EnvironmentReviewers"
110+
},
111+
"deployment_policy": {
112+
"$ref": "#/$defs/EnvironmentDeploymentPolicy"
113+
}
114+
},
115+
"additionalProperties": false,
116+
"type": "object",
117+
"required": [
118+
"environment"
119+
]
120+
},
121+
"EnvironmentDeploymentPolicy": {
122+
"properties": {
123+
"policy_type": {
124+
"type": "string",
125+
"enum": [
126+
"all",
127+
"protected_branches",
128+
"selected_branches_and_tags"
129+
]
130+
},
131+
"branch_patterns": {
132+
"items": {
133+
"type": "string"
134+
},
135+
"type": "array"
136+
}
137+
},
138+
"additionalProperties": false,
139+
"type": "object",
140+
"required": [
141+
"policy_type"
142+
]
143+
},
144+
"EnvironmentReviewers": {
145+
"properties": {
146+
"users": {
147+
"items": {
148+
"type": "string"
149+
},
150+
"type": "array"
151+
},
152+
"teams": {
153+
"items": {
154+
"type": "string"
155+
},
156+
"type": "array"
157+
}
158+
},
159+
"additionalProperties": false,
160+
"type": "object"
161+
},
94162
"HighIntegrityConfig": {
95163
"properties": {
96164
"enabled": {
@@ -421,6 +489,12 @@
421489
},
422490
"type": "array"
423491
},
492+
"environments": {
493+
"items": {
494+
"$ref": "#/$defs/Environment"
495+
},
496+
"type": "array"
497+
},
424498
"high_integrity": {
425499
"$ref": "#/$defs/HighIntegrityConfig"
426500
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
9+
"github.com/google/go-github/v67/github"
10+
)
11+
12+
const (
13+
protectionRuleTypeWaitTimer = "wait_timer"
14+
protectionRuleTypeRequiredReviewers = "required_reviewers"
15+
16+
deploymentPolicyTypeAll = "all"
17+
deploymentPolicyTypeProtectedBranches = "protected_branches"
18+
deploymentPolicyTypeSelectedBranchesAndTags = "selected_branches_and_tags"
19+
)
20+
21+
// fetchEnvironments lists all deployment environments for a repository and resolves
22+
// their configuration (reviewers, branch policies, wait timers, etc).
23+
// Returns nil, nil on 403 (insufficient permissions).
24+
func fetchEnvironments(owner, repo string, orgTeams []*github.Team) ([]Environment, error) {
25+
teamSlugMap := buildTeamSlugMap(orgTeams)
26+
27+
opts := &github.EnvironmentListOptions{
28+
ListOptions: github.ListOptions{PerPage: 100},
29+
}
30+
31+
var environments []Environment
32+
for {
33+
resp, httpResp, err := v3client.Repositories.ListEnvironments(context.Background(), owner, repo, opts)
34+
if err != nil {
35+
if httpResp != nil && httpResp.StatusCode == http.StatusForbidden {
36+
fmt.Printf("skipping environments due to insufficient permissions: %v\n", err)
37+
return nil, nil
38+
}
39+
return nil, fmt.Errorf("failed to list environments: %w", err)
40+
}
41+
42+
for _, env := range resp.Environments {
43+
environments = append(environments, resolveEnvironment(env, owner, repo, teamSlugMap))
44+
}
45+
46+
if httpResp.NextPage == 0 {
47+
break
48+
}
49+
opts.Page = httpResp.NextPage
50+
}
51+
52+
return environments, nil
53+
}
54+
55+
// resolveEnvironment converts a go-github Environment into our YAML-serialisable struct.
56+
func resolveEnvironment(env *github.Environment, owner, repo string, teamSlugMap map[int64]string) Environment {
57+
result := Environment{
58+
Environment: env.GetName(),
59+
CanAdminsBypass: env.CanAdminsBypass,
60+
}
61+
62+
for _, rule := range env.ProtectionRules {
63+
switch rule.GetType() {
64+
case protectionRuleTypeWaitTimer:
65+
result.WaitTimer = rule.WaitTimer
66+
67+
case protectionRuleTypeRequiredReviewers:
68+
result.PreventSelfReview = rule.PreventSelfReview
69+
if len(rule.Reviewers) > 0 {
70+
result.Reviewers = resolveEnvironmentReviewers(rule.Reviewers, teamSlugMap)
71+
}
72+
}
73+
}
74+
75+
if env.DeploymentBranchPolicy != nil {
76+
switch {
77+
case env.DeploymentBranchPolicy.GetProtectedBranches():
78+
result.DeploymentPolicy = &EnvironmentDeploymentPolicy{
79+
PolicyType: deploymentPolicyTypeProtectedBranches,
80+
}
81+
case env.DeploymentBranchPolicy.GetCustomBranchPolicies():
82+
patterns := fetchDeploymentBranchPatterns(owner, repo, env.GetName())
83+
result.DeploymentPolicy = &EnvironmentDeploymentPolicy{
84+
PolicyType: deploymentPolicyTypeSelectedBranchesAndTags,
85+
BranchPatterns: patterns,
86+
}
87+
}
88+
}
89+
90+
return result
91+
}
92+
93+
// resolveEnvironmentReviewers converts raw API reviewer objects to usernames/team slugs.
94+
func resolveEnvironmentReviewers(reviewers []*github.RequiredReviewer, teamSlugMap map[int64]string) *EnvironmentReviewers {
95+
result := &EnvironmentReviewers{}
96+
97+
for _, r := range reviewers {
98+
switch r.GetType() {
99+
case "User":
100+
user, err := extractUser(r.Reviewer)
101+
if err != nil {
102+
fmt.Printf("failed to extract user reviewer: %v\n", err)
103+
continue
104+
}
105+
if login := user.GetLogin(); login != "" {
106+
result.Users = append(result.Users, login)
107+
}
108+
109+
case "Team":
110+
team, err := extractTeam(r.Reviewer)
111+
if err != nil {
112+
fmt.Printf("failed to extract team reviewer: %v\n", err)
113+
continue
114+
}
115+
// Prefer the slug from the org teams map (more reliable) but fall back to the object's slug.
116+
if slug, ok := teamSlugMap[team.GetID()]; ok {
117+
result.Teams = append(result.Teams, slug)
118+
} else if slug := team.GetSlug(); slug != "" {
119+
result.Teams = append(result.Teams, slug)
120+
}
121+
}
122+
}
123+
124+
if len(result.Users) == 0 && len(result.Teams) == 0 {
125+
return nil
126+
}
127+
return result
128+
}
129+
130+
// fetchDeploymentBranchPatterns retrieves the branch/tag patterns configured for an
131+
// environment that uses custom branch policies.
132+
func fetchDeploymentBranchPatterns(owner, repo, environmentName string) []string {
133+
resp, _, err := v3client.Repositories.ListDeploymentBranchPolicies(context.Background(), owner, repo, environmentName)
134+
if err != nil {
135+
fmt.Printf("failed to fetch deployment branch policies for %s/%s environment %q: %v\n", owner, repo, environmentName, err)
136+
return nil
137+
}
138+
139+
var patterns []string
140+
for _, p := range resp.BranchPolicies {
141+
if name := p.GetName(); name != "" {
142+
patterns = append(patterns, name)
143+
}
144+
}
145+
return patterns
146+
}
147+
148+
// buildTeamSlugMap creates an id → slug lookup from a slice of org teams.
149+
func buildTeamSlugMap(teams []*github.Team) map[int64]string {
150+
m := make(map[int64]string, len(teams))
151+
for _, t := range teams {
152+
if t.ID != nil && t.Slug != nil {
153+
m[*t.ID] = *t.Slug
154+
}
155+
}
156+
return m
157+
}
158+
159+
// extractUser re-marshals the opaque Reviewer interface{} into a *github.User.
160+
func extractUser(reviewer interface{}) (*github.User, error) {
161+
data, err := json.Marshal(reviewer)
162+
if err != nil {
163+
return nil, fmt.Errorf("marshal reviewer: %w", err)
164+
}
165+
var user github.User
166+
if err := json.Unmarshal(data, &user); err != nil {
167+
return nil, fmt.Errorf("unmarshal user reviewer: %w", err)
168+
}
169+
return &user, nil
170+
}
171+
172+
// extractTeam re-marshals the opaque Reviewer interface{} into a *github.Team.
173+
func extractTeam(reviewer interface{}) (*github.Team, error) {
174+
data, err := json.Marshal(reviewer)
175+
if err != nil {
176+
return nil, fmt.Errorf("marshal reviewer: %w", err)
177+
}
178+
var team github.Team
179+
if err := json.Unmarshal(data, &team); err != nil {
180+
return nil, fmt.Errorf("unmarshal team reviewer: %w", err)
181+
}
182+
return &team, nil
183+
}

0 commit comments

Comments
 (0)