Skip to content

Commit 19e2691

Browse files
authored
Environments tolerate concise configuration (#649)
* decompose unit tests, patch sync for environments * remove logging, combine loops as per review comments * Add NopCommand, log.error, and errors * Allow concise config for Environments This commit combines PR [616](#616) and [646](#646) environments.js Add defensive code to prevent the GitHub API from being called with undefined data. In the UI, and API an environment can be added with just an name. Now, safe-settings permits this as well. In the UI, and API an environment can be added without variables. Now, safe-settings permits this as well. In the UI, and API an environment can be added without deployment_protection_rules. Now, safe-settings permits this as well. environments.test.js Add a test case for the scenario when there are zero existing environments Add a test case for an environment name change Add a test case inspired by PR 616 which adds 7 new environments with various attributes Move expect statements out of aftereach() as there is now variability in what is expected across test cases. Specifically, when there is no existing environment, that environment should NOT be queried for variables nor deployment_protection_rules * Update documentation: Environments permissions. Addresses issue: [Environments do not get provisioned for repositories set to internal or private #623](#623) Adds documentation for permissions required for safe-settings when Environments are used [List Environments](https://docs.github.com/en/rest/deployments/environments?apiVersion=2022-11-28#list-environments) API requires: ``` The fine-grained token must have the following permission set: "Actions" repository permissions (read) ``` [Create an environment variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#create-an-environment-variable) API requires: ``` The fine-grained token must have the following permission set: "Variables" repository permissions (write) and "Environments" repository permissions (write) ``` With permissions added, issue 623 was resolved.
1 parent f6c8d43 commit 19e2691

File tree

5 files changed

+1160
-260
lines changed

5 files changed

+1160
-260
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ And the `checkrun` page will look like this:
266266
<img width="860" alt="image" src="https://github.com/github/safe-settings/assets/57544838/893ff4e6-904c-4a07-924a-7c23dc068983">
267267
</p>
268268

269-
### The Settings File
269+
### The Settings Files
270270

271-
The settings file can be used to set the policies at the `org`, `suborg` or `repo` level.
271+
The settings files can be used to set the policies at the `org`, `suborg` or `repo` level.
272272

273273
The following can be configured:
274274

@@ -284,6 +284,7 @@ The following can be configured:
284284
- `Autolinks`
285285
- `Repository name validation` using regex pattern
286286
- `Rulesets`
287+
- `Environments` - wait timer, required reviewers, prevent self review, protected branches deployment branch policy, custom deployment branch policy, variables, deployment protection rules
287288

288289
It is possible to provide an `include` or `exclude` settings to restrict the `collaborators`, `teams`, `labels` to a list of repos or exclude a set of repos for a collaborator.
289290

app.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ default_permissions:
3434
repository_custom_properties: write
3535
organization_custom_properties: admin
3636

37+
# Workflows, workflow runs and artifacts. (needed to read environments when repo is private or internal)
38+
# https://developer.github.com/v3/apps/permissions/#repository-permissions-for-actions
39+
actions: read
40+
3741
# Repository creation, deletion, settings, teams, and collaborators.
3842
# https://developer.github.com/v3/apps/permissions/#permission-on-administration
3943
administration: write
@@ -50,6 +54,10 @@ default_permissions:
5054
# https://developer.github.com/v3/apps/permissions/#permission-on-deployments
5155
# deployments: read
5256

57+
# Manage repository environments.
58+
# https://developer.github.com/v3/apps/permissions/#repository-permissions-for-environments
59+
environments: write
60+
5361
# Issues and related comments, assignees, labels, and milestones.
5462
# https://developer.github.com/v3/apps/permissions/#permission-on-issues
5563
issues: write
@@ -106,6 +114,10 @@ default_permissions:
106114
# https://developer.github.com/v3/apps/permissions/
107115
organization_administration: write
108116

117+
# Manage Actions repository variables.
118+
# https://developer.github.com/v3/apps/permissions/#repository-permissions-for-variables
119+
variables: write
120+
109121

110122
# The name of the GitHub App. Defaults to the name specified in package.json
111123
name: Safe Settings

docs/deploy.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,17 @@ Every deployment will need an [App](https://developer.github.com/apps/).
255255
256256
#### Repository Permissions
257257
258+
- Actions: **Read-only**
258259
- Administration: **Read & Write**
259260
- Checks: **Read & Write**
260261
- Commit statuses: **Read & Write**
261262
- Contents: **Read & Write**
262263
- Custom properties: **Read & Write**
264+
- Environments: **Read & Write**
263265
- Issues: **Read & Write**
264266
- Metadata: **Read-only**
265267
- Pull requests: **Read & Write**
268+
- Variables: **Read & Write**
266269
267270
#### Organization Permissions
268271

lib/plugins/environments.js

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const Diffable = require('./diffable')
2+
const MergeDeep = require('../mergeDeep')
3+
const NopCommand = require('../nopcommand')
24

35
module.exports = class Environments extends Diffable {
46
constructor(...args) {
@@ -14,7 +16,11 @@ module.exports = class Environments extends Diffable {
1416
});
1517
}
1618
})
17-
}
19+
}
20+
21+
// Remove 'name' from filtering list so Environments with only a name defined are processed.
22+
MergeDeep.NAME_FIELDS.splice(MergeDeep.NAME_FIELDS.indexOf('name'), 1)
23+
1824
}
1925

