diff --git a/lib/settings.js b/lib/settings.js index 919e0185..fd7ca269 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,6 +378,11 @@ ${this.results.reduce((x, y) => { this.appendToResults(unArchiveResults) } + if (isArchived && !shouldUnarchive) { + this.log.debug(`Skipping repo/child plugin updates for archived repo ${repo.repo}`) + return + } + const repoResults = await new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync() this.appendToResults(repoResults) diff --git a/schema/dereferenced/settings.json b/schema/dereferenced/settings.json index 8698b201..4dcdf0eb 100644 --- a/schema/dereferenced/settings.json +++ b/schema/dereferenced/settings.json @@ -50,6 +50,17 @@ } } }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, "code_security": { "type": "object", "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", @@ -90,6 +101,16 @@ } } }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, "secret_scanning_non_provider_patterns": { "type": "object", "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", diff --git a/test/unit/lib/plugins/archive.test.js b/test/unit/lib/plugins/archive.test.js index 0ed0f38d..a60eafac 100644 --- a/test/unit/lib/plugins/archive.test.js +++ b/test/unit/lib/plugins/archive.test.js @@ -93,6 +93,56 @@ describe('Archive Plugin', () => { }) }) + describe('getState', () => { + it('getState when repo is already archived and desired state is not set returns isArchived true shouldArchive false shouldUnarchive false', async () => { + // Arrange + github.rest.repos.get.mockResolvedValue({ data: { archived: true } }) + archive = new Archive(false, github, repo, {}, log) + + // Act + const result = await archive.getState() + + // Assert + expect(result).toEqual({ + isArchived: true, + shouldArchive: false, + shouldUnarchive: false + }) + }) + + it('getState when repo is not archived and desired state is not set returns isArchived false shouldArchive false shouldUnarchive false', async () => { + // Arrange + github.rest.repos.get.mockResolvedValue({ data: { archived: false } }) + archive = new Archive(false, github, repo, {}, log) + + // Act + const result = await archive.getState() + + // Assert + expect(result).toEqual({ + isArchived: false, + shouldArchive: false, + shouldUnarchive: false + }) + }) + + it('getState when repo is archived and desired state is false returns isArchived true shouldArchive false shouldUnarchive true', async () => { + // Arrange + github.rest.repos.get.mockResolvedValue({ data: { archived: true } }) + archive = new Archive(false, github, repo, { archived: false }, log) + + // Act + const result = await archive.getState() + + // Assert + expect(result).toEqual({ + isArchived: true, + shouldArchive: false, + shouldUnarchive: true + }) + }) + }) + describe('sync', () => { beforeEach(() => { archive = new Archive(false, github, repo, settings, log) diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index b3610651..e102379a 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -462,4 +462,91 @@ repository: ); }); }); + + describe('updateRepos - archived repo skipping', () => { + const Archive = require('../../../lib/plugins/archive') + + let settings + let mockRepoSync + let originalRepoPlugin + + beforeEach(() => { + // Preserve the original RepoPlugin so it can be restored after each test + originalRepoPlugin = Settings.PLUGINS.repository + + // Replace RepoPlugin with a mock constructor whose sync() we can assert on + mockRepoSync = jest.fn().mockResolvedValue([]) + Settings.PLUGINS.repository = jest.fn().mockImplementation(() => ({ + sync: mockRepoSync + })) + + // Build a Settings instance that will enter the `if (repoConfig)` branch: + // config.repository must be defined so repoConfig is truthy + settings = new Settings( + false, + stubContext, + { owner: 'test-org', repo: 'test-repo' }, + { repository: { name: 'test-repo' } }, + 'main' + ) + + // Pre-set subOrgConfigs so updateRepos() does not call the async getSubOrgConfigs() + settings.subOrgConfigs = {} + + // Pre-set repoConfigs so getRepoOverrideConfig() does not throw on undefined + settings.repoConfigs = {} + }) + + afterEach(() => { + // Restore the real RepoPlugin and all prototype spies + Settings.PLUGINS.repository = originalRepoPlugin + jest.restoreAllMocks() + }) + + it('updateRepos when repo is already archived and not being unarchived does not call RepoPlugin sync', async () => { + // Arrange + jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ + isArchived: true, + shouldArchive: false, + shouldUnarchive: false + }) + + // Act + await settings.updateRepos({ owner: 'test-org', repo: 'test-repo' }) + + // Assert + expect(mockRepoSync).not.toHaveBeenCalled() + }) + + it('updateRepos when repo is archived but is being unarchived calls RepoPlugin sync', async () => { + // Arrange + jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ + isArchived: true, + shouldArchive: false, + shouldUnarchive: true + }) + jest.spyOn(Archive.prototype, 'sync').mockResolvedValue([]) + + // Act + await settings.updateRepos({ owner: 'test-org', repo: 'test-repo' }) + + // Assert + expect(mockRepoSync).toHaveBeenCalledTimes(1) + }) + + it('updateRepos when repo is not archived calls RepoPlugin sync', async () => { + // Arrange + jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ + isArchived: false, + shouldArchive: false, + shouldUnarchive: false + }) + + // Act + await settings.updateRepos({ owner: 'test-org', repo: 'test-repo' }) + + // Assert + expect(mockRepoSync).toHaveBeenCalledTimes(1) + }) + }) // updateRepos - archived repo skipping }) // Settings Tests