From faf2a5a6b2c42666aedecf9dfacbce7e554b3457 Mon Sep 17 00:00:00 2001 From: Tiago Pascoal Date: Sun, 6 Mar 2022 21:12:26 +0000 Subject: [PATCH] Support repo unarchiving If the repo is archived (and the archive config has been set) then other plugins will not be executed making unnecessary calls (they would fail since repo is in read only mode) This is optimized for the case that the archive source of truth is configured via safe settings If this isn't the case then plugins may fail if they try to make changes to archived repos, it will be their responsability to optimize for this scenario. --- lib/plugins/unarchiver.js | 67 +++++++++++++++++++++++++++++++++++++ lib/settings.js | 69 +++++++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 lib/plugins/unarchiver.js diff --git a/lib/plugins/unarchiver.js b/lib/plugins/unarchiver.js new file mode 100644 index 00000000..4a8cc239 --- /dev/null +++ b/lib/plugins/unarchiver.js @@ -0,0 +1,67 @@ +// THIS IS A SPECIAL PLUGIN, DON'T ADD IT TO THE LIST OF PLUGINS IN SETTINGS +const NopCommand = require('../nopcommand') + +const UnlockRepoMutation = ` +mutation($repoId: ID!) { + unarchiveRepository(input:{clientMutationId:"true",repositoryId: $repoId}) { + repository { + isArchived + } + } +}` +const GetRepoQuery = `query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo){ + id, + isArchived, + } +}` + +function returnValue (shouldContinue, nop) { + return { shouldContinue, nopCommands: nop } +} + +/// Unarchives repos before the other plugins. +/// This is optimized for the case where safe-settings is the source of truth when it cames to the archive config bit. + +/// If the archived config is not set then we don't even read the archived status for the repo (it saves one call) +/// This has the implication that if a repo is archived outside safe settings, other plugins may attempt to change the data and fail (it's their responsability to check if the repo is archived) +/// However if the archive config is set, we will return false to indicate other plugins shouldn't be executed if the repo is archived (and archive config is true) +module.exports = class Unarchiver { + constructor (nop, github, repo, settings, log) { + this.github = github + this.repo = repo + this.settings = settings + this.log = log + this.nop = nop + } + + // Returns true if should proceed false otherwise + async sync () { + if (typeof (this.settings?.archived) !== 'undefined') { + this.log.debug(`Checking if ${this.repo.owner}/${this.repo.repo} is archived`) + const graphQLResponse = await this.github.graphql(GetRepoQuery, { owner: this.repo.owner, repo: this.repo.repo }) + this.log(`Repo ${this.repo.owner}/${this.repo.repo} is ${graphQLResponse.repository.isArchived ? 'archived' : 'not archived'}`) + + if (graphQLResponse.repository.isArchived) { + if (this.settings.archived) { + this.log(`Repo ${this.repo.owner}/${this.repo.repo} already archived, inform other plugins should not run.`) + return returnValue(false) + } else { + this.log(`Unarchiving ${this.repo.owner}/${this.repo.repo} ${graphQLResponse.repository.id}`) + if (this.nop) { + return returnValue(true, [new NopCommand('Unarchiver', this.repo, null, 'will unarchive')]) + } else { + const graphQLUnlockResponse = await this.github.graphql(UnlockRepoMutation, { repoId: graphQLResponse.repository.id }) + this.log.debug(`Unarchived result ${JSON.stringify(graphQLUnlockResponse)}`) + + return returnValue(true) + } + } + } + this.log(`Repo ${this.repo.owner}/${this.repo.repo} not archived, ignoring.`) + } else { + this.log(`Repo ${this.repo.owner}/${this.repo.repo} archived config not set, ignoring.`) + } + return returnValue(true) + } +} diff --git a/lib/settings.js b/lib/settings.js index 6a97aad0..8bd2d0c1 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -3,6 +3,7 @@ const fs = require('fs') const Glob = require('./glob') const NopCommand = require('./nopcommand') const { throws } = require('assert') +const Unarchiver = require('./plugins/unarchiver') class Settings { static async syncAll(nop, context, repo, config, ref) { @@ -152,39 +153,49 @@ ${this.results.reduce((x,y) => { if (overrideRepoConfig) { repoConfig = this.mergeDeep({}, repoConfig, overrideRepoConfig) } - if (repoConfig) { - try { - this.log.debug(`found a matching repoconfig for this repo ${JSON.stringify(repoConfig)}`) + + // Although this is a plugin, it must be handled differently since it cannot run in parallel with the other plugins + // Because if the repo is archived we need to unarchived before all other plugins. + // If the repo is archived but there is no config to unarchive we don't run any plugins since they would fail anyway (and uncessary calls as well) + const {shouldContinue, nopCommands} = await new Unarchiver(this.nop, this.github, repo, repoConfig ?? config, this.log).sync() + + if (nopCommands) this.appendToResults(nopCommands) + + if(shouldContinue) { + if (repoConfig) { + try { + this.log.debug(`found a matching repoconfig for this repo ${JSON.stringify(repoConfig)}`) + const childPlugins = this.childPluginsList(repo.repo) + const RepoPlugin = Settings.PLUGINS.repository + return new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log).sync().then( res => { + this.appendToResults(res) + return Promise.all( + childPlugins.map(([Plugin, config]) => { + return new Plugin(this.nop, this.github, repo, config, this.log).sync() + })) + }).then( res => { + this.appendToResults(res) + }) + } catch(e) { + if (this.nop) { + const nopcommand = new NopCommand(this.constructor.name, this.repo, null,e, "ERROR") + console.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + this.appendToResults([nopcommand]) + //throw e + } else { + throw e + } + } + + } 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.repo) - const RepoPlugin = Settings.PLUGINS.repository - return new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log).sync().then( res => { - this.appendToResults(res) - return Promise.all( - childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log).sync() - })) - }).then( res => { + return Promise.all(childPlugins.map(([Plugin, config]) => { + return new Plugin(this.nop, this.github, repo, config, this.log).sync().then( res => { this.appendToResults(res) }) - } catch(e) { - if (this.nop) { - const nopcommand = new NopCommand(this.constructor.name, this.repo, null,e, "ERROR") - console.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - this.appendToResults([nopcommand]) - //throw e - } else { - throw e - } + })) } - - } 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.repo) - return Promise.all(childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log).sync().then( res => { - this.appendToResults(res) - }) - })) } }