diff --git a/.eslintrc.json b/.eslintrc.json index b6721aca..7c9e3f26 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,16 +1,23 @@ { "env": { - "browser": true, + "node": true, "commonjs": true, "es2021": true }, "extends": [ - "standard" + "eslint:recommended" ], "parserOptions": { - "ecmaVersion": 12 + "ecmaVersion": 13 }, "rules": { }, - "ignorePatterns": ["test/**/*.js"] + "overrides": [ + { + "files": ["test/**/*.js"], + "env": { + "jest": true + } + } + ] } diff --git a/index.js b/index.js index 7e4ef032..408425bb 100644 --- a/index.js +++ b/index.js @@ -9,11 +9,15 @@ const env = require('./lib/env') let deploymentConfig - -module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => { +/** + * @import { Probot, ApplicationFunctionOptions, ProbotOctokit } from "probot" + * @param {Probot} robot + * @param {ApplicationFunctionOptions} probotOptions + */ +module.exports = (robot, _, Settings = require('./lib/settings')) => { let appName = 'safe-settings' let appSlug = 'safe-settings' - async function syncAllSettings (nop, context, repo = context.repo(), ref) { + async function syncAllSettings(nop, context, repo = context.repo(), ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -42,7 +46,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { + async function syncSubOrgSettings(nop, context, suborg, repo = context.repo(), ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -67,7 +71,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncSettings (nop, context, repo = context.repo(), ref) { + async function syncSettings(nop, context, repo = context.repo(), ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -92,7 +96,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function renameSync (nop, context, repo = context.repo(), rename, ref) { + async function renameSync(nop, context, repo = context.repo(), rename, ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -101,7 +105,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const config = Object.assign({}, deploymentConfig, runtimeConfig) const renameConfig = Object.assign({}, config, rename) robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.sync(nop, context, repo, renameConfig, ref ) + return Settings.sync(nop, context, repo, renameConfig, ref) } catch (e) { if (nop) { let filename = env.SETTINGS_FILE_PATH @@ -123,7 +127,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => * * @return The parsed YAML file */ - async function loadYamlFileSystem () { + async function loadYamlFileSystem() { if (deploymentConfig === undefined) { const deploymentConfigPath = env.DEPLOYMENT_CONFIG_FILE if (fs.existsSync(deploymentConfigPath)) { @@ -135,7 +139,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return deploymentConfig } - function getAllChangedSubOrgConfigs (payload) { + function getAllChangedSubOrgConfigs(payload) { const settingPattern = new Glob(`${env.CONFIG_PATH}/suborgs/*.yml`) // Changes will be an array of files that were added const added = payload.commits.map(c => { @@ -159,7 +163,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return configs } - function getAllChangedRepoConfigs (payload, owner) { + function getAllChangedRepoConfigs(payload, owner) { const settingPattern = new Glob(`${env.CONFIG_PATH}/repos/*.yml`) // Changes will be an array of files that were added const added = payload.commits.map(c => { @@ -182,7 +186,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return configs } - function getChangedRepoConfigName (glob, files, owner) { + function getChangedRepoConfigName(glob, files, owner) { const modifiedFiles = files.filter(s => { robot.log.debug(JSON.stringify(s)) return (s.search(glob) >= 0) @@ -193,7 +197,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => }) } - function getChangedSubOrgConfigName (glob, files) { + function getChangedSubOrgConfigName(glob, files) { const modifiedFiles = files.filter(s => { robot.log.debug(JSON.stringify(s)) return (s.search(glob) >= 0) @@ -205,7 +209,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => }) } - async function createCheckRun (context, pull_request, head_sha, head_branch) { + async function createCheckRun(context, pull_request, head_sha) { const { payload } = context // robot.log.debug(`Check suite was requested! for ${context.repo()} ${pull_request.number} ${head_sha} ${head_branch}`) const res = await context.octokit.checks.create({ @@ -229,12 +233,12 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const app = await github.apps.getAuthenticated() appName = app.data.name appSlug = app.data.slug - robot.log.debug(`Validated the app is configured properly = \n${JSON.stringify(app.data, null, 2)}`) + robot.log.debug(`Validated the app ${appName} is configured properly = \n${JSON.stringify(app.data, null, 2)}`) } } - async function syncInstallation () { + async function syncInstallation() { robot.log.trace('Fetching installations') const github = await robot.auth() @@ -383,7 +387,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => robot.on('repository.edited', async context => { const { payload } = context const { sender } = payload - robot.log.debug('repository.edited payload from ', JSON.stringify(sender)) + robot.log.debug(sender, 'repository.edited payload from') if (sender.type === 'Bot') { robot.log.debug('Repository Edited by a Bot') @@ -395,7 +399,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => }) robot.on('repository.renamed', async context => { - if (env.BLOCK_REPO_RENAME_BY_HUMAN!== 'true') { + if (env.BLOCK_REPO_RENAME_BY_HUMAN !== 'true') { robot.log.debug(`"env.BLOCK_REPO_RENAME_BY_HUMAN" is 'false' by default. Repo rename is not managed by Safe-settings. Continue with the default behavior.`) return } @@ -414,7 +418,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const newPath = `.github/repos/${payload.repository.name}.yml` robot.log.debug(oldPath) try { - const repofile = await context.octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { + const repofile = await context.octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: payload.repository.owner.login, repo: env.ADMIN_REPO, path: oldPath, @@ -439,11 +443,11 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } catch (error) { if (error.status === 404) { // if the a config file does not exist, create one from the old one - const update = await context.octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', { + await context.octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', { owner: payload.repository.owner.login, repo: env.ADMIN_REPO, path: newPath, - name: `${payload.repository.name}.yml`, + name: `${payload.repository.name}.yml`, content: content, message: `Repo Renamed and safe-settings renamed the file from ${payload.changes.repository.name.from} to ${payload.repository.name}`, sha: repofile.data.sha, @@ -455,21 +459,21 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } else { robot.log.error(error) } - } + } } catch (error) { if (error.status === 404) { //nop - } else { + } else { robot.log.error(error) } - } + } return } else { robot.log.debug('Repository Edited by a Human') // Create a repository config to reset the name back to the previous name - const rename = {repository: { name: payload.changes.repository.name.from, oldname: payload.repository.name}} - const repo = {repo: payload.changes.repository.name.from, owner: payload.repository.owner.login} + const rename = { repository: { name: payload.changes.repository.name.from, oldname: payload.repository.name } } + const repo = { repo: payload.changes.repository.name.from, owner: payload.repository.owner.login } return renameSync(false, context, repo, rename) } }) @@ -663,7 +667,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => syncInstallation() }) } - + // Get info about the app info() diff --git a/lib/mergeDeep.js b/lib/mergeDeep.js index 054a5b34..e15b1b58 100644 --- a/lib/mergeDeep.js +++ b/lib/mergeDeep.js @@ -1,7 +1,7 @@ const mergeBy = require('./mergeArrayBy') const DeploymentConfig = require('./deploymentConfig') -const NAME_FIELDS = ['name', 'username', 'actor_id', 'login', 'type', 'key_prefix'] +const NAME_FIELDS = ['name', 'username', 'actor_id', 'login', 'type', 'key_prefix', 'title'] const NAME_USERNAME_PROPERTY = item => NAME_FIELDS.find(prop => Object.prototype.hasOwnProperty.call(item, prop)) const GET_NAME_USERNAME_PROPERTY = item => { if (NAME_USERNAME_PROPERTY(item)) return item[NAME_USERNAME_PROPERTY(item)] } diff --git a/lib/plugins/branches.js b/lib/plugins/branches.js index cac0aadf..30ab607a 100644 --- a/lib/plugins/branches.js +++ b/lib/plugins/branches.js @@ -73,7 +73,15 @@ module.exports = class Branches extends ErrorStash { return Promise.resolve(resArray) } this.log.debug(`Adding branch protection ${JSON.stringify(params)}`) - return this.github.repos.updateBranchProtection(params).then(res => this.log(`Branch protection applied successfully ${JSON.stringify(res.url)}`)).catch(e => { this.logError(`Error applying branch protection ${JSON.stringify(e)}`); return [] }) + return this.github.repos.updateBranchProtection(params) + .then(res => { + this.log.info(`Branch protection applied successfully ${JSON.stringify(res.url)}`) + return res.url + }) + .catch(e => { + this.logError(`Error applying branch protection ${JSON.stringify(e)}`) + return [] + }) }).catch((e) => { if (e.status === 404) { Object.assign(params, branch.protection, { headers: previewHeaders }) diff --git a/lib/plugins/diffable.js b/lib/plugins/diffable.js index 8db6a0ef..ac1c6ba9 100644 --- a/lib/plugins/diffable.js +++ b/lib/plugins/diffable.js @@ -24,13 +24,14 @@ const MergeDeep = require('../mergeDeep') const NopCommand = require('../nopcommand') const ignorableFields = ['id', 'node_id', 'default', 'url'] module.exports = class Diffable extends ErrorStash { - constructor (nop, github, repo, entries, log, errors) { + constructor (nop, github, repo, entries, log, errors, filterNameFields) { super(errors) this.github = github this.repo = repo this.entries = entries this.log = log this.nop = nop + this.filterNameFields = filterNameFields ?? MergeDeep.NAME_FIELDS } filterEntries () { @@ -102,7 +103,7 @@ module.exports = class Diffable extends ErrorStash { } } // Delete any diffable that now only has name and no other attributes - filteredEntries = filteredEntries.filter(entry => Object.keys(entry).filter(key => !MergeDeep.NAME_FIELDS.includes(key)).length !== 0) + filteredEntries = filteredEntries.filter(entry => Object.keys(entry).filter(key => !this.filterNameFields.includes(key)).length !== 0) const changes = [] diff --git a/lib/plugins/milestones.js b/lib/plugins/milestones.js index 9fc93ab2..bfbc8b53 100644 --- a/lib/plugins/milestones.js +++ b/lib/plugins/milestones.js @@ -1,8 +1,9 @@ +const MergeDeep = require('../mergeDeep') const Diffable = require('./diffable') module.exports = class Milestones extends Diffable { - constructor (...args) { - super(...args) + constructor (nop, github, repo, entries, log, errors) { + super(nop, github, repo, entries, log, errors, MergeDeep.NAME_FIELDS.filter(i => i !== 'title')) if (this.entries) { this.entries.forEach(milestone => { diff --git a/lib/plugins/rulesets.js b/lib/plugins/rulesets.js index 6846e1ce..f450846d 100644 --- a/lib/plugins/rulesets.js +++ b/lib/plugins/rulesets.js @@ -20,13 +20,13 @@ module.exports = class Rulesets extends Diffable { // Find all Rulesets for this org find () { if (this.scope === 'org') { - this.log.debug(`Getting all rulesets for the org ${this.org}`) + this.log.debug(`Getting all rulesets for the org ${this.repo.owner}`) const listOptions = this.github.request.endpoint.merge('GET /orgs/{org}/rulesets', { org: this.repo.owner, headers: version }) - this.log(listOptions) + this.log.debug(listOptions) return this.github.paginate(listOptions) .then(res => { const rulesets = res.map(ruleset => { diff --git a/lib/settings.js b/lib/settings.js index 961aa4c6..fe04283f 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -9,8 +9,15 @@ 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 +/** @import { Context, ProbotOctokit } from "probot" */ class Settings { - static async syncAll (nop, context, repo, config, ref) { + /** @type {InstanceType} */ + github + /** + * @param {boolean} nop + * @param {Context} context + */ + static async syncAll(nop, context, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref) try { await settings.loadConfigs() @@ -24,7 +31,11 @@ class Settings { } } - static async syncSubOrgs (nop, context, suborg, repo, config, ref) { + /** + * @param {boolean} nop + * @param {Context} context + */ + static async syncSubOrgs(nop, context, suborg, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref, suborg) try { await settings.loadConfigs() @@ -36,7 +47,11 @@ class Settings { } } - static async sync (nop, context, repo, config, ref) { + /** + * @param {boolean} nop + * @param {Context} context + */ + static async sync(nop, context, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref) try { await settings.loadConfigs(repo) @@ -51,13 +66,17 @@ class Settings { } } - static async handleError (nop, context, repo, config, ref, nopcommand) { + static async handleError(nop, context, repo, config, ref, nopcommand) { const settings = new Settings(nop, context, repo, config, ref) settings.appendToResults([nopcommand]) await settings.handleResults() } - constructor (nop, context, repo, config, ref, suborg) { + /** + * @param {boolean} nop + * @param {Context} context + */ + constructor(nop, context, repo, config, ref, suborg) { this.ref = ref this.context = context this.installation_id = context.payload.installation.id @@ -96,7 +115,7 @@ class Settings { } // Create a check in the Admin repo for safe-settings. - async createCheckRun () { + async createCheckRun() { const startTime = new Date() let conclusion = 'success' let details = `Run on: \`${new Date().toISOString()}\`` @@ -142,7 +161,7 @@ class Settings { }) } - logError (msg) { + logError(msg) { this.log.error(msg) this.errors.push({ owner: this.repo.owner, @@ -152,7 +171,7 @@ class Settings { }) } - async handleResults () { + async handleResults() { const { payload } = this.context // Create a checkrun if not in nop mode @@ -162,6 +181,13 @@ class Settings { return } + //remove duplicate rows in this.results + this.results = this.results.filter((thing, index, self) => { + return index === self.findIndex((t) => { + return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin + }) + }) + let error = false // Different logic const stats = { @@ -226,23 +252,23 @@ class Settings { #### :robot: Safe-Settings config changes detected: ${this.results.reduce((x, y) => { - if (!y) { - return x - } - if (y.type === 'ERROR') { - error = true - return `${x} + if (!y) { + return x + } + if (y.type === 'ERROR') { + 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) { - return `${x}` - } else { - if (y.action === undefined) { - return `${x}` - } - return `${x} + } else if (y.action.additions === null && y.action.deletions === null && y.action.modifications === null) { + return `${x}` + } else { + if (y.action === undefined) { + return `${x}` + } + return `${x} ✋ ${y.plugin} ${prettify(y.repo)} ${prettify(y.action.additions)} ${prettify(y.action.deletions)} ${prettify(y.action.modifications)} ` - } -}, table)} + } + }, table)} ` const pullRequest = payload.check_run.check_suite.pull_requests[0] @@ -272,22 +298,21 @@ ${this.results.reduce((x, y) => { await this.github.checks.update(params) } - async loadConfigs (repo) { + async loadConfigs(repo) { this.subOrgConfigs = await this.getSubOrgConfigs() this.repoConfigs = await this.getRepoConfigs(repo) } - async updateOrg () { + async updateOrg() { const rulesetsConfig = this.config.rulesets if (rulesetsConfig) { const RulesetsPlugin = Settings.PLUGINS.rulesets - return new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG).sync().then(res => { - this.appendToResults(res) - }) + const res = await new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG).sync() + this.appendToResults(res) } } - async updateRepos (repo) { + async updateRepos(repo) { this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs() let repoConfig = this.config.repository if (repoConfig) { @@ -318,50 +343,40 @@ ${this.results.reduce((x, y) => { if (overrideRepoConfig) { repoConfig = this.mergeDeep.mergeDeep({}, repoConfig, overrideRepoConfig) } - if (repoConfig) { - try { + try { + const childPlugins = this.childPluginsList(repo) + if (repoConfig) { this.log.debug(`found a matching repoconfig for this repo ${JSON.stringify(repoConfig)}`) - const childPlugins = this.childPluginsList(repo) const RepoPlugin = Settings.PLUGINS.repository - return new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync().then(res => { - this.appendToResults(res) - return Promise.all( - childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync() - })) - }).then(res => { - this.appendToResults(res) - }) - } catch (e) { - if (this.nop) { - const nopcommand = new NopCommand(this.constructor.name, this.repo, null, `${e}`, 'ERROR') - this.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - this.appendToResults([nopcommand]) - // throw e - } else { - throw e - } + const res = await new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync(); + this.appendToResults(res) } - } else { - this.log.debug(`Didnt find any a matching repoconfig for this repo ${JSON.stringify(repo)} in ${JSON.stringify(this.repoConfigs)}`) - const childPlugins = this.childPluginsList(repo) - return Promise.all(childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync().then(res => { - this.appendToResults(res) - }) + else { + this.log.debug(`Didnt find any a matching repoconfig for this repo ${JSON.stringify(repo)} in ${JSON.stringify(this.repoConfigs)}`) + } + await Promise.all(childPlugins.map(async ([Plugin, config]) => { + const res = await new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync() + this.appendToResults(res) })) + } catch (e) { + if (this.nop) { + const nopcommand = new NopCommand(this.constructor.name, this.repo, null, `${e}`, 'ERROR') + this.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + this.appendToResults([nopcommand]) + // throw e + } else { + throw e + } } } - async updateAll () { + async updateAll() { // this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs(this.github, this.repo, this.log) // this.repoConfigs = this.repoConfigs || await this.getRepoConfigs(this.github, this.repo, this.log) - return this.eachRepositoryRepos(this.github, this.config.restrictedRepos, this.log).then(res => { - this.appendToResults(res) - }) + await this.eachRepositoryRepos(this.github, this.config.restrictedRepos, this.log) } - getSubOrgConfig (repoName) { + getSubOrgConfig(repoName) { if (this.subOrgConfigs) { for (const k of Object.keys(this.subOrgConfigs)) { const repoPattern = new Glob(k) @@ -374,13 +389,13 @@ ${this.results.reduce((x, y) => { } // Remove Org specific configs from the repo config - returnRepoSpecificConfigs (config) { + returnRepoSpecificConfigs(config) { const newConfig = Object.assign({}, config) // clone delete newConfig.rulesets return newConfig } - childPluginsList (repo) { + childPluginsList(repo) { const repoName = repo.repo const subOrgOverrideConfig = this.getSubOrgConfig(repoName) this.log.debug(`suborg config for ${repoName} is ${JSON.stringify(subOrgOverrideConfig)}`) @@ -412,7 +427,7 @@ ${this.results.reduce((x, y) => { return childPlugins } - validate (section, baseConfig, overrideConfig) { + validate(section, baseConfig, overrideConfig) { const configValidator = this.configvalidators[section] if (configValidator) { this.log.debug(`Calling configvalidator for key ${section} `) @@ -431,7 +446,7 @@ ${this.results.reduce((x, y) => { } } - isRestricted (repoName) { + isRestricted(repoName) { const restrictedRepos = this.config.restrictedRepos // Skip configuring any restricted repos if (Array.isArray(restrictedRepos)) { @@ -463,23 +478,21 @@ ${this.results.reduce((x, y) => { return false } - includesRepo (repoName, restrictedRepos) { + includesRepo(repoName, restrictedRepos) { return restrictedRepos.filter((restrictedRepo) => { return RegExp(restrictedRepo).test(repoName) }).length > 0 } - async eachRepositoryRepos (github, restrictedRepos, log) { + async eachRepositoryRepos(github, restrictedRepos, log) { log.debug('Fetching repositories') - return github.paginate('GET /installation/repositories').then(repositories => { - return Promise.all(repositories.map(repository => { - if (this.isRestricted(repository.name)) { - return null - } + const repositories = await github.paginate('GET /installation/repositories') + await Promise.all(repositories.map(async repository => { + if (this.isRestricted(repository.name)) { + return + } - const { owner, name } = repository - return this.updateRepos({ owner: owner.login, repo: name }) - }) - ) - }) + const { owner, name } = repository + await this.updateRepos({ owner: owner.login, repo: name }) + })) } /** @@ -488,7 +501,7 @@ ${this.results.reduce((x, y) => { * @param params Params to fetch the file with * @return The parsed YAML file */ - async loadConfigMap (params) { + async loadConfigMap(params) { try { this.log.debug(` In loadConfigMap ${JSON.stringify(params)}`) const response = await this.github.repos.getContent(params).catch(e => { @@ -535,7 +548,7 @@ ${this.results.reduce((x, y) => { * @param params Params to fetch the file with * @return The parsed YAML file */ - async getRepoConfigMap () { + async getRepoConfigMap() { try { this.log.debug(` In getRepoConfigMap ${JSON.stringify(this.repo)}`) // GitHub getContent api has a hard limit of returning 1000 entries without @@ -602,7 +615,7 @@ ${this.results.reduce((x, y) => { * @param params Params to fetch the file with * @return The parsed YAML file */ - async getSubOrgConfigMap () { + async getSubOrgConfigMap() { try { this.log.debug(` In getSubOrgConfigMap ${JSON.stringify(this.repo)}`) const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO } @@ -629,7 +642,7 @@ ${this.results.reduce((x, y) => { * @param {*} repo repo param * @returns repoConfigs object */ - async getRepoConfigs (repo) { + async getRepoConfigs(repo) { try { const overridePaths = await this.getRepoConfigMap() const repoConfigs = {} @@ -681,12 +694,11 @@ ${this.results.reduce((x, y) => { * @param params Params to fetch the file with * @return The parsed YAML file */ - async getSubOrgConfigs () { + async getSubOrgConfigs() { try { - if (this.subOrgConfigMap) { - this.log.debug(`SubOrg config was changed and the associated overridePaths is = ${JSON.stringify(this.subOrgConfigMap)}`) - } - const overridePaths = this.subOrgConfigMap || await this.getSubOrgConfigMap() + // Get all suborg configs even though we might be here becuase of a suborg config change + // we will filter them out if request is due to a suborg config change + const overridePaths = await this.getSubOrgConfigMap() const subOrgConfigs = {} for (const override of overridePaths) { @@ -698,7 +710,19 @@ ${this.results.reduce((x, y) => { subOrgConfigs[override.name] = data if (data.suborgrepos) { data.suborgrepos.forEach(repository => { - subOrgConfigs[repository] = data + this.storeSubOrgConfigIfNoConflicts(subOrgConfigs, override.path, repository, data) + + // In case support for multiple suborg configs for the same repo is required, merge the configs. + // + // Planned for the future to support multiple suborgrepos for the same repo + // + // if (existingConfigForRepo) { + // subOrgConfigs[repository] = this.mergeDeep.mergeDeep({}, existingConfigForRepo, data) + // } else { + // subOrgConfigs[repository] = data + // } + + subOrgConfigs[repository] = Object.assign({}, data, { source: override.path }) }) } if (data.suborgteams) { @@ -708,7 +732,7 @@ ${this.results.reduce((x, y) => { await Promise.all(promises).then(res => { res.forEach(r => { r.forEach(e => { - subOrgConfigs[e.name] = data + this.storeSubOrgConfigIfNoConflicts(subOrgConfigs, override.path, e.name, data) }) }) }) @@ -720,12 +744,26 @@ ${this.results.reduce((x, y) => { await Promise.all(promises).then(res => { res.forEach(r => { r.forEach(e => { - subOrgConfigs[e.repository_name] = data + this.storeSubOrgConfigIfNoConflicts(subOrgConfigs, override.path, e.repository_name, data) }) }) }) } } + + // If this was result of a suborg config change, only return the repos that are part of the suborg config + if (this.subOrgConfigMap) { + this.log.debug(`SubOrg config was changed and the associated overridePaths is = ${JSON.stringify(this.subOrgConfigMap)}`) + // enumerate the properties of the subOrgConfigs object and delete the ones that are not part of the suborg + for (const [key, value] of Object.entries(subOrgConfigs)) { + if (!this.subOrgConfigMap.some((overridePath) => { + return overridePath.path === value.source + } + )) { + delete subOrgConfigs[key] + } + } + } return subOrgConfigs } catch (e) { if (this.nop) { @@ -739,13 +777,21 @@ ${this.results.reduce((x, y) => { } } + storeSubOrgConfigIfNoConflicts(subOrgConfigs, overridePath, repoName, data) { + const existingConfigForRepo = subOrgConfigs[repoName] + if (existingConfigForRepo && existingConfigForRepo.source !== overridePath) { + throw new Error(`Multiple suborg configs for ${repoName} in ${overridePath} and ${existingConfigForRepo?.source}`) + } + subOrgConfigs[repoName] = Object.assign({}, data, { source: overridePath }) + } + /** * Loads a file from GitHub * * @param params Params to fetch the file with * @return The parsed YAML file */ - async loadYaml (filePath) { + async loadYaml(filePath) { try { const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO } const params = Object.assign(repo, { path: filePath, ref: this.ref }) @@ -782,13 +828,16 @@ ${this.results.reduce((x, y) => { } } - appendToResults (res) { + appendToResults(res) { if (this.nop) { - this.results = this.results.concat(res.flat(3)) + //Remove nulls and undefined from the results + const results = res.flat(3).filter(r => r) + + this.results = this.results.concat(results) } } - async getReposForTeam (teamslug) { + async getReposForTeam(teamslug) { const options = this.github.rest.teams.listReposInOrg.endpoint.merge({ org: this.repo.owner, team_slug: teamslug, @@ -797,20 +846,19 @@ ${this.results.reduce((x, y) => { return this.github.paginate(options) } - async getReposForCustomProperty (customPropertyTuple) { - const name=Object.keys(customPropertyTuple)[0] + async getReposForCustomProperty(customPropertyTuple) { + const name = Object.keys(customPropertyTuple)[0] let q = `props.${name}:${customPropertyTuple[name]}` q = encodeURIComponent(q) const options = this.github.request.endpoint((`/orgs/${this.repo.owner}/properties/values?repository_query=${q}`)) return this.github.paginate(options) } - - isObject (item) { + isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item)) } - isIterable (obj) { + isIterable(obj) { // checks for null and undefined if (obj == null) { return false diff --git a/test/fixtures/api_responses/get_repository.json b/test/fixtures/api_responses/get_repository.json new file mode 100644 index 00000000..f46611bd --- /dev/null +++ b/test/fixtures/api_responses/get_repository.json @@ -0,0 +1,521 @@ +{ + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "forks_count": 9, + "forks": 9, + "stargazers_count": 80, + "watchers_count": 80, + "watchers": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "open_issues": 0, + "is_template": false, + "topics": [ + "octocat", + "atom", + "electron", + "api" + ], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "has_discussions": false, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "pull": true, + "push": false, + "admin": false + }, + "allow_rebase_merge": true, + "template_repository": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World-Template", + "full_name": "octocat/Hello-World-Template", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World-Template", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World-Template", + "archive_url": "https://api.github.com/repos/octocat/Hello-World-Template/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World-Template/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World-Template/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World-Template/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World-Template/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World-Template/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World-Template/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World-Template/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World-Template/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World-Template/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World-Template/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World-Template/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World-Template/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World-Template.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World-Template/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World-Template/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World-Template/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World-Template/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World-Template/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World-Template/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World-Template/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World-Template/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World-Template.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World-Template/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World-Template/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World-Template/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World-Template/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World-Template/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World-Template/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World-Template.git", + "mirror_url": "git:git.example.com/octocat/Hello-World-Template", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World-Template/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World-Template", + "homepage": "https://github.com", + "language": null, + "forks": 9, + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "watchers": 80, + "size": 108, + "default_branch": "master", + "open_issues": 0, + "open_issues_count": 0, + "is_template": true, + "license": { + "key": "mit", + "name": "MIT License", + "url": "https://api.github.com/licenses/mit", + "spdx_id": "MIT", + "node_id": "MDc6TGljZW5zZW1pdA==", + "html_url": "https://api.github.com/licenses/mit" + }, + "topics": [ + "octocat", + "atom", + "electron", + "api" + ], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "allow_rebase_merge": true, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0 + }, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "allow_forking": true, + "subscribers_count": 42, + "network_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + "organization": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "Organization", + "site_admin": false + }, + "parent": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "language": null, + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "is_template": true, + "topics": [ + "octocat", + "atom", + "electron", + "api" + ], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "allow_rebase_merge": true, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "url": "https://api.github.com/licenses/mit", + "spdx_id": "MIT", + "node_id": "MDc6TGljZW5zZW1pdA==", + "html_url": "https://api.github.com/licenses/mit" + }, + "forks": 1, + "open_issues": 1, + "watchers": 1 + }, + "source": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "is_template": true, + "topics": [ + "octocat", + "atom", + "electron", + "api" + ], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "allow_rebase_merge": true, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "url": "https://api.github.com/licenses/mit", + "spdx_id": "MIT", + "node_id": "MDc6TGljZW5zZW1pdA==", + "html_url": "https://api.github.com/licenses/mit" + }, + "forks": 1, + "open_issues": 1, + "watchers": 1, + "security_and_analysis": { + "advanced_security": { + "status": "enabled" + }, + "secret_scanning": { + "status": "enabled" + }, + "secret_scanning_push_protection": { + "status": "disabled" + }, + "secret_scanning_non_provider_patterns": { + "status": "disabled" + } + } + } +} diff --git a/test/fixtures/events/push.settings.json b/test/fixtures/events/push.settings.json index d12de4c2..9c080cba 100644 --- a/test/fixtures/events/push.settings.json +++ b/test/fixtures/events/push.settings.json @@ -61,7 +61,8 @@ "full_name": "bkeepers-inc/botland", "owner": { "name": "bkeepers-inc", - "email": null + "email": null, + "login": "bkeepers-inc" }, "private": true, "html_url": "https://github.com/bkeepers-inc/botland", diff --git a/test/fixtures/events/repository.edited.json b/test/fixtures/events/repository.edited.json index 42c0068e..92ee6791 100644 --- a/test/fixtures/events/repository.edited.json +++ b/test/fixtures/events/repository.edited.json @@ -130,7 +130,7 @@ "repos_url": "https://api.github.com/users/_safe_settings%5Bbot%5D/repos", "events_url": "https://api.github.com/users/_safe_settings%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/_safe_settings%5Bbot%5D/received_events", - "type": "Bot", + "type": "NotBot", "site_admin": false }, "installation": { diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml new file mode 100644 index 00000000..3a808505 --- /dev/null +++ b/test/fixtures/settings.yml @@ -0,0 +1,195 @@ +# This settings file can be used to create org-level settings + +# This is the settings that need to be applied to all repositories in the org +# See https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-an-organization-repository for all available settings for a repository +repository: + # A short description of the repository that will show up on GitHub + description: description of the repo + + # A URL with more information about the repository + homepage: https://example.github.io/ + + # Create an initial commit with empty README. + # Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos + auto_init: true + + # The default branch for this repository. + default_branch: main + +# Rulesets +# See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options +rulesets: + - name: Template + # The target of the ruleset. Can be one of: + # - branch + # - tag + target: branch + # The enforcement level of the ruleset. `evaluate` allows admins to test + # rules before enforcing them. + # - disabled + # - active + # - evaluate + enforcement: active + + # The actors that can bypass the rules in this ruleset + bypass_actors: + - actor_id: number + # type: The type of actor that can bypass a ruleset + # - RepositoryRole + # - Team + # - Integration + # - OrganizationAdmin + actor_type: Team + # When the specified actor can bypass the ruleset. `pull_request` + # means that an actor can only bypass rules on pull requests. + # - always + # - pull_request + bypass_mode: pull_request + + - actor_id: 1 + actor_type: OrganizationAdmin + bypass_mode: always + + - actor_id: 7898 + actor_type: RepositoryRole + bypass_mode: always + + - actor_id: 210920 + actor_type: Integration + bypass_mode: always + + conditions: + # Parameters for a repository ruleset ref name condition + ref_name: + # Array of ref names or patterns to include. One of these + # patterns must match for the condition to pass. Also accepts + # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to + # include all branches. + include: ["~DEFAULT_BRANCH"] + + # Array of ref names or patterns to exclude. The condition + # will not pass if any of these patterns match. + exclude: ["refs/heads/oldmaster"] + + # This condition only exists at the org level (remove for suborg and repo level rulesets) + repository_name: + # Array of repository names or patterns to include. + # One of these patterns must match for the condition + # to pass. Also accepts `~ALL` to include all + # repositories. + include: ["test*"] + # Array of repository names or patterns to exclude. The + # condition will not pass if any of these patterns + # match. + exclude: ["test", "test1"] + # Whether renaming of target repositories is + # prevented. + protected: true + + # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset + rules: + - type: creation + - type: update + parameters: + # Branch can pull changes from its upstream repository + update_allows_fetch_and_merge: true + - type: deletion + - type: required_linear_history + - type: required_signatures + + - type: required_deployments + parameters: + required_deployment_environments: ["staging"] + + - type: pull_request + parameters: + # Reviewable commits pushed will dismiss previous pull + # request review approvals. + dismiss_stale_reviews_on_push: true + # Require an approving review in pull requests that modify + # files that have a designated code owner + require_code_owner_review: true + # Whether the most recent reviewable push must be approved + # by someone other than the person who pushed it. + require_last_push_approval: true + # The number of approving reviews that are required before a + # pull request can be merged. + required_approving_review_count: 1 + # All conversations on code must be resolved before a pull + # request can be merged. + required_review_thread_resolution: true + + # Choose which status checks must pass before branches can be merged + # into a branch that matches this rule. When enabled, commits must + # first be pushed to another branch, then merged or pushed directly + # to a branch that matches this rule after status checks have + # passed. + - type: required_status_checks + parameters: + # Whether pull requests targeting a matching branch must be + # tested with the latest code. This setting will not take + # effect unless at least one status check is enabled. + strict_required_status_checks_policy: true + required_status_checks: + - context: CodeQL + integration_id: 1234 + - context: GHAS Compliance + integration_id: 1234 + + # Choose which workflows must pass before branches can be merged. + - type: workflows + parameters: + workflows: + - path: .github/workflows/example.yml + # Run $("meta[name=octolytics-dimension-repository_id]").getAttribute('content') + # in the browser console of the repository to get the repository_id + repository_id: 123456 + # One of the following: + # Branch or tag + ref: refs/heads/main + # Commit SHA + sha: 1234567890abcdef + + - type: commit_message_pattern + parameters: + name: test commit_message_pattern + # required: + # - operator + # - pattern + negate: true + operator: starts_with + # The operator to use for matching. + # - starts_with + # - ends_with + # - contains + # - regex + pattern: skip* + # The pattern to match with. + + - type: commit_author_email_pattern + parameters: + name: test commit_author_email_pattern + negate: false + operator: regex + pattern: "^.*@example.com$" + + - type: committer_email_pattern + parameters: + name: test committer_email_pattern + negate: false + operator: regex + pattern: "^.*@example.com$" + + - type: branch_name_pattern + parameters: + name: test branch_name_pattern + negate: false + operator: regex + pattern: ".*\/.*" + + - type: "tag_name_pattern" + parameters: + name: test tag_name_pattern + negate: false + operator: regex + pattern: ".*\/.*" diff --git a/test/unit/index.test.js b/test/unit/index.test.js index 9d09804d..1cbfe13c 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -1,10 +1,29 @@ -const { Probot, ProbotOctokit } = require('probot') +const { Probot } = require('probot') +const { getLog } = require('probot/lib/helpers/get-log') const plugin = require('../../index') - -describe.skip('plugin', () => { - let app, event, sync, github - - beforeEach(() => { +const env = require('../../lib/env') +const fs = require('fs') +const path = require('path') +/** @import { Probot, ProbotOctokit } from "probot" */ + +function getFixture (...paths) { + const pathToFixtures = path.resolve(__dirname, '..', 'fixtures', ...paths) + return fs.readFileSync(pathToFixtures, 'utf-8') +} +function getApiResponse (filename) { + const pathToFixtures = path.resolve(__dirname, '..', 'fixtures', 'api_responses', filename) + return { data: JSON.parse(fs.readFileSync(pathToFixtures, 'utf-8')) } +} + +describe('webhooks', () => { + /** @type {Probot} */ + let app + let event + /** @type {InstanceType} */ + let github + + beforeEach(async () => { + /** @type {InstanceType} */ class Octokit { static defaults () { return Octokit @@ -14,8 +33,60 @@ describe.skip('plugin', () => { this.config = { get: jest.fn().mockReturnValue({}) } + this.paginate = jest.fn() + .mockImplementation(async (params) => { + if (params === 'GET /installation/repositories') { + return [{ owner: { login: 'bkeepers-inc' }, name: 'botland' }] + } else if (params && params.route === 'GET /orgs/{org}/rulesets') { + return [{ id: 21 }] + } else if (params && params.route === 'GET /orgs/{org}/rulesets/{id}') { + return [] + } else if (params && params.route === 'GET /orgs/{org}/installations') { + return [{ id: 21 }] + } else { + console.log({ params }) + throw new Error('not implemented') + } + }) this.repos = { - getContent: jest.fn(() => Promise.resolve({ data: { content: '' } })) + getContents: jest.fn().mockImplementation(() => Promise.resolve({ data: { content: '' } })), + getContent: jest.fn().mockImplementation((params) => { + if (params && params.path === '.github/settings.yml') { + return Promise.resolve({ data: { content: btoa(getFixture('settings.yml')) } }) + } else if (params && params.path === '.github') { + return Promise.resolve({ data: [{ name: 'repos', path: '.github/repos', sha: '2a97853ea484cd71a00e2cfe0dac45067b05b3e4' }] }) + } else if (params && params.path === '.github/suborgs') { + return Promise.resolve({ data: [] }) // Should return a list of files in that folder. GitHub would return 404 on an empty (non-existing) folder, but this works for testing purposes + } else { + return Promise.resolve({ data: { content: '' } }) // Usually we don't need any return value when this is called while testing + } + }), + get: jest.fn().mockResolvedValue(getApiResponse('get_repository.json')), + listCommits: jest.fn().mockResolvedValue({ data: { commits: { data: [{ sha: 'bb8a050117521bc7a01c2f691d5709da0510a387' }] } } }), + getBranch: jest.fn().mockResolvedValue({ data: { name: 'main' } }), + update: jest.fn().mockImplementation(() => null) + } + this.git = { + getTree: jest.fn().mockResolvedValue({ data: { tree: [{ path: 'botland.yml' }] } }) + } + this.request = Object.assign( + async function (route, parameters) { + if (route === 'POST /orgs/{org}/rulesets') { + return { url: 'mock call' } + } else { + console.log({ route, parameters }) + throw new Error('not implemented') + } + }, + { + endpoint: { merge: jest.fn().mockImplementation((route, options) => ({ route, options })) } + } + ) + this.apps = { + listInstallations: { + endpoint: { merge: jest.fn().mockImplementation(() => ({ route: 'GET /orgs/{org}/installations' })) } + }, + getAuthenticated: jest.fn().mockResolvedValue({ data: { slug: 'octoapp' } }) } } @@ -24,27 +95,21 @@ describe.skip('plugin', () => { } } - app = new Probot({ secret: "abcdef", Octokit }) - github = { - repos: { - getContents: jest.fn(() => Promise.resolve({ data: { content: '' } })) - } - } - app.auth = () => Promise.resolve(github) - app.log = { debug: jest.fn(), error: console.error } + app = new Probot({ secret: 'abcdef', Octokit, log: getLog({ level: 'info' }) }) + github = await app.auth() event = { name: 'push', payload: JSON.parse(JSON.stringify(require('../fixtures/events/push.settings.json'))) } - sync = jest.fn() + env.ADMIN_REPO = 'botland' - plugin(app, {}, { sync, FILE_NAME: '.github/settings.yml' }) + plugin(app, {}) }) describe('with settings modified on master', () => { it('syncs settings', async () => { await app.receive(event) - expect(sync).toHaveBeenCalled() + expect(github.repos.update).toHaveBeenCalled() }) }) @@ -55,7 +120,7 @@ describe.skip('plugin', () => { it('does not sync settings', async () => { await app.receive(event) - expect(sync).not.toHaveBeenCalled() + expect(github.repos.update).not.toHaveBeenCalled() }) }) @@ -66,7 +131,7 @@ describe.skip('plugin', () => { it('does not sync settings', async () => { await app.receive(event) - expect(sync).not.toHaveBeenCalled() + expect(github.repos.update).not.toHaveBeenCalled() }) }) @@ -80,7 +145,7 @@ describe.skip('plugin', () => { it('does sync settings', async () => { await app.receive(event) - expect(sync).toHaveBeenCalled() + expect(github.repos.update).toHaveBeenCalled() }) }) @@ -94,7 +159,7 @@ describe.skip('plugin', () => { it('does sync settings', async () => { await app.receive(event) - expect(sync).toHaveBeenCalled() + expect(github.repos.update).toHaveBeenCalled() }) }) @@ -108,7 +173,7 @@ describe.skip('plugin', () => { it('does sync settings', async () => { await app.receive(event) - expect(sync).toHaveBeenCalled() + expect(github.repos.update).toHaveBeenCalled() }) }) @@ -122,7 +187,7 @@ describe.skip('plugin', () => { it('does sync settings', async () => { await app.receive(event) - expect(sync).toHaveBeenCalled() + expect(github.repos.update).toHaveBeenCalled() }) }) @@ -136,7 +201,7 @@ describe.skip('plugin', () => { it('does sync settings', async () => { await app.receive(event) - expect(sync).toHaveBeenCalled() + expect(github.repos.update).toHaveBeenCalled() }) }) @@ -148,7 +213,7 @@ describe.skip('plugin', () => { it('does sync settings', async () => { await app.receive(event) - expect(sync).toHaveBeenCalled() + expect(github.repos.update).toHaveBeenCalled() }) }) diff --git a/test/unit/lib/plugins/branches.test.js b/test/unit/lib/plugins/branches.test.js index 68290ea4..8b6e49d1 100644 --- a/test/unit/lib/plugins/branches.test.js +++ b/test/unit/lib/plugins/branches.test.js @@ -1,13 +1,13 @@ /* eslint-disable no-undef */ +const { getLog } = require('probot/lib/helpers/get-log') const { when } = require('jest-when') const Branches = require('../../../../lib/plugins/branches') describe('Branches', () => { let github - const log = jest.fn() - log.debug = jest.fn() - log.error = jest.fn() + const log = getLog() + log.level = process.env.LOG_LEVEL ?? 'info' function configure (config) { const noop = false @@ -28,7 +28,7 @@ describe('Branches', () => { enforce_admins: { enabled: false } } }), - updateBranchProtection: jest.fn().mockImplementation(() => Promise.resolve('updateBranchProtection')), + updateBranchProtection: jest.fn().mockImplementation(() => Promise.resolve({ url: 'updateBranchProtection' })), deleteBranchProtection: jest.fn().mockImplementation(() => Promise.resolve('deleteBranchProtection')) } } @@ -204,7 +204,7 @@ describe('Branches', () => { }) }) - describe.skip('return values', () => { + describe('return values', () => { it('returns updateBranchProtection Promise', () => { const plugin = configure( [{ diff --git a/test/unit/lib/plugins/milestones.test.js b/test/unit/lib/plugins/milestones.test.js index 93cde7db..c08bab38 100644 --- a/test/unit/lib/plugins/milestones.test.js +++ b/test/unit/lib/plugins/milestones.test.js @@ -1,19 +1,24 @@ +const { getLog } = require('probot/lib/helpers/get-log') const Milestones = require('../../../../lib/plugins/milestones') -describe.skip('Milestones', () => { +describe('Milestones', () => { let github + const log = getLog() + log.level = process.env.LOG_LEVEL ?? 'info' function configure (config) { - return new Milestones(github, { owner: 'bkeepers', repo: 'test' }, config) + const noop = false + const errors = [] + return new Milestones(noop, github, { owner: 'bkeepers', repo: 'test' }, config, log, errors) } beforeEach(() => { github = { - paginate: jest.fn().mockImplementation(() => Promise.resolve()), + paginate: jest.fn().mockResolvedValue({}), issues: { - listMilestonesForRepo: { + listMilestones: { endpoint: { - merge: jest.fn().mockImplementation(() => {}) + merge: jest.fn().mockImplementation(() => ({ route: 'GET /repos/{owner}/{repo}/milestones' })) } }, createMilestone: jest.fn().mockImplementation(() => Promise.resolve()), @@ -25,12 +30,12 @@ describe.skip('Milestones', () => { describe('sync', () => { it('syncs milestones', async () => { - github.paginate.mockReturnValueOnce(Promise.resolve([ + github.paginate.mockResolvedValueOnce([ { title: 'no-change', description: 'no-change-description', due_on: null, state: 'open', number: 5 }, { title: 'new-description', description: 'old-description', due_on: null, state: 'open', number: 2 }, { title: 'new-state', description: 'FF0000', due_on: null, state: 'open', number: 4 }, { title: 'remove-milestone', description: 'old-description', due_on: null, state: 'open', number: 1 } - ])) + ]) const plugin = configure([ { title: 'no-change', description: 'no-change-description', due_on: '2019-03-29T07:00:00Z', state: 'open' }, diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index c289f563..c91583d3 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -1,6 +1,13 @@ /* eslint-disable no-undef */ - +const { Octokit } = require('octokit') const Settings = require('../../../lib/settings') +const yaml = require('js-yaml') +// jest.mock('../../../lib/settings', () => { +// const OriginalSettings = jest.requireActual('../../../lib/settings') +// //const orginalSettingsInstance = new OriginalSettings(false, stubContext, mockRepo, config, mockRef, mockSubOrg) +// return OriginalSettings +// }) + describe('Settings Tests', () => { let stubContext @@ -8,19 +15,61 @@ describe('Settings Tests', () => { let stubConfig let mockRef let mockSubOrg + let subOrgConfig - function createSettings (config) { - return new Settings(false, stubContext, mockRepo, config, mockRef, mockSubOrg) + function createSettings(config) { + const settings = new Settings(false, stubContext, mockRepo, config, mockRef, mockSubOrg) + return settings; } beforeEach(() => { + const mockOctokit = jest.mocked(Octokit) + const content = Buffer.from(` +suborgrepos: +- new-repo +#- test* +#- secret* + +suborgteams: +- core + +suborgproperties: +- EDP: true +- do_no_delete: true + +teams: + - name: core + permission: bypass + - name: docss + permission: pull + - name: docs + permission: pull + +validator: + pattern: '[a-zA-Z0-9_-]+_[a-zA-Z0-9_-]+.*' + +repository: + # A comma-separated list of topics to set on the repository + topics: + - frontend + `).toString('base64'); + mockOctokit.repos = { + getContent: jest.fn().mockResolvedValue({ data: { content } }) + } + + mockOctokit.request = { + endpoint: jest.fn().mockReturnValue({}) + } + + mockOctokit.paginate = jest.fn().mockResolvedValue([]) + stubContext = { payload: { installation: { id: 123 } }, - octokit: jest.fn(), + octokit: mockOctokit, log: { debug: jest.fn((msg) => { console.log(msg) @@ -34,9 +83,11 @@ describe('Settings Tests', () => { } } - mockRepo = jest.fn() - mockRef = jest.fn() - mockSubOrg = jest.fn() + + + mockRepo = { owner: 'test', repo: 'test-repo' } + mockRef = 'main' + mockSubOrg = 'frontend' }) describe('restrictedRepos', () => { @@ -140,4 +191,76 @@ describe('Settings Tests', () => { }) }) }) // restrictedRepos + + describe('loadConfigs', () => { + describe('load suborg configs', () => { + beforeEach(() => { + stubConfig = { + restrictedRepos: { + } + } + subOrgConfig = yaml.load(` + suborgrepos: + - new-repo + + suborgproperties: + - EDP: true + - do_no_delete: true + + teams: + - name: core + permission: bypass + - name: docss + permission: pull + - name: docs + permission: pull + + validator: + pattern: '[a-zA-Z0-9_-]+_[a-zA-Z0-9_-]+.*' + + repository: + # A comma-separated list of topics to set on the repository + topics: + - frontend + + `) + + }) + + it("Should load configMap for suborgs'", async () => { + //mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) + mockSubOrg = undefined + settings = createSettings(stubConfig) + jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: "frontend", path: ".github/suborgs/frontend.yml" }]) + jest.spyOn(settings, 'loadYaml').mockImplementation(() => subOrgConfig) + jest.spyOn(settings, 'getReposForTeam').mockImplementation(() => [{ name: 'repo-test' }]) + jest.spyOn(settings, 'getReposForCustomProperty').mockImplementation(() => [{ repository_name: 'repo-for-property' }]) + + const subOrgConfigs = await settings.getSubOrgConfigs() + expect(settings.loadConfigMap).toHaveBeenCalledTimes(1) + + // Get own properties of subOrgConfigs + const ownProperties = Object.getOwnPropertyNames(subOrgConfigs); + expect(ownProperties.length).toEqual(3) + }) + + it("Should throw an error when a repo is found in multiple suborgs configs'", async () => { + //mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) + mockSubOrg = undefined + settings = createSettings(stubConfig) + jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: "frontend", path: ".github/suborgs/frontend.yml" }, { name: "backend", path: ".github/suborgs/backend.yml" }]) + jest.spyOn(settings, 'loadYaml').mockImplementation(() => subOrgConfig) + jest.spyOn(settings, 'getReposForTeam').mockImplementation(() => [{ name: 'repo-test' }]) + jest.spyOn(settings, 'getReposForCustomProperty').mockImplementation(() => [{ repository_name: 'repo-for-property' }]) + + expect(async () => await settings.getSubOrgConfigs()).rejects.toThrow('Multiple suborg configs for new-repo in .github/suborgs/backend.yml and .github/suborgs/frontend.yml') + // try { + // await settings.getSubOrgConfigs() + // } catch (e) { + // console.log(e) + // } + }) + }) + }) // loadConfigs + }) // Settings Tests