Skip to content

Commit 28de0fd

Browse files
feat: separate runtime and deployment configs
1 parent 620cf14 commit 28de0fd

File tree

7 files changed

+202
-120
lines changed

7 files changed

+202
-120
lines changed

index.js

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const fs = require('fs')
44
const cron = require('node-cron')
55
const Glob = require('./lib/glob')
66
const ConfigManager = require('./lib/configManager')
7+
const DeploymentConfig = require('./lib/deploymentConfig')
78
const NopCommand = require('./lib/nopcommand')
89
const env = require('./lib/env')
910

@@ -13,11 +14,11 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
1314
let appSlug = 'safe-settings'
1415
async function syncAllSettings (nop, context, repo = context.repo(), ref) {
1516
try {
16-
deploymentConfig = await loadYamlFileSystem()
17+
deploymentConfig = await loadYamlFileSystem(context)
1718
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
1819
const configManager = new ConfigManager(context, ref)
1920
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
20-
const config = Object.assign({}, deploymentConfig, runtimeConfig)
21+
const config = { deploymentConfig, runtimeConfig }
2122
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
2223
if (ref) {
2324
return Settings.syncAll(nop, context, repo, config, ref)
@@ -42,11 +43,11 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
4243

4344
async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) {
4445
try {
45-
deploymentConfig = await loadYamlFileSystem()
46+
deploymentConfig = await loadYamlFileSystem(context)
4647
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
4748
const configManager = new ConfigManager(context, ref)
4849
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
49-
const config = Object.assign({}, deploymentConfig, runtimeConfig)
50+
const config = { deploymentConfig, runtimeConfig }
5051
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
5152
return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref)
5253
} catch (e) {
@@ -67,11 +68,11 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
6768

6869
async function syncSettings (nop, context, repo = context.repo(), ref) {
6970
try {
70-
deploymentConfig = await loadYamlFileSystem()
71+
deploymentConfig = await loadYamlFileSystem(context)
7172
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
7273
const configManager = new ConfigManager(context, ref)
7374
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
74-
const config = Object.assign({}, deploymentConfig, runtimeConfig)
75+
const config = { deploymentConfig, runtimeConfig }
7576
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
7677
return Settings.sync(nop, context, repo, config, ref)
7778
} catch (e) {
@@ -92,14 +93,14 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
9293

9394
async function renameSync (nop, context, repo = context.repo(), rename, ref) {
9495
try {
95-
deploymentConfig = await loadYamlFileSystem()
96+
deploymentConfig = await loadYamlFileSystem(context)
9697
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
9798
const configManager = new ConfigManager(context, ref)
9899
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
99-
const config = Object.assign({}, deploymentConfig, runtimeConfig)
100-
const renameConfig = Object.assign({}, config, rename)
100+
const renameConfig = Object.assign({}, runtimeConfig, rename)
101+
const config = { deploymentConfig, runtimeConfig: renameConfig }
101102
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
102-
return Settings.sync(nop, context, repo, renameConfig, ref)
103+
return Settings.sync(nop, context, repo, config, ref)
103104
} catch (e) {
104105
if (nop) {
105106
let filename = env.SETTINGS_FILE_PATH
@@ -121,16 +122,8 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
121122
*
122123
* @return The parsed YAML file
123124
*/
124-
async function loadYamlFileSystem () {
125-
if (deploymentConfig === undefined) {
126-
const deploymentConfigPath = env.DEPLOYMENT_CONFIG_FILE
127-
if (fs.existsSync(deploymentConfigPath)) {
128-
deploymentConfig = yaml.load(fs.readFileSync(deploymentConfigPath))
129-
} else {
130-
deploymentConfig = { restrictedRepos: ['admin', '.github', 'safe-settings'] }
131-
}
132-
}
133-
return deploymentConfig
125+
async function loadYamlFileSystem (context) {
126+
return new DeploymentConfig(context)
134127
}
135128

136129
function getAllChangedSubOrgConfigs (payload) {

lib/deploymentConfig.js

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,43 @@ const yaml = require('js-yaml')
22
const fs = require('fs')
33
const env = require('./env')
44

5+
function isNonEmptyArray (obj) {
6+
return Array.isArray(obj) && obj.length > 0
7+
}
8+
59
/**
610
* Class representing a deployment config.
7-
* It is a singleton (class object) for the deployment settings.
8-
* The settings are loaded from the deployment-settings.yml file during initialization and stored as static properties.
11+
* The settings are loaded from the deployment-settings.yml file.
912
*/
10-
class DeploymentConfig {
11-
// static config
12-
static configvalidators = {}
13-
static overridevalidators = {}
13+
module.exports = class DeploymentConfig {
14+
constructor (context, configPath) {
15+
const deploymentConfigPath = configPath ?? env.DEPLOYMENT_CONFIG_FILE
1416

15-
static {
16-
const deploymentConfigPath = process.env.DEPLOYMENT_CONFIG_FILE ? process.env.DEPLOYMENT_CONFIG_FILE : 'deployment-settings.yml'
17+
let deploymentConfig = {}
1718
if (fs.existsSync(deploymentConfigPath)) {
18-
this.config = yaml.load(fs.readFileSync(deploymentConfigPath)) || {}
19+
deploymentConfig = yaml.load(fs.readFileSync(deploymentConfigPath)) ?? {}
1920
} else {
20-
this.config = { restrictedRepos: ['admin', '.github', 'safe-settings'] }
21+
context.log.info(`No deployment settings found at ${deploymentConfigPath}`)
2122
}
2223

23-
const overridevalidators = this.config.overridevalidators || []
24-
if (this.isNonEmptyArray(overridevalidators)) {
25-
for (const validator of overridevalidators) {
24+
this.overridevalidators = {}
25+
if (isNonEmptyArray(deploymentConfig.overridevalidators)) {
26+
for (const validator of deploymentConfig.overridevalidators) {
2627
// eslint-disable-next-line no-new-func
2728
const f = new Function('baseconfig', 'overrideconfig', 'githubContext', validator.script)
2829
this.overridevalidators[validator.plugin] = { canOverride: f, error: validator.error }
2930
}
3031
}
31-
const configvalidators = this.config.configvalidators || []
32-
if (this.isNonEmptyArray(configvalidators)) {
33-
for (const validator of configvalidators) {
32+
33+
this.configvalidators = {}
34+
if (isNonEmptyArray(deploymentConfig.configvalidators)) {
35+
for (const validator of deploymentConfig.configvalidators) {
3436
// eslint-disable-next-line no-new-func
3537
const f = new Function('baseconfig', 'githubContext', validator.script)
3638
this.configvalidators[validator.plugin] = { isValid: f, error: validator.error }
3739
}
3840
}
39-
}
4041

41-
static isNonEmptyArray (obj) {
42-
return Array.isArray(obj) && obj.length > 0
43-
}
44-
45-
// eslint-disable-next-line no-useless-constructor
46-
constructor (nop, context, repo, config, ref, suborg) {
42+
this.restrictedRepos = deploymentConfig.restrictedRepos ?? ['admin', '.github', 'safe-settings']
4743
}
4844
}
49-
DeploymentConfig.FILE_NAME = `${env.CONFIG_PATH}/settings.yml`
50-
51-
module.exports = DeploymentConfig

lib/mergeDeep.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ const NAME_USERNAME_PROPERTY = item => NAME_FIELDS.find(prop => Object.prototype
66
const GET_NAME_USERNAME_PROPERTY = item => { if (NAME_USERNAME_PROPERTY(item)) return item[NAME_USERNAME_PROPERTY(item)] }
77

88
class MergeDeep {
9-
constructor (log, github, ignorableFields = [], configvalidators = {}, overridevalidators = {}) {
9+
constructor (log, github, ignorableFields = []) {
1010
this.log = log
1111
this.github = github
1212
this.ignorableFields = ignorableFields
13-
this.configvalidators = DeploymentConfig.configvalidators
14-
this.overridevalidators = DeploymentConfig.overridevalidators
13+
14+
const deploymentConfig = new DeploymentConfig({ log })
15+
this.configvalidators = deploymentConfig.configvalidators
16+
this.overridevalidators = deploymentConfig.overridevalidators
1517
}
1618

1719
isObjectNotArray (item) {

lib/settings.js

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const env = require('./env')
1010
const CONFIG_PATH = env.CONFIG_PATH
1111
const eta = new Eta({ views: path.join(__dirname) })
1212
const SCOPE = { ORG: 'org', REPO: 'repo' } // Determine if the setting is a org setting or repo setting
13+
1314
class Settings {
1415
static async syncAll (nop, context, repo, config, ref) {
1516
const settings = new Settings(nop, context, repo, config, ref)
@@ -65,7 +66,6 @@ class Settings {
6566
this.installation_id = context.payload.installation.id
6667
this.github = context.octokit
6768
this.repo = repo
68-
this.config = config
6969
this.nop = nop
7070
this.suborgChange = !!suborg
7171
// If suborg config has been updated, do not load the entire suborg config, and only process repos restricted to it.
@@ -75,26 +75,15 @@ class Settings {
7575
this.log = context.log
7676
this.results = []
7777
this.errors = []
78-
this.configvalidators = {}
79-
this.overridevalidators = {}
80-
const overridevalidators = config.overridevalidators
81-
if (this.isIterable(overridevalidators)) {
82-
for (const validator of overridevalidators) {
83-
// eslint-disable-next-line no-new-func
84-
const f = new Function('baseconfig', 'overrideconfig', 'githubContext', validator.script)
85-
this.overridevalidators[validator.plugin] = { canOverride: f, error: validator.error }
86-
}
87-
}
88-
const configvalidators = config.configvalidators
89-
if (this.isIterable(configvalidators)) {
90-
for (const validator of configvalidators) {
91-
this.log.debug(`Logging each script: ${typeof validator.script}`)
92-
// eslint-disable-next-line no-new-func
93-
const f = new Function('baseconfig', 'githubContext', validator.script)
94-
this.configvalidators[validator.plugin] = { isValid: f, error: validator.error }
95-
}
96-
}
97-
this.mergeDeep = new MergeDeep(this.log, this.github, [], this.configvalidators, this.overridevalidators)
78+
79+
this.mergeDeep = new MergeDeep(this.log, this.github, [])
80+
81+
this.config = config.runtimeConfig
82+
83+
// these can only be defined in the deployment config
84+
this.overridevalidators = config.deploymentConfig.overridevalidators
85+
this.configvalidators = config.deploymentConfig.configvalidators
86+
this.restrictedRepos = config.deploymentConfig.restrictedRepos
9887
}
9988

10089
// Create a check in the Admin repo for safe-settings.
@@ -449,7 +438,7 @@ ${this.results.reduce((x, y) => {
449438
}
450439

451440
isRestricted(repoName) {
452-
const restrictedRepos = this.config.restrictedRepos
441+
const restrictedRepos = this.restrictedRepos
453442
// Skip configuring any restricted repos
454443
if (Array.isArray(restrictedRepos)) {
455444
// For backward compatibility support the old format
@@ -891,14 +880,6 @@ ${this.results.reduce((x, y) => {
891880
isObject (item) {
892881
return (item && typeof item === 'object' && !Array.isArray(item))
893882
}
894-
895-
isIterable(obj) {
896-
// checks for null and undefined
897-
if (obj == null) {
898-
return false
899-
}
900-
return typeof obj[Symbol.iterator] === 'function'
901-
}
902883
}
903884

904885
function prettify (obj) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const DeploymentConfig = require('../../../lib/deploymentConfig')
2+
3+
const defaultConfig = {
4+
configvalidators: {},
5+
overridevalidators: {},
6+
restrictedRepos: ['admin', '.github', 'safe-settings']
7+
}
8+
9+
const context = { log: { info: jest.fn() } }
10+
11+
describe('no deploymentConfig', () => {
12+
const deploymentConfig = new DeploymentConfig(context, 'nonexistent.yml')
13+
14+
test('matches default config', () => {
15+
expect(deploymentConfig).toMatchObject(defaultConfig)
16+
})
17+
18+
test('outputs info message', () => {
19+
expect(context.log.info).toHaveBeenCalledWith('No deployment settings found at nonexistent.yml')
20+
})
21+
})
22+
23+
describe('sample deploymentConfig', () => {
24+
const deploymentConfig = new DeploymentConfig(context, './docs/sample-settings/sample-deployment-settings.yml')
25+
26+
test('matches snapshot', () => {
27+
expect(deploymentConfig).toMatchInlineSnapshot(`
28+
DeploymentConfig {
29+
"configvalidators": {
30+
"collaborators": {
31+
"error": "\`Admin cannot be assigned to collaborators\`
32+
",
33+
"isValid": [Function],
34+
},
35+
},
36+
"overridevalidators": {
37+
"branches": {
38+
"canOverride": [Function],
39+
"error": "\`Branch protection required_approving_review_count cannot be overidden to a lower value\`
40+
",
41+
},
42+
"labels": {
43+
"canOverride": [Function],
44+
"error": "Some error
45+
",
46+
},
47+
},
48+
"restrictedRepos": {
49+
"exclude": [
50+
"^admin$",
51+
"^\\.github$",
52+
"^safe-settings$",
53+
".*-test",
54+
],
55+
"include": [
56+
"^test$",
57+
],
58+
},
59+
}
60+
`)
61+
})
62+
})

0 commit comments

Comments
 (0)