diff --git a/lib/env.js b/lib/env.js
index 94c0ea74..06b3d2d6 100644
--- a/lib/env.js
+++ b/lib/env.js
@@ -6,5 +6,6 @@ module.exports = {
CREATE_PR_COMMENT: process.env.CREATE_PR_COMMENT || 'true',
CREATE_ERROR_ISSUE: process.env.CREATE_ERROR_ISSUE || 'true',
BLOCK_REPO_RENAME_BY_HUMAN: process.env.BLOCK_REPO_RENAME_BY_HUMAN || 'false',
- FULL_SYNC_NOP: process.env.FULL_SYNC_NOP === 'true'
+ FULL_SYNC_NOP: process.env.FULL_SYNC_NOP === 'true',
+ VALIDATE_CONFIG_SCHEMA: process.env.VALIDATE_CONFIG_SCHEMA === 'true'
}
diff --git a/lib/settings.js b/lib/settings.js
index 20e71167..112d291c 100644
--- a/lib/settings.js
+++ b/lib/settings.js
@@ -1,16 +1,21 @@
const path = require('path')
const { Eta } = require('eta')
-const commetMessageTemplate = require('./commentmessage')
+const commentMessageTemplate = require('./commentmessage')
const errorTemplate = require('./error')
const Glob = require('./glob')
const NopCommand = require('./nopcommand')
const MergeDeep = require('./mergeDeep')
-const Archive = require('./plugins/archive')
const env = require('./env')
const CONFIG_PATH = env.CONFIG_PATH
const eta = new Eta({ views: path.join(__dirname) })
const SCOPE = { ORG: 'org', REPO: 'repo' } // Determine if the setting is a org setting or repo setting
const yaml = require('js-yaml');
+const Ajv = require('ajv/dist/2020')
+const schema = require('../schema/dereferenced/settings.json');
+
+const ajv = new Ajv({
+ strict: false
+})
class Settings {
static fileCache = {};
@@ -99,6 +104,34 @@ class Settings {
}
}
this.mergeDeep = new MergeDeep(this.log, this.github, [], this.configvalidators, this.overridevalidators)
+
+ const filePath = path.posix.join(env.CONFIG_PATH, env.SETTINGS_FILE_PATH)
+ const validation = this.validateConfig(this.config)
+
+ if (validation instanceof Error) {
+ this.appendToResults([{
+ type: 'ERROR',
+ plugin: 'settings',
+ repo: this.repo.repo,
+ action: {
+ msg: `${filePath} fails schema validation:\n${validation.message}`
+ }
+ }])
+
+ this.logError(`${filePath} fails schema validation:\n${validation.message}`)
+ }
+ }
+
+ validateConfig (config) {
+ if (env.VALIDATE_CONFIG_SCHEMA) {
+ const valid = ajv.validate(schema, config)
+
+ if (!valid) {
+ return new Error(ajv.errorsText(ajv.errors))
+ }
+ }
+
+ return true
}
// Create a check in the Admin repo for safe-settings.
@@ -232,7 +265,7 @@ class Settings {
`
- const renderedCommentMessage = await eta.renderString(commetMessageTemplate, stats)
+ const renderedCommentMessage = await eta.renderString(commentMessageTemplate, stats)
if (env.CREATE_PR_COMMENT === 'true') {
const summary = `
@@ -246,7 +279,7 @@ ${this.results.reduce((x, y) => {
error = true
return `${x}
❗ ${y.action.msg} | ${y.plugin} | ${prettify(y.repo)} | ${prettify(y.action.additions)} | ${prettify(y.action.deletions)} | ${prettify(y.action.modifications)} |
`
- } else if (y.action.additions === null && y.action.deletions === null && y.action.modifications === null) {
+ } else if (y.action?.additions === null && y.action.deletions === null && y.action.modifications === null) {
return `${x}`
} else {
if (y.action === undefined) {
@@ -644,7 +677,7 @@ ${this.results.reduce((x, y) => {
/**
* If repo param is null load configs for all repos
* If repo param is null and suborg change, load configs for suborg repos only
- * If repo partam is not null, load the config for a specific repo
+ * If repo param is not null, load the config for a specific repo
* @param {*} repo repo param
* @returns repoConfigs object
*/
@@ -844,6 +877,20 @@ ${this.results.reduce((x, y) => {
}
}
+ const validation = this.validateConfig(content)
+
+ if (validation instanceof Error) {
+ this.appendToResults([{
+ type: 'ERROR',
+ plugin: 'settings',
+ repo: this.repo.repo,
+ action: {
+ msg: `${namespacedFilepath} fails schema validation:\n${validation.message}`
+ }
+ }])
+ this.logError(`${namespacedFilepath} fails schema validation:\n${validation.message}`)
+ }
+
return content
} catch (e) {
if (e.status === 404) {
diff --git a/package-lock.json b/package-lock.json
index cf04008a..b3be4f6c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^12.0.2",
"@probot/adapter-aws-lambda-serverless": "^4.0.3",
+ "ajv": "^8.17.1",
"deepmerge": "^4.3.1",
"eta": "^3.5.0",
"js-yaml": "^4.1.0",
@@ -753,6 +754,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -808,6 +826,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3908,15 +3933,15 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@@ -6178,6 +6203,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/eslint/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -6259,6 +6301,13 @@
"node": ">=8"
}
},
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -6525,8 +6574,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.3.2",
@@ -6583,6 +6631,22 @@
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
+ "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
@@ -9597,10 +9661,10 @@
"dev": true
},
"node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -11771,6 +11835,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-in-the-middle": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
@@ -13291,6 +13364,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
diff --git a/package.json b/package.json
index e765fb80..5bdc24bf 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^12.0.2",
"@probot/adapter-aws-lambda-serverless": "^4.0.3",
+ "ajv": "^8.17.1",
"deepmerge": "^4.3.1",
"eta": "^3.5.0",
"js-yaml": "^4.1.0",
diff --git a/test/unit/lib/settings-validation.test.js b/test/unit/lib/settings-validation.test.js
new file mode 100644
index 00000000..4842b431
--- /dev/null
+++ b/test/unit/lib/settings-validation.test.js
@@ -0,0 +1,47 @@
+/* eslint-disable no-undef */
+const Settings = require('../../../lib/settings')
+
+jest.mock('../../../lib/env', () => ({
+ VALIDATE_CONFIG_SCHEMA: true,
+ CONFIG_PATH: '.github',
+ SETTINGS_FILE_PATH: 'settings.yml'
+}))
+
+const context = {
+ log: {
+ debug: jest.fn(),
+ info: jest.fn(),
+ error: jest.fn()
+ },
+ payload: {
+ installation: {
+ id: 123
+ }
+ }
+}
+
+describe('Settings Validation Tests', () => {
+ it('should validate config schema when VALIDATE_CONFIG_SCHEMA is true', async () => {
+ const settings = new Settings(true, context, {}, {
+ repositories: {
+ has_wiki: 'nonsense'
+ }
+ }, 'main', 'github')
+
+ expect(settings.results).toContainEqual(expect.objectContaining({
+ action: {
+ msg: expect.stringContaining('has_wiki must be boolean')
+ }
+ }))
+ })
+
+ it('should pass valid config', async () => {
+ const settings = new Settings(false, context, {}, {
+ repositories: {
+ has_wiki: true
+ }
+ }, 'main', 'github')
+
+ expect(settings.results).toEqual([])
+ })
+}) // Settings Tests
diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js
index 39aac216..2bae6267 100644
--- a/test/unit/lib/settings.test.js
+++ b/test/unit/lib/settings.test.js
@@ -82,8 +82,6 @@ repository:
}
}
-
-
mockRepo = { owner: 'test', repo: 'test-repo' }
mockRef = 'main'
mockSubOrg = 'frontend'