Skip to content

Commit 87b8a95

Browse files
committed
Merge branch 'main-enterprise' into yj-repo-in-many-suborgs
2 parents 775b7ca + 85aae4f commit 87b8a95

13 files changed

+1991
-659
lines changed
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Delete old releases
2+
permissions: write-all
3+
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
beforeDate:
8+
type: string
9+
required: true
10+
description: YYYY-MM-DD - All releases before this date are deleted.
11+
default: "2024-01-01"
12+
13+
jobs:
14+
delete-releases:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Delete releases
18+
run: |
19+
for i in $(gh release list --repo https://github.com/$GITHUB_REPOSITORY --json createdAt,tagName --limit 1000 | jq --arg date $BEFORE_DATE '.[] | select(.createdAt < $date ) | .tagName' | tr -d '"'); do gh release delete $i -y --cleanup-tag --repo https://github.com/$GITHUB_REPOSITORY ; done
20+
echo Deleted releases before $BEFORE_DATE in https://github.com/$GITHUB_REPOSITORY >> $GITHUB_STEP_SUMMARY
21+
env:
22+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23+
BEFORE_DATE: ${{ inputs.beforeDate }}
24+
25+

README.md

+3-2
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

+12
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

+3
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/glob.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
class Glob {
22
constructor (glob) {
33
this.glob = glob
4-
const regexptex = glob.replace(/\//g, '\\/').replace(/\?/g, '([^\\/])').replace(/\./g, '\\.').replace(/\*/g, '([^\\/]*)')
5-
this.regexp = new RegExp(`^${regexptex}$`, 'u')
4+
5+
// If not a glob pattern then just match the string.
6+
if (!this.glob.includes('*')) {
7+
this.regexp = new RegExp(`.*${this.glob}.*`, 'u')
8+
return
9+
}
10+
this.regexptText = this.globize(this.glob)
11+
this.regexp = new RegExp(`^${this.regexptText}$`, 'u')
12+
}
13+
14+
globize (glob) {
15+
return glob
16+
.replace(/\\/g, '\\\\') // escape backslashes
17+
.replace(/\//g, '\\/') // escape forward slashes
18+
.replace(/\./g, '\\.') // escape periods
19+
.replace(/\?/g, '([^\\/])') // match any single character except /
20+
.replace(/\*\*/g, '.+') // match any character except /, including /
21+
.replace(/\*/g, '([^\\/]*)') // match any character except /
622
}
723

824
toString () {

lib/mergeDeep.js

+26-13
Original file line numberDiff line numberDiff line change
@@ -211,18 +211,19 @@ class MergeDeep {
211211
}
212212
}
213213
const combined = []
214-
for (const fields of Object.keys(visited)) {
215-
combined.push(visited[fields])
216-
}
217-
// Elements that are not in target are additions
218-
additions[key] = combined.filter(item => {
219-
if (this.isObjectNotArray(item)) {
220-
return !target.some(targetItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(targetItem))
221-
} else {
222-
return !target.includes(item)
214+
if (Object.keys(visited).length !== 0) {
215+
for (const fields of Object.keys(visited)) {
216+
combined.push(visited[fields])
223217
}
224-
})
225-
218+
// Elements that are not in target are additions
219+
additions[key] = combined.filter(item => {
220+
if (this.isObjectNotArray(item)) {
221+
return !target.some(targetItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(targetItem))
222+
} else {
223+
return !target.includes(item)
224+
}
225+
})
226+
}
226227
// Elements that not in source are deletions
227228
if (combined.length > 0) {
228229
// Elements that not in source are deletions
@@ -247,8 +248,21 @@ class MergeDeep {
247248
this.compareDeep(a, visited[id], additions[additions.length - 1], modifications[modifications.length - 1], deletions[deletions.length - 1])
248249
}
249250
// Any addtions for the matching key must be moved to modifications
251+
const lastAddition = additions[additions.length - 1]
252+
const lastModification = modifications[modifications.length - 1]
253+
250254
if (!this.isEmpty(additions)) {
251-
modifications = modifications.concat(additions)
255+
for (const key in lastAddition) {
256+
if (!lastModification[key]) {
257+
lastModification[key] = Array.isArray(lastAddition[key]) ? [] : {}
258+
}
259+
if (!Array.isArray(lastAddition[key])) {
260+
Object.assign(lastModification[key], lastAddition[key])
261+
} else {
262+
lastModification[key].push(...lastAddition[key])
263+
}
264+
}
265+
additions.length = 0
252266
}
253267
// Add name attribute to the modifications to make it look better ; it won't be added otherwise as it would be the same
254268
if (!this.isEmpty(modifications[modifications.length - 1])) {
@@ -364,5 +378,4 @@ class MergeDeep {
364378
}
365379
}
366380
MergeDeep.NAME_FIELDS = NAME_FIELDS
367-
MergeDeep.NAME_FIELDS = NAME_FIELDS
368381
module.exports = MergeDeep

lib/plugins/diffable.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,15 @@ module.exports = class Diffable extends ErrorStash {
9393
}
9494
}
9595

96-
// Filter out all empty entries (usually from repo override)
96+
// Remove any null or undefined values from the diffables (usually comes from repo override)
9797
for (const entry of filteredEntries) {
9898
for (const key of Object.keys(entry)) {
9999
if (entry[key] === null || entry[key] === undefined) {
100100
delete entry[key]
101101
}
102102
}
103103
}
104+
// Delete any diffable that now only has name and no other attributes
104105
filteredEntries = filteredEntries.filter(entry => Object.keys(entry).filter(key => !MergeDeep.NAME_FIELDS.includes(key)).length !== 0)
105106

106107
const changes = []

lib/plugins/environments.js

+110-22
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,7 @@ module.exports = class Environments extends Diffable {
1416
});
1517
}
1618
})
17-
}
19+
}
1820
}
1921

