diff --git a/lib/settings.js b/lib/settings.js index 919e0185..d8863543 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -370,7 +370,7 @@ ${this.results.reduce((x, y) => { const RepoPlugin = Settings.PLUGINS.repository const archivePlugin = new Archive(this.nop, this.github, repo, repoConfig, this.log) - const { shouldArchive, shouldUnarchive } = await archivePlugin.getState() + const { isArchived, shouldArchive, shouldUnarchive } = await archivePlugin.getState() if (shouldUnarchive) { this.log.debug(`Unarchiving repo ${repo.repo}`) @@ -378,15 +378,24 @@ ${this.results.reduce((x, y) => { this.appendToResults(unArchiveResults) } - const repoResults = await new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync() - this.appendToResults(repoResults) + // An archived repo is read-only: GitHub rejects any settings update + // (repository, branch protection, labels, teams, etc.) with a 403. If a + // repo is archived and is not being unarchived in this run, skip every + // other plugin and let only the archive plugin run. Newly-archived repos + // (shouldArchive) are still configured first and archived last, below. + if (isArchived && !shouldUnarchive) { + this.log.debug(`Skipping settings sync for archived repo ${repo.repo}`) + } else { + const repoResults = await new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync() + this.appendToResults(repoResults) - const childResults = await Promise.all( - childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync() - }) - ) - this.appendToResults(childResults) + const childResults = await Promise.all( + childPlugins.map(([Plugin, config]) => { + return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync() + }) + ) + this.appendToResults(childResults) + } if (shouldArchive) { this.log.debug(`Archiving repo ${repo.repo}`) diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index b3610651..956fbdca 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -462,4 +462,75 @@ repository: ); }); }); + + describe('updateRepos archived repos', () => { + const Archive = require('../../../lib/plugins/archive') + let settings + + beforeEach(() => { + // suborg must be undefined, otherwise updateRepos returns early because the + // repo is not part of the changed suborg config. + mockSubOrg = undefined + stubConfig = { + restrictedRepos: {}, + // Presence of a repository section means a repoConfig is built and the + // main `if (repoConfig)` branch of updateRepos runs. + repository: { has_wiki: false } + } + settings = createSettings(stubConfig) + // Avoid any network calls for config loading. + settings.subOrgConfigs = {} + settings.repoConfigs = {} + jest.spyOn(settings, 'childPluginsList').mockReturnValue([]) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('skips the repository plugin for a repo that is already archived', async () => { + jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ + isArchived: true, + shouldArchive: false, + shouldUnarchive: false + }) + const repoSync = jest.spyOn(Settings.PLUGINS.repository.prototype, 'sync').mockResolvedValue([]) + const archiveSync = jest.spyOn(Archive.prototype, 'sync').mockResolvedValue([]) + + await settings.updateRepos({ owner: 'test', repo: 'archived-repo' }) + + // No settings update is attempted against the read-only archived repo. + expect(repoSync).not.toHaveBeenCalled() + expect(archiveSync).not.toHaveBeenCalled() + }) + + it('configures and then archives a repo that is being newly archived', async () => { + jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ + isArchived: false, + shouldArchive: true, + shouldUnarchive: false + }) + const repoSync = jest.spyOn(Settings.PLUGINS.repository.prototype, 'sync').mockResolvedValue([]) + const archiveSync = jest.spyOn(Archive.prototype, 'sync').mockResolvedValue([]) + + await settings.updateRepos({ owner: 'test', repo: 'to-archive' }) + + // The repo is still writable, so settings are applied before it is archived. + expect(repoSync).toHaveBeenCalled() + expect(archiveSync).toHaveBeenCalled() + }) + + it('configures a non-archived repo as usual', async () => { + jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ + isArchived: false, + shouldArchive: false, + shouldUnarchive: false + }) + const repoSync = jest.spyOn(Settings.PLUGINS.repository.prototype, 'sync').mockResolvedValue([]) + + await settings.updateRepos({ owner: 'test', repo: 'normal-repo' }) + + expect(repoSync).toHaveBeenCalled() + }) + }) // updateRepos archived repos }) // Settings Tests