2026
async find() {
@@ -78,7 +84,7 @@ module.exports = class Environments extends Diffable {
7884
const wait_timer = existing.wait_timer !== attrs.wait_timer;
7985
const prevent_self_review = existing.prevent_self_review !== attrs.prevent_self_review;
8086
const reviewers = JSON.stringify(existing.reviewers.sort((x1, x2) => x1.id - x2.id)) !== JSON.stringify(attrs.reviewers.sort((x1, x2) => x1.id - x2.id));
81-
87+
8288
let existing_custom_branch_policies = existing.deployment_branch_policy === null ? null : existing.deployment_branch_policy.custom_branch_policies;
8389
if(typeof(existing_custom_branch_policies) === 'object' && existing_custom_branch_policies !== null) {
8490
existing_custom_branch_policies = existing_custom_branch_policies.sort();
@@ -158,6 +164,7 @@ module.exports = class Environments extends Diffable {
158164

159165
if(variables) {
160166
let existingVariables = [...existing.variables];
167+
161168
for(let variable of attrs.variables) {
162169
const existingVariable = existingVariables.find((_var) => _var.name === variable.name);
163170
if(existingVariable) {
@@ -195,6 +202,7 @@ module.exports = class Environments extends Diffable {
195202

196203
if(deployment_protection_rules) {
197204
let existingRules = [...existing.deployment_protection_rules];
205+
198206
for(let rule of attrs.deployment_protection_rules) {
199207
const existingRule = existingRules.find((_rule) => _rule.id === rule.id);
200208

@@ -227,13 +235,14 @@ module.exports = class Environments extends Diffable {
227235
wait_timer: attrs.wait_timer,
228236
prevent_self_review: attrs.prevent_self_review,
229237
reviewers: attrs.reviewers,
230-
deployment_branch_policy: attrs.deployment_branch_policy === null ? null : {
231-
protected_branches: attrs.deployment_branch_policy.protected_branches,
238+
deployment_branch_policy: attrs.deployment_branch_policy == null ? null : {
239+
protected_branches: !!attrs.deployment_branch_policy.protected_branches,
232240
custom_branch_policies: !!attrs.deployment_branch_policy.custom_branch_policies
233241
}
234242
});
235243

236244
if(attrs.deployment_branch_policy && attrs.deployment_branch_policy.custom_branch_policies) {
245+
237246
for(let policy of attrs.deployment_branch_policy.custom_branch_policies) {
238247
await this.github.request('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', {
239248
org: this.repo.owner,
@@ -242,26 +251,34 @@ module.exports = class Environments extends Diffable {
242251
name: policy.name
243252
});
244253
}
245-
}
246-
247254

248-
for(let variable of attrs.variables) {
249-
await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/variables`, {
250-
org: this.repo.owner,
251-
repo: this.repo.repo,
252-
environment_name: attrs.name,
253-
name: variable.name,
254-
value: variable.value
255-
});
256255
}
257256

258-
for(let rule of attrs.deployment_protection_rules) {
259-
await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules`, {
260-
org: this.repo.owner,
261-
repo: this.repo.repo,
262-
environment_name: attrs.name,
263-
integration_id: rule.app_id
264-
});
257+
if(attrs.variables) {
258+
259+
for(let variable of attrs.variables) {
260+
await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/variables`, {
261+
org: this.repo.owner,
262+
repo: this.repo.repo,
263+
environment_name: attrs.name,
264+
name: variable.name,
265+
value: variable.value
266+
});
267+
}
268+
269+
}
270+
271+
if(attrs.deployment_protection_rules) {
272+
273+
for(let rule of attrs.deployment_protection_rules) {
274+
await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules`, {
275+
org: this.repo.owner,
276+
repo: this.repo.repo,
277+
environment_name: attrs.name,
278+
integration_id: rule.app_id
279+
});
280+
}
281+
265282
}
266283
}
267284

@@ -272,4 +289,79 @@ module.exports = class Environments extends Diffable {
272289
environment_name: existing.name
273290
});
274291
}
275-
}
292+
293+
sync () {
294+
const resArray = []
295+
if (this.entries) {
296+
let filteredEntries = this.filterEntries()
297+
return this.find().then(existingRecords => {
298+
299+
// Filter out all empty entries (usually from repo override)
300+
for (const entry of filteredEntries) {
301+
for (const key of Object.keys(entry)) {
302+
if (entry[key] === null || entry[key] === undefined) {
303+
delete entry[key]
304+
}
305+
}
306+
}
307+
filteredEntries = filteredEntries.filter(entry => Object.keys(entry).filter(key => !MergeDeep.NAME_FIELDS.includes(key)).length !== 0)
308+
309+
const changes = []
310+
311+
existingRecords.forEach(x => {
312+
if (!filteredEntries.find(y => this.comparator(x, y))) {
313+
const change = this.remove(x).then(res => {
314+
if (this.nop) {
315+
return resArray.push(res)
316+
}
317+
return res
318+
})
319+
changes.push(change)
320+
}
321+
})
322+
323+
filteredEntries.forEach(attrs => {
324+
const existing = existingRecords.find(record => {
325+
return this.comparator(record, attrs)
326+
})
327+
328+
if (!existing) {
329+
const change = this.add(attrs).then(res => {
330+
if (this.nop) {
331+
return resArray.push(res)
332+
}
333+
return res
334+
})
335+
changes.push(change)
336+
} else if (this.changed(existing, attrs)) {
337+
const change = this.update(existing, attrs).then(res => {
338+
if (this.nop) {
339+
return resArray.push(res)
340+
}
341+
return res
342+
})
343+
changes.push(change)
344+
}
345+
})
346+
347+
if (this.nop) {
348+
return Promise.resolve(resArray)
349+
}
350+
return Promise.all(changes)
351+
}).catch(e => {
352+
if (this.nop) {
353+
if (e.status === 404) {
354+
// Ignore 404s which can happen in dry-run as the repo may not exist.
355+
return Promise.resolve(resArray)
356+
} else {
357+
resArray.push(new NopCommand(this.constructor.name, this.repo, null, `error ${e} in ${this.constructor.name} for repo: ${JSON.stringify(this.repo)} entries ${JSON.stringify(this.entries)}`, 'ERROR'))
358+
return Promise.resolve(resArray)
359+
}
360+
} else {
361+
this.logError(`Error ${e} in ${this.constructor.name} for repo: ${JSON.stringify(this.repo)} entries ${JSON.stringify(this.entries)}`)
362+
}
363+
})
364+
}
365+
}
366+
367+
}

0 commit comments

Comments
 (0)