2022
async find() {
@@ -78,7 +80,7 @@ module.exports = class Environments extends Diffable {
7880
const wait_timer = existing.wait_timer !== attrs.wait_timer;
7981
const prevent_self_review = existing.prevent_self_review !== attrs.prevent_self_review;
8082
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-
83+
8284
let existing_custom_branch_policies = existing.deployment_branch_policy === null ? null : existing.deployment_branch_policy.custom_branch_policies;
8385
if(typeof(existing_custom_branch_policies) === 'object' && existing_custom_branch_policies !== null) {
8486
existing_custom_branch_policies = existing_custom_branch_policies.sort();
@@ -158,6 +160,7 @@ module.exports = class Environments extends Diffable {
158160

159161
if(variables) {
160162
let existingVariables = [...existing.variables];
163+
161164
for(let variable of attrs.variables) {
162165
const existingVariable = existingVariables.find((_var) => _var.name === variable.name);
163166
if(existingVariable) {
@@ -195,6 +198,7 @@ module.exports = class Environments extends Diffable {
195198

196199
if(deployment_protection_rules) {
197200
let existingRules = [...existing.deployment_protection_rules];
201+
198202
for(let rule of attrs.deployment_protection_rules) {
199203
const existingRule = existingRules.find((_rule) => _rule.id === rule.id);
200204

@@ -227,13 +231,14 @@ module.exports = class Environments extends Diffable {
227231
wait_timer: attrs.wait_timer,
228232
prevent_self_review: attrs.prevent_self_review,
229233
reviewers: attrs.reviewers,
230-
deployment_branch_policy: attrs.deployment_branch_policy === null ? null : {
231-
protected_branches: attrs.deployment_branch_policy.protected_branches,
234+
deployment_branch_policy: attrs.deployment_branch_policy == null ? null : {
235+
protected_branches: !!attrs.deployment_branch_policy.protected_branches,
232236
custom_branch_policies: !!attrs.deployment_branch_policy.custom_branch_policies
233237
}
234238
});
235239

236240
if(attrs.deployment_branch_policy && attrs.deployment_branch_policy.custom_branch_policies) {
241+
237242
for(let policy of attrs.deployment_branch_policy.custom_branch_policies) {
238243
await this.github.request('POST /repos/:org/:repo/environments/:environment_name/deployment-branch-policies', {
239244
org: this.repo.owner,
@@ -242,26 +247,34 @@ module.exports = class Environments extends Diffable {
242247
name: policy.name
243248
});
244249
}
245-
}
246-
247250

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-
});
256251
}
257252

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-
});
253+
if(attrs.variables) {
254+
255+
for(let variable of attrs.variables) {
256+
await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/variables`, {
257+
org: this.repo.owner,
258+
repo: this.repo.repo,
259+
environment_name: attrs.name,
260+
name: variable.name,
261+
value: variable.value
262+
});
263+
}
264+
265+
}
266+
267+
if(attrs.deployment_protection_rules) {
268+
269+
for(let rule of attrs.deployment_protection_rules) {
270+
await this.github.request(`POST /repos/:org/:repo/environments/:environment_name/deployment_protection_rules`, {
271+
org: this.repo.owner,
272+
repo: this.repo.repo,
273+
environment_name: attrs.name,
274+
integration_id: rule.app_id
275+
});
276+
}
277+
265278
}
266279
}
267280

@@ -272,4 +285,79 @@ module.exports = class Environments extends Diffable {
272285
environment_name: existing.name
273286
});
274287
}
275-
}
288+
289+
sync () {
290+
const resArray = []
291+
if (this.entries) {
292+
let filteredEntries = this.filterEntries()
293+
return this.find().then(existingRecords => {
294+
295+
// Remove any null or undefined values from the diffables (usually comes from repo override)
296+
for (const entry of filteredEntries) {
297+
for (const key of Object.keys(entry)) {
298+
if (entry[key] === null || entry[key] === undefined) {
299+
delete entry[key]
300+
}
301+
}
302+
}
303+
// For environments, we want to keep the entries with only name defined.
304+
305+
const changes = []
306+
307+
existingRecords.forEach(x => {
308+
if (!filteredEntries.find(y => this.comparator(x, y))) {
309+
const change = this.remove(x).then(res => {
310+
if (this.nop) {
311+
return resArray.push(res)
312+
}
313+
return res
314+
})
315+
changes.push(change)
316+
}
317+
})
318+
319+
filteredEntries.forEach(attrs => {
320+
const existing = existingRecords.find(record => {
321+
return this.comparator(record, attrs)
322+
})
323+
324+
if (!existing) {
325+
const change = this.add(attrs).then(res => {
326+
if (this.nop) {
327+
return resArray.push(res)
328+
}
329+
return res
330+
})
331+
changes.push(change)
332+
} else if (this.changed(existing, attrs)) {
333+
const change = this.update(existing, attrs).then(res => {
334+
if (this.nop) {
335+
return resArray.push(res)
336+
}
337+
return res
338+
})
339+
changes.push(change)
340+
}
341+
})
342+
343+
if (this.nop) {
344+
return Promise.resolve(resArray)
345+
}
346+
return Promise.all(changes)
347+
}).catch(e => {
348+
if (this.nop) {
349+
if (e.status === 404) {
350+
// Ignore 404s which can happen in dry-run as the repo may not exist.
351+
return Promise.resolve(resArray)
352+
} else {
353+
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'))
354+
return Promise.resolve(resArray)
355+
}
356+
} else {
357+
this.logError(`Error ${e} in ${this.constructor.name} for repo: ${JSON.stringify(this.repo)} entries ${JSON.stringify(this.entries)}`)
358+
}
359+
})
360+
}
361+
}
362+
363+
}

0 commit comments

Comments
 (0)