Skip to content

Commit 84229c7

Browse files
feat: enable some settings to be marked unsafe, settable in the repositories themselves
1 parent 620cf14 commit 84229c7

File tree

9 files changed

+330
-142
lines changed

9 files changed

+330
-142
lines changed

docs/sample-settings/sample-deployment-settings.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ overridevalidators:
2727
Some error
2828
script: |
2929
return true
30+
unsafeFields:
31+
# You can specify the fields that are allowed to be controlled by individual repositories
32+
- /repository/description
33+
- /repository/allow_auto_merge
34+

index.js

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -258,41 +258,48 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
258258
const { payload } = context
259259
const { repository } = payload
260260

261-
const adminRepo = repository.name === env.ADMIN_REPO
262-
if (!adminRepo) {
263-
return
264-
}
265-
266261
const defaultBranch = payload.ref === 'refs/heads/' + repository.default_branch
267262
if (!defaultBranch) {
268263
robot.log.debug('Not working on the default branch, returning...')
269264
return
270265
}
271266

272-
const settingsModified = payload.commits.find(commit => {
273-
return commit.added.includes(Settings.FILE_NAME) ||
274-
commit.modified.includes(Settings.FILE_NAME)
275-
})
276-
if (settingsModified) {
277-
robot.log.debug(`Changes in '${Settings.FILE_NAME}' detected, doing a full synch...`)
278-
return syncAllSettings(false, context)
279-
}
267+
const adminRepo = repository.name === env.ADMIN_REPO
268+
if (adminRepo) {
269+
const settingsModified = payload.commits.find(commit => {
270+
return commit.added.includes(Settings.FILE_NAME) ||
271+
commit.modified.includes(Settings.FILE_NAME)
272+
})
273+
if (settingsModified) {
274+
robot.log.debug(`Changes in '${Settings.FILE_NAME}' detected, doing a full synch...`)
275+
return syncAllSettings(false, context)
276+
}
280277

281-
const repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner)
282-
if (repoChanges.length > 0) {
283-
return Promise.all(repoChanges.map(repo => {
284-
return syncSettings(false, context, repo)
285-
}))
286-
}
278+
const repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner)
279+
if (repoChanges.length > 0) {
280+
return Promise.all(repoChanges.map(repo => {
281+
return syncSettings(false, context, repo)
282+
}))
283+
}
287284

288-
const changes = getAllChangedSubOrgConfigs(payload)
289-
if (changes.length) {
290-
return Promise.all(changes.map(suborg => {
291-
return syncSubOrgSettings(false, context, suborg)
292-
}))
293-
}
285+
const changes = getAllChangedSubOrgConfigs(payload)
286+
if (changes.length) {
287+
return Promise.all(changes.map(suborg => {
288+
return syncSubOrgSettings(false, context, suborg)
289+
}))
290+
}
294291

295-
robot.log.debug(`No changes in '${Settings.FILE_NAME}' detected, returning...`)
292+
robot.log.debug(`No changes in '${Settings.FILE_NAME}' detected, returning...`)
293+
} else {
294+
const settingsModified = payload.commits.find(commit => {
295+
return commit.added.includes('.github/settings.yml') ||
296+
commit.modified.includes('.github/settings.yml')
297+
})
298+
if (settingsModified) {
299+
robot.log.debug(`Changes in '.github/settings.yml' detected, doing a sync for ${repository.name}...`)
300+
return syncSettings(false, context)
301+
}
302+
}
296303
})
297304

298305
robot.on('create', async context => {

lib/configManager.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,20 @@ module.exports = class ConfigManager {
1010
}
1111

1212
/**
13-
* Loads a file from GitHub
14-
*
15-
* @param params Params to fetch the file with
16-
* @return The parsed YAML file
17-
*/
18-
async loadYaml (filePath) {
13+
* Loads a file from GitHub
14+
*
15+
* @param params Params to fetch the file with
16+
* @return The parsed YAML file
17+
*/
18+
static async loadYaml (octokit, params) {
1919
try {
20-
const repo = { owner: this.context.repo().owner, repo: env.ADMIN_REPO }
21-
const params = Object.assign(repo, { path: filePath, ref: this.ref })
22-
const response = await this.context.octokit.repos.getContent(params).catch(e => {
23-
this.log.error(`Error getting settings ${e}`)
24-
})
20+
const response = await octokit.repos.getContent(params)
2521

2622
// Ignore in case path is a folder
2723
// - https://developer.github.com/v3/repos/contents/#response-if-content-is-a-directory
2824
if (Array.isArray(response.data)) {
2925
return null
3026
}
31-
3227
// we don't handle symlinks or submodule
3328
// - https://developer.github.com/v3/repos/contents/#response-if-content-is-a-symlink
3429
// - https://developer.github.com/v3/repos/contents/#response-if-content-is-a-submodule
@@ -45,14 +40,18 @@ module.exports = class ConfigManager {
4540
}
4641

4742
/**
48-
* Loads a file from GitHub
43+
* Loads the settings file from GitHub
4944
*
50-
* @param params Params to fetch the file with
5145
* @return The parsed YAML file
5246
*/
5347
async loadGlobalSettingsYaml () {
5448
const CONFIG_PATH = env.CONFIG_PATH
5549
const filePath = path.posix.join(CONFIG_PATH, env.SETTINGS_FILE_PATH)
56-
return this.loadYaml(filePath)
50+
return ConfigManager.loadYaml(this.context.octokit, {
51+
owner: this.context.repo().owner,
52+
repo: env.ADMIN_REPO,
53+
path: filePath,
54+
ref: this.ref
55+
})
5756
}
5857
}

lib/deploymentConfig.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class DeploymentConfig {
1111
// static config
1212
static configvalidators = {}
1313
static overridevalidators = {}
14+
static unsafeFields = []
1415

1516
static {
1617
const deploymentConfigPath = process.env.DEPLOYMENT_CONFIG_FILE ? process.env.DEPLOYMENT_CONFIG_FILE : 'deployment-settings.yml'
@@ -36,6 +37,15 @@ class DeploymentConfig {
3637
this.configvalidators[validator.plugin] = { isValid: f, error: validator.error }
3738
}
3839
}
40+
41+
const unsafeFields = this.config.unsafeFields
42+
if (unsafeFields) {
43+
if (this.isIterable(unsafeFields)) {
44+
this.unsafeFields = unsafeFields
45+
} else {
46+
throw new Error('unsafeFields must be an array')
47+
}
48+
}
3949
}
4050

4151
static isNonEmptyArray (obj) {

lib/settings.js

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
const path = require('path')
22
const { Eta } = require('eta')
3+
const jsonPointer = require('json-pointer')
4+
const lodashSet = require('lodash.set')
35
const commetMessageTemplate = require('./commentmessage')
46
const errorTemplate = require('./error')
7+
const ConfigManager = require('./configManager')
58
const Glob = require('./glob')
69
const NopCommand = require('./nopcommand')
710
const MergeDeep = require('./mergeDeep')
@@ -298,11 +301,6 @@ ${this.results.reduce((x, y) => {
298301

299302
async updateRepos(repo) {
300303
this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs()
301-
let repoConfig = this.config.repository
302-
if (repoConfig) {
303-
repoConfig = Object.assign(repoConfig, { name: repo.repo, org: repo.owner })
304-
}
305-
306304
const subOrgConfig = this.getSubOrgConfig(repo.repo)
307305

308306
// If suborg config has been updated then only restrict to the repos for that suborg
@@ -313,6 +311,14 @@ ${this.results.reduce((x, y) => {
313311

314312
this.log.debug(`Process normally... Not a SubOrg config change or SubOrg config was changed and this repo is part of it. ${JSON.stringify(repo)} suborg config ${JSON.stringify(this.subOrgConfigMap)}`)
315313

314+
let repoConfig = this.config.repository
315+
316+
// Overlay with repo information
317+
if (repoConfig) {
318+
repoConfig = Object.assign(repoConfig, { name: repo.repo, org: repo.owner })
319+
}
320+
321+
// Overlay with suborg
316322
if (subOrgConfig) {
317323
let suborgRepoConfig = subOrgConfig.repository
318324
if (suborgRepoConfig) {
@@ -321,19 +327,26 @@ ${this.results.reduce((x, y) => {
321327
}
322328
}
323329

324-
// Overlay repo config
330+
// Overlay with centralized repo config
325331
// RepoConfigs should be preloaded but checking anyway
326332
const overrideRepoConfig = this.repoConfigs[`${repo.repo}.yml`]?.repository || this.repoConfigs[`${repo.repo}.yaml`]?.repository
327333
if (overrideRepoConfig) {
328334
repoConfig = this.mergeDeep.mergeDeep({}, repoConfig, overrideRepoConfig)
329335
}
336+
337+
// Overlay with decentralized repo config
338+
const unsafeRepoOverrideConfig = (await this.getUnsafeRepoConfig(repo))?.repository
339+
if (unsafeRepoOverrideConfig) {
340+
repoConfig = this.mergeDeep.mergeDeep({}, repoConfig, unsafeRepoOverrideConfig)
341+
}
342+
330343
const {shouldContinue, nopCommands} = await new Archive(this.nop, this.github, repo, repoConfig, this.log).sync()
331344
if (nopCommands) this.appendToResults(nopCommands)
332345
if (shouldContinue) {
333346
if (repoConfig) {
334347
try {
335348
this.log.debug(`found a matching repoconfig for this repo ${JSON.stringify(repoConfig)}`)
336-
const childPlugins = this.childPluginsList(repo)
349+
const childPlugins = await this.childPluginsList(repo)
337350
const RepoPlugin = Settings.PLUGINS.repository
338351
return new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync().then(res => {
339352
this.appendToResults(res)
@@ -356,7 +369,7 @@ ${this.results.reduce((x, y) => {
356369
}
357370
} else {
358371
this.log.debug(`Didnt find any a matching repoconfig for this repo ${JSON.stringify(repo)} in ${JSON.stringify(this.repoConfigs)}`)
359-
const childPlugins = this.childPluginsList(repo)
372+
const childPlugins = await this.childPluginsList(repo)
360373
return Promise.all(childPlugins.map(([Plugin, config]) => {
361374
return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync().then(res => {
362375
this.appendToResults(res)
@@ -379,26 +392,59 @@ ${this.results.reduce((x, y) => {
379392
for (const k of Object.keys(this.subOrgConfigs)) {
380393
const repoPattern = new Glob(k)
381394
if (repoName.search(repoPattern) >= 0) {
382-
return this.subOrgConfigs[k]
395+
const subOrgConfig = this.subOrgConfigs[k]
396+
// Coerce 'repositories' to 'repository'
397+
subOrgConfig.repository = subOrgConfig.repositories
398+
delete subOrgConfig.repositories
399+
return subOrgConfig
383400
}
384401
}
385402
}
386403
return undefined
387404
}
388405

406+
async getUnsafeRepoConfig (repo) {
407+
const repoConfig = await ConfigManager.loadYaml(this.github, {
408+
...repo,
409+
path: '.github/settings.yml'
410+
})
411+
412+
const { unsafeFields } = this.config
413+
414+
const result = {}
415+
for (const unsafeField of unsafeFields) {
416+
let value
417+
try {
418+
value = jsonPointer.get(repoConfig, unsafeField)
419+
} catch {}
420+
421+
if (value !== undefined) {
422+
lodashSet(result, jsonPointer.parse(unsafeField), value)
423+
}
424+
}
425+
426+
return result
427+
}
428+
389429
// Remove Org specific configs from the repo config
390430
returnRepoSpecificConfigs(config) {
391431
const newConfig = Object.assign({}, config) // clone
392432
delete newConfig.rulesets
433+
434+
// Coerce 'repositories' to 'repository'
435+
newConfig.repository = newConfig.repositories
436+
delete newConfig.repositories
437+
393438
return newConfig
394439
}
395440

396-
childPluginsList(repo) {
441+
async childPluginsList(repo) {
397442
const repoName = repo.repo
398443
const subOrgOverrideConfig = this.getSubOrgConfig(repoName)
399-
this.log.debug(`suborg config for ${repoName} is ${JSON.stringify(subOrgOverrideConfig)}`)
444+
this.log.debug(`suborg config for ${repoName} is ${JSON.stringify(subOrgOverrideConfig)}`)
400445
const repoOverrideConfig = this.getRepoOverrideConfig(repoName)
401-
const overrideConfig = this.mergeDeep.mergeDeep({}, this.returnRepoSpecificConfigs(this.config), subOrgOverrideConfig, repoOverrideConfig)
446+
const unsafeRepoOverrideConfig = await this.getUnsafeRepoConfig(repo)
447+
const overrideConfig = this.mergeDeep.mergeDeep({}, this.returnRepoSpecificConfigs(this.config), subOrgOverrideConfig, repoOverrideConfig, unsafeRepoOverrideConfig)
402448

403449
this.log.debug(`consolidated config is ${JSON.stringify(overrideConfig)}`)
404450

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
"deepmerge": "^4.3.1",
3131
"eta": "^3.5.0",
3232
"js-yaml": "^4.1.0",
33+
"json-pointer": "^0.6.2",
3334
"lodash": "^4.17.21",
35+
"lodash.set": "^4.3.2",
3436
"node-cron": "^3.0.2",
3537
"octokit": "^4.1.2",
3638
"probot": "^13.4.4"

0 commit comments

Comments
 (0)