Skip to content

Commit 0ca994f

Browse files
rlmartintravi
andauthored
feat: Add support for environments (#671)
Co-authored-by: Matt Travi <[email protected]> Fixes #607
1 parent 97c12a3 commit 0ca994f

File tree

7 files changed

+719
-1
lines changed

7 files changed

+719
-1
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,25 @@ collaborators:
116116
# * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions.
117117
# * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access.
118118

119+
# See https://docs.github.com/en/rest/deployments/environments#create-or-update-an-environment for available options
120+
# Note: deployment_branch_policy differs from the API for ease of use. Either protected_branches (boolean) OR custom_branches (array of strings) can be provided; this will manage the API requirements under the hood. See https://docs.github.com/en/rest/deployments/branch-policies for documentation of custom_branches. If both are provided in an unexpected manner, protected_branches will be used.
121+
# Either removing or simply not setting deployment_branch_policy will restore the default 'All branches' setting.
122+
environments:
123+
- name: production
124+
wait_timer: 5
125+
reviewers:
126+
- id: 1
127+
type: 'Team'
128+
- id: 2
129+
type: 'User'
130+
deployment_branch_policy:
131+
protected_branches: true
132+
- name: development
133+
deployment_branch_policy:
134+
custom_branches:
135+
- main
136+
- dev/*
137+
119138
# See https://docs.github.com/en/rest/reference/teams#add-or-update-team-repository-permissions for available options
120139
teams:
121140
- name: core

lib/plugins/environments.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
const Diffable = require('./diffable')
2+
3+
const environmentRepoEndpoint = '/repos/:org/:repo/environments/:environment_name'
4+
5+
module.exports = class Environments extends Diffable {
6+
constructor (...args) {
7+
super(...args)
8+
9+
if (this.entries) {
10+
// Force all names to lowercase to avoid comparison issues.
11+
this.entries.forEach(environment => {
12+
environment.name = environment.name.toLowerCase()
13+
})
14+
}
15+
}
16+
17+
async find () {
18+
const {
19+
data: { environments }
20+
} = await this.github.request('GET /repos/:org/:repo/environments', {
21+
org: this.repo.owner,
22+
repo: this.repo.repo
23+
})
24+
return Promise.all(
25+
environments.map(async environment => {
26+
if (environment.deployment_branch_policy) {
27+
if (environment.deployment_branch_policy.custom_branch_policies) {
28+
const branchPolicies = await this.getDeploymentBranchPolicies(
29+
this.repo.owner,
30+
this.repo.repo,
31+
environment.name
32+
)
33+
environment.deployment_branch_policy = {
34+
custom_branches: branchPolicies.map(_ => _.name)
35+
}
36+
} else {
37+
environment.deployment_branch_policy = {
38+
protected_branches: true
39+
}
40+
}
41+
}
42+
return {
43+
...environment,
44+
// Force all names to lowercase to avoid comparison issues.
45+
name: environment.name.toLowerCase()
46+
}
47+
})
48+
)
49+
}
50+
51+
comparator (existing, attrs) {
52+
return existing.name === attrs.name
53+
}
54+
55+
changed (existing, attrs) {
56+
if (!attrs.wait_timer) attrs.wait_timer = 0
57+
return (
58+
(existing.wait_timer || 0) !== attrs.wait_timer ||
59+
this.reviewersToString(existing.reviewers) !== this.reviewersToString(attrs.reviewers) ||
60+
this.deploymentBranchPolicyToString(existing.deployment_branch_policy) !==
61+
this.deploymentBranchPolicyToString(attrs.deployment_branch_policy)
62+
)
63+
}
64+
65+
async update (existing, attrs) {
66+
if (existing.deployment_branch_policy && existing.deployment_branch_policy.custom_branches) {
67+
const branchPolicies = await this.getDeploymentBranchPolicies(this.repo.owner, this.repo.repo, existing.name)
68+
await Promise.all(
69+
branchPolicies.map(branchPolicy =>
70+
this.github.request(
71+
'DELETE /repos/:org/:repo/environments/:environment_name/deployment-branch-policies/:id',
72+
{
73+
org: this.repo.owner,
74+
repo: this.repo.repo,
75+
environment_name: existing.name,
76+
id: branchPolicy.id
77+
}
78+
)
79+
)
80+
)
81+
}
82+
return this.add(attrs)
83+
}
84+
85+
async add (attrs) {
86+
await this.github.request(`PUT ${environmentRepoEndpoint}`, this.toParams({ name: attrs.name }, attrs))
87+
if (attrs.deployment_branch_policy && attrs.deployment_branch_policy.custom_branches) {
88+
await Promise.all(
89+
attrs.deployment_branch_policy.custom_branches.map(name =>
90+
this.github.request(`POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies`, {
91+
org: this.repo.owner,
92+
repo: this.repo.repo,
93+
environment_name: attrs.name,
94+
name
95+
})
96+
)
97+
)
98+
}
99+
}
100+
101+
remove (existing) {
102+
return this.github.request(`DELETE ${environmentRepoEndpoint}`, {
103+
environment_name: existing.name,
104+
repo: this.repo.repo,
105+
org: this.repo.owner
106+
})
107+
}
108+
109+
reviewersToString (attrs) {
110+
if (attrs === null || attrs === undefined) {
111+
return ''
112+
} else {
113+
attrs.sort((a, b) => {
114+
if (a.id < b.id) return -1
115+
if (a.id > b.id) return 1
116+
if (a.type < b.type) return -1
117+
if (a.type > b.type) return 1
118+
return 0
119+
})
120+
return JSON.stringify(
121+
attrs.map(reviewer => {
122+
return {
123+
id: reviewer.id,
124+
type: reviewer.type
125+
}
126+
})
127+
)
128+
}
129+
}
130+
131+
deploymentBranchPolicyToString (attrs) {
132+
if (attrs === null || attrs === undefined) {
133+
return ''
134+
} else {
135+
return JSON.stringify(
136+
this.shouldUseProtectedBranches(attrs.protected_branches, attrs.custom_branches)
137+
? { protected_branches: true }
138+
: { custom_branches: attrs.custom_branches.sort() }
139+
)
140+
}
141+
}
142+
143+
async getDeploymentBranchPolicies (owner, repo, environmentName) {
144+
const {
145+
data: { branch_policies: branchPolicies }
146+
} = await this.github.request('GET /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', {
147+
org: owner,
148+
repo,
149+
environment_name: environmentName
150+
})
151+
return branchPolicies
152+
}
153+
154+
toParams (existing, attrs) {
155+
const deploymentBranchPolicy = attrs.deployment_branch_policy
156+
? this.shouldUseProtectedBranches(
157+
attrs.deployment_branch_policy.protected_branches,
158+
attrs.deployment_branch_policy.custom_branches
159+
)
160+
? { protected_branches: true, custom_branch_policies: false }
161+
: { protected_branches: false, custom_branch_policies: true }
162+
: null
163+
return {
164+
environment_name: existing.name,
165+
repo: this.repo.repo,
166+
org: this.repo.owner,
167+
wait_timer: attrs.wait_timer,
168+
reviewers: attrs.reviewers,
169+
deployment_branch_policy: deploymentBranchPolicy
170+
}
171+
}
172+
173+
shouldUseProtectedBranches (protectedBranches, customBranchPolicies) {
174+
if (protectedBranches || customBranchPolicies === undefined || customBranchPolicies === null) {
175+
return true // Returning booleans like this to avoid unexpected datatypes that result in truthy values
176+
} else {
177+
return false
178+
}
179+
}
180+
}

lib/settings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Settings.PLUGINS = {
3838
repository: require('./plugins/repository'),
3939
labels: require('./plugins/labels'),
4040
collaborators: require('./plugins/collaborators'),
41+
environments: require('./plugins/environments'),
4142
teams: require('./plugins/teams'),
4243
milestones: require('./plugins/milestones'),
4344
branches: require('./plugins/branches')

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"lint:peer": "npm ls >/dev/null",
1717
"test:unit": "jest 'test/unit/'",
1818
"test:unit:watch": "npm run test:unit -- --watch",
19-
"test:integration": "jest --test-timeout=10000 'test/integration/'",
19+
"test:integration": "jest --test-timeout=20000 'test/integration/'",
2020
"test:integration:debug": "LOG_LEVEL=debug DEBUG=nock.* run-s test:integration"
2121
},
2222
"author": "Brandon Keepers",

test/fixtures/environments-config.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
environments:
2+
- name: changed-wait-timer
3+
wait_timer: 10
4+
5+
- name: changed-reviewers-type
6+
reviewers:
7+
- id: 1
8+
type: User
9+
- id: 2
10+
type: User
11+
12+
- name: changed-reviewers-id
13+
reviewers:
14+
- id: 1
15+
type: Team
16+
- id: 3
17+
type: User
18+
19+
- name: changed-reviewers-remove
20+
reviewers:
21+
- id: 1
22+
type: Team
23+
24+
- name: changed-reviewers-add
25+
reviewers:
26+
- id: 1
27+
type: Team
28+
- id: 2
29+
type: User
30+
- id: 3
31+
type: User
32+
33+
- name: changed-deployment-branch-policy
34+
deployment_branch_policy:
35+
custom_branches:
36+
- stage/*
37+
- uat/*
38+
39+
- name: changed-all
40+
wait_timer: 10
41+
reviewers:
42+
- id: 2
43+
type: User
44+
deployment_branch_policy:
45+
custom_branches:
46+
- dev/*
47+
48+
- name: new-environment
49+
wait_timer: 1
50+
reviewers:
51+
- id: 1
52+
type: Team
53+
- id: 2
54+
type: User
55+
deployment_branch_policy:
56+
protected_branches: true
57+
58+
- name: unchanged-default-wait-timer
59+
60+
- name: unchanged-wait-timer
61+
wait_timer: 1
62+
63+
- name: unchanged-reviewers-unsorted
64+
reviewers:
65+
- id: 2
66+
type: User
67+
- id: 1
68+
type: Team
69+
70+
- name: unchanged-reviewers-sorted
71+
reviewers:
72+
- id: 1
73+
type: Team
74+
- id: 2
75+
type: User
76+
77+
- name: unchanged-deployment-branch-policy
78+
deployment_branch_policy:
79+
custom_branches:
80+
- dev/*
81+
- dev-*

0 commit comments

Comments
 (0)