Skip to content

Commit a692dbf

Browse files
authored
Same repository that is part of multiple suborgs (#664)
* add initial code for preventing multiple suborg config for repos * detect conflict even when a single suborg config is changed * remove duplicate errors * dont add nulls and undefined to results * Update ESLint config for browser and standard
1 parent 20a4508 commit a692dbf

File tree

2 files changed

+226
-61
lines changed

2 files changed

+226
-61
lines changed

lib/settings.js

+96-54
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Settings {
2525
return settings
2626
}
2727

28-
static async syncSubOrgs (nop, context, suborg, repo, config, ref) {
28+
static async syncSubOrgs(nop, context, suborg, repo, config, ref) {
2929
const settings = new Settings(nop, context, repo, config, ref, suborg)
3030
try {
3131
await settings.loadConfigs()
@@ -37,7 +37,7 @@ class Settings {
3737
}
3838
}
3939

40-
static async sync (nop, context, repo, config, ref) {
40+
static async sync(nop, context, repo, config, ref) {
4141
const settings = new Settings(nop, context, repo, config, ref)
4242
try {
4343
await settings.loadConfigs(repo)
@@ -52,13 +52,13 @@ class Settings {
5252
}
5353
}
5454

55-
static async handleError (nop, context, repo, config, ref, nopcommand) {
55+
static async handleError(nop, context, repo, config, ref, nopcommand) {
5656
const settings = new Settings(nop, context, repo, config, ref)
5757
settings.appendToResults([nopcommand])
5858
await settings.handleResults()
5959
}
6060

61-
constructor (nop, context, repo, config, ref, suborg) {
61+
constructor(nop, context, repo, config, ref, suborg) {
6262
this.ref = ref
6363
this.context = context
6464
this.installation_id = context.payload.installation.id
@@ -97,7 +97,7 @@ class Settings {
9797
}
9898

9999
// Create a check in the Admin repo for safe-settings.
100-
async createCheckRun () {
100+
async createCheckRun() {
101101
const startTime = new Date()
102102
let conclusion = 'success'
103103
let details = `Run on: \`${new Date().toISOString()}\``
@@ -143,7 +143,7 @@ class Settings {
143143
})
144144
}
145145

146-
logError (msg) {
146+
logError(msg) {
147147
this.log.error(msg)
148148
this.errors.push({
149149
owner: this.repo.owner,
@@ -153,7 +153,7 @@ class Settings {
153153
})
154154
}
155155

156-
async handleResults () {
156+
async handleResults() {
157157
const { payload } = this.context
158158

159159
// Create a checkrun if not in nop mode
@@ -163,6 +163,13 @@ class Settings {
163163
return
164164
}
165165

166+
//remove duplicate rows in this.results
167+
this.results = this.results.filter((thing, index, self) => {
168+
return index === self.findIndex((t) => {
169+
return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin
170+
})
171+
})
172+
166173
let error = false
167174
// Different logic
168175
const stats = {
@@ -227,23 +234,23 @@ class Settings {
227234
#### :robot: Safe-Settings config changes detected:
228235
229236
${this.results.reduce((x, y) => {
230-
if (!y) {
231-
return x
232-
}
233-
if (y.type === 'ERROR') {
234-
error = true
235-
return `${x}
237+
if (!y) {
238+
return x
239+
}
240+
if (y.type === 'ERROR') {
241+
error = true
242+
return `${x}
236243
<tr><td> ❗ ${y.action.msg} </td><td> ${y.plugin} </td><td> ${prettify(y.repo)} </td><td> ${prettify(y.action.additions)} </td><td> ${prettify(y.action.deletions)} </td><td> ${prettify(y.action.modifications)} </td><tr>`
237-
} else if (y.action.additions === null && y.action.deletions === null && y.action.modifications === null) {
238-
return `${x}`
239-
} else {
240-
if (y.action === undefined) {
241-
return `${x}`
242-
}
243-
return `${x}
244+
} else if (y.action.additions === null && y.action.deletions === null && y.action.modifications === null) {
245+
return `${x}`
246+
} else {
247+
if (y.action === undefined) {
248+
return `${x}`
249+
}
250+
return `${x}
244251
<tr><td> ✋ </td><td> ${y.plugin} </td><td> ${prettify(y.repo)} </td><td> ${prettify(y.action.additions)} </td><td> ${prettify(y.action.deletions)} </td><td> ${prettify(y.action.modifications)} </td><tr>`
245-
}
246-
}, table)}
252+
}
253+
}, table)}
247254
`
248255

249256
const pullRequest = payload.check_run.check_suite.pull_requests[0]
@@ -273,12 +280,12 @@ ${this.results.reduce((x, y) => {
273280
await this.github.checks.update(params)
274281
}
275282

276-
async loadConfigs (repo) {
283+
async loadConfigs(repo) {
277284
this.subOrgConfigs = await this.getSubOrgConfigs()
278285
this.repoConfigs = await this.getRepoConfigs(repo)
279286
}
280287

281-
async updateOrg () {
288+
async updateOrg() {
282289
const rulesetsConfig = this.config.rulesets
283290
if (rulesetsConfig) {
284291
const RulesetsPlugin = Settings.PLUGINS.rulesets
@@ -288,7 +295,7 @@ ${this.results.reduce((x, y) => {
288295
}
289296
}
290297

291-
async updateRepos (repo) {
298+
async updateRepos(repo) {
292299
this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs()
293300
let repoConfig = this.config.repository
294301
if (repoConfig) {
@@ -354,15 +361,15 @@ ${this.results.reduce((x, y) => {
354361
}
355362
}
356363

357-
async updateAll () {
364+
async updateAll() {
358365
// this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs(this.github, this.repo, this.log)
359366
// this.repoConfigs = this.repoConfigs || await this.getRepoConfigs(this.github, this.repo, this.log)
360367
return this.eachRepositoryRepos(this.github, this.config.restrictedRepos, this.log).then(res => {
361368
this.appendToResults(res)
362369
})
363370
}
364371

365-
getSubOrgConfig (repoName) {
372+
getSubOrgConfig(repoName) {
366373
if (this.subOrgConfigs) {
367374
for (const k of Object.keys(this.subOrgConfigs)) {
368375
const repoPattern = new Glob(k)
@@ -375,13 +382,13 @@ ${this.results.reduce((x, y) => {
375382
}
376383

377384
// Remove Org specific configs from the repo config
378-
returnRepoSpecificConfigs (config) {
385+
returnRepoSpecificConfigs(config) {
379386
const newConfig = Object.assign({}, config) // clone
380387
delete newConfig.rulesets
381388
return newConfig
382389
}
383390

384-
childPluginsList (repo) {
391+
childPluginsList(repo) {
385392
const repoName = repo.repo
386393
const subOrgOverrideConfig = this.getSubOrgConfig(repoName)
387394
this.log.debug(`suborg config for ${repoName} is ${JSON.stringify(subOrgOverrideConfig)}`)
@@ -413,7 +420,7 @@ ${this.results.reduce((x, y) => {
413420
return childPlugins
414421
}
415422

416-
validate (section, baseConfig, overrideConfig) {
423+
validate(section, baseConfig, overrideConfig) {
417424
const configValidator = this.configvalidators[section]
418425
if (configValidator) {
419426
this.log.debug(`Calling configvalidator for key ${section} `)
@@ -432,7 +439,7 @@ ${this.results.reduce((x, y) => {
432439
}
433440
}
434441

435-
isRestricted (repoName) {
442+
isRestricted(repoName) {
436443
const restrictedRepos = this.config.restrictedRepos
437444
// Skip configuring any restricted repos
438445
if (Array.isArray(restrictedRepos)) {
@@ -464,11 +471,11 @@ ${this.results.reduce((x, y) => {
464471
return false
465472
}
466473

467-
includesRepo (repoName, restrictedRepos) {
474+
includesRepo(repoName, restrictedRepos) {
468475
return restrictedRepos.filter((restrictedRepo) => { return RegExp(restrictedRepo).test(repoName) }).length > 0
469476
}
470477

471-
async eachRepositoryRepos (github, restrictedRepos, log) {
478+
async eachRepositoryRepos(github, restrictedRepos, log) {
472479
log.debug('Fetching repositories')
473480
return github.paginate('GET /installation/repositories').then(repositories => {
474481
return Promise.all(repositories.map(repository => {
@@ -489,7 +496,7 @@ ${this.results.reduce((x, y) => {
489496
* @param params Params to fetch the file with
490497
* @return The parsed YAML file
491498
*/
492-
async loadConfigMap (params) {
499+
async loadConfigMap(params) {
493500
try {
494501
this.log.debug(` In loadConfigMap ${JSON.stringify(params)}`)
495502
const response = await this.github.repos.getContent(params).catch(e => {
@@ -536,7 +543,7 @@ ${this.results.reduce((x, y) => {
536543
* @param params Params to fetch the file with
537544
* @return The parsed YAML file
538545
*/
539-
async getRepoConfigMap () {
546+
async getRepoConfigMap() {
540547
try {
541548
this.log.debug(` In getRepoConfigMap ${JSON.stringify(this.repo)}`)
542549
// GitHub getContent api has a hard limit of returning 1000 entries without
@@ -603,7 +610,7 @@ ${this.results.reduce((x, y) => {
603610
* @param params Params to fetch the file with
604611
* @return The parsed YAML file
605612
*/
606-
async getSubOrgConfigMap () {
613+
async getSubOrgConfigMap() {
607614
try {
608615
this.log.debug(` In getSubOrgConfigMap ${JSON.stringify(this.repo)}`)
609616
const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO }
@@ -630,7 +637,7 @@ ${this.results.reduce((x, y) => {
630637
* @param {*} repo repo param
631638
* @returns repoConfigs object
632639
*/
633-
async getRepoConfigs (repo) {
640+
async getRepoConfigs(repo) {
634641
try {
635642
const overridePaths = await this.getRepoConfigMap()
636643
const repoConfigs = {}
@@ -682,12 +689,11 @@ ${this.results.reduce((x, y) => {
682689
* @param params Params to fetch the file with
683690
* @return The parsed YAML file
684691
*/
685-
async getSubOrgConfigs () {
692+
async getSubOrgConfigs() {
686693
try {
687-
if (this.subOrgConfigMap) {
688-
this.log.debug(`SubOrg config was changed and the associated overridePaths is = ${JSON.stringify(this.subOrgConfigMap)}`)
689-
}
690-
const overridePaths = this.subOrgConfigMap || await this.getSubOrgConfigMap()
694+
// Get all suborg configs even though we might be here becuase of a suborg config change
695+
// we will filter them out if request is due to a suborg config change
696+
const overridePaths = await this.getSubOrgConfigMap()
691697
const subOrgConfigs = {}
692698

693699
for (const override of overridePaths) {
@@ -699,7 +705,19 @@ ${this.results.reduce((x, y) => {
699705
subOrgConfigs[override.name] = data
700706
if (data.suborgrepos) {
701707
data.suborgrepos.forEach(repository => {
702-
subOrgConfigs[repository] = data
708+
this.storeSubOrgConfigIfNoConflicts(subOrgConfigs, override.path, repository, data)
709+
710+
// In case support for multiple suborg configs for the same repo is required, merge the configs.
711+
//
712+
// Planned for the future to support multiple suborgrepos for the same repo
713+
//
714+
// if (existingConfigForRepo) {
715+
// subOrgConfigs[repository] = this.mergeDeep.mergeDeep({}, existingConfigForRepo, data)
716+
// } else {
717+
// subOrgConfigs[repository] = data
718+
// }
719+
720+
subOrgConfigs[repository] = Object.assign({}, data, { source: override.path })
703721
})
704722
}
705723
if (data.suborgteams) {
@@ -709,7 +727,7 @@ ${this.results.reduce((x, y) => {
709727
await Promise.all(promises).then(res => {
710728
res.forEach(r => {
711729
r.forEach(e => {
712-
subOrgConfigs[e.name] = data
730+
this.storeSubOrgConfigIfNoConflicts(subOrgConfigs, override.path, e.name, data)
713731
})
714732
})
715733
})
@@ -721,12 +739,26 @@ ${this.results.reduce((x, y) => {
721739
await Promise.all(promises).then(res => {
722740
res.forEach(r => {
723741
r.forEach(e => {
724-
subOrgConfigs[e.repository_name] = data
742+
this.storeSubOrgConfigIfNoConflicts(subOrgConfigs, override.path, e.repository_name, data)
725743
})
726744
})
727745
})
728746
}
729747
}
748+
749+
// If this was result of a suborg config change, only return the repos that are part of the suborg config
750+
if (this.subOrgConfigMap) {
751+
this.log.debug(`SubOrg config was changed and the associated overridePaths is = ${JSON.stringify(this.subOrgConfigMap)}`)
752+
// enumerate the properties of the subOrgConfigs object and delete the ones that are not part of the suborg
753+
for (const [key, value] of Object.entries(subOrgConfigs)) {
754+
if (!this.subOrgConfigMap.some((overridePath) => {
755+
return overridePath.path === value.source
756+
}
757+
)) {
758+
delete subOrgConfigs[key]
759+
}
760+
}
761+
}
730762
return subOrgConfigs
731763
} catch (e) {
732764
if (this.nop) {
@@ -740,13 +772,21 @@ ${this.results.reduce((x, y) => {
740772
}
741773
}
742774

775+
storeSubOrgConfigIfNoConflicts(subOrgConfigs, overridePath, repoName, data) {
776+
const existingConfigForRepo = subOrgConfigs[repoName]
777+
if (existingConfigForRepo && existingConfigForRepo.source !== overridePath) {
778+
throw new Error(`Multiple suborg configs for ${repoName} in ${overridePath} and ${existingConfigForRepo?.source}`)
779+
}
780+
subOrgConfigs[repoName] = Object.assign({}, data, { source: overridePath })
781+
}
782+
743783
/**
744784
* Loads a file from GitHub
745785
*
746786
* @param params Params to fetch the file with
747787
* @return The parsed YAML file
748788
*/
749-
async loadYaml (filePath) {
789+
async loadYaml(filePath) {
750790
try {
751791
const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO }
752792
const params = Object.assign(repo, { path: filePath, ref: this.ref })
@@ -783,13 +823,16 @@ ${this.results.reduce((x, y) => {
783823
}
784824
}
785825

786-
appendToResults (res) {
826+
appendToResults(res) {
787827
if (this.nop) {
788-
this.results = this.results.concat(res.flat(3))
828+
//Remove nulls and undefined from the results
829+
const results = res.flat(3).filter(r => r)
830+
831+
this.results = this.results.concat(results)
789832
}
790833
}
791834

792-
async getReposForTeam (teamslug) {
835+
async getReposForTeam(teamslug) {
793836
const options = this.github.rest.teams.listReposInOrg.endpoint.merge({
794837
org: this.repo.owner,
795838
team_slug: teamslug,
@@ -798,20 +841,19 @@ ${this.results.reduce((x, y) => {
798841
return this.github.paginate(options)
799842
}
800843

801-
async getReposForCustomProperty (customPropertyTuple) {
802-
const name=Object.keys(customPropertyTuple)[0]
844+
async getReposForCustomProperty(customPropertyTuple) {
845+
const name = Object.keys(customPropertyTuple)[0]
803846
let q = `props.${name}:${customPropertyTuple[name]}`
804847
q = encodeURIComponent(q)
805848
const options = this.github.request.endpoint((`/orgs/${this.repo.owner}/properties/values?repository_query=${q}`))
806849
return this.github.paginate(options)
807850
}
808851

809-
810-
isObject (item) {
852+
isObject(item) {
811853
return (item && typeof item === 'object' && !Array.isArray(item))
812854
}
813855

814-
isIterable (obj) {
856+
isIterable(obj) {
815857
// checks for null and undefined
816858
if (obj == null) {
817859
return false

0 commit comments

Comments
 (0)