From 4452422bea3d81bd7cdecb56f56148787af81026 Mon Sep 17 00:00:00 2001 From: Igwe Kalu Date: Mon, 19 Feb 2024 13:47:58 +0100 Subject: [PATCH] igwejk/continue on repository loop error (#139) * Fix spelling 'Defulat' --> 'Default' * Isolate exceptions due individual repository entry An exception when executing enablement in a single repository should not cause premature exit of the overall enablement process. Exceptions should be handled in isolation, and the overall process should continue. --- .devcontainer/devcontainer.json | 4 +- src/utils/enableFeaturesForRepository.ts | 121 ++++++++++++ src/utils/findDefaultBranch.ts | 2 +- src/utils/findDefaultBranchSHA.ts | 2 +- src/utils/worker.ts | 108 ++--------- tests/enableFeaturesForRepository.test.ts | 219 ++++++++++++++++++++++ tests/findDefaultBranch.test.ts | 6 +- tests/findDefaultBranchSHA.test.ts | 6 +- tsconfig.json | 4 +- 9 files changed, 364 insertions(+), 108 deletions(-) create mode 100644 src/utils/enableFeaturesForRepository.ts create mode 100644 tests/enableFeaturesForRepository.test.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1d566e0..ba59da0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,8 +14,8 @@ "github.copilot", "donjayamanne.githistory", "nixon.env-cmd-file-syntax", - "mattpocock.ts-error-translator" + "mattpocock.ts-error-translator", ], "postCreateCommand": "yarn install --frozen-lockfile && yarn run build", - "remoteUser": "root" + "remoteUser": "root", } diff --git a/src/utils/enableFeaturesForRepository.ts b/src/utils/enableFeaturesForRepository.ts new file mode 100644 index 0000000..4fb329b --- /dev/null +++ b/src/utils/enableFeaturesForRepository.ts @@ -0,0 +1,121 @@ +import { Octokit } from "@octokit/core"; + +import { inform } from "./globals"; +import { findDefaultBranch } from "./findDefaultBranch"; +import { findDefaultBranchSHA } from "./findDefaultBranchSHA"; +import { createBranch } from "./createBranch"; +import { enableSecretScanningAlerts } from "./enableSecretScanning"; +import { createPullRequest } from "./createPullRequest"; +import { writeToFile } from "./writeToFile"; +import { commitFileMac } from "./commitFile"; +import { enableGHAS } from "./enableGHAS"; +import { enableDependabotAlerts } from "./enableDependabotAlerts"; +import { enableDependabotFixes } from "./enableDependabotUpdates"; +import { enableIssueCreation } from "./enableIssueCreation"; +import { enableActionsOnRepo } from "./enableActions"; +import { checkIfCodeQLHasAlreadyRanOnRepo } from "./checkCodeQLEnablement"; + +export type RepositoryFeatures = { + enableDependabot: boolean; + enableDependabotUpdates: boolean; + enableSecretScanning: boolean; + enableCodeScanning: boolean; + enablePushProtection: boolean; + enableActions: boolean; + primaryLanguage: string; + createIssue: boolean; + repo: string; +}; + +export const enableFeaturesForRepository = async ({ + repository, + client, + generateAuth, +}: { + repository: RepositoryFeatures; + client: Octokit; + generateAuth: () => Promise; +}): Promise => { + const { + repo: repoName, + enableDependabot, + enableDependabotUpdates, + enableSecretScanning, + enablePushProtection, + primaryLanguage, + createIssue, + enableCodeScanning, + enableActions, + } = repository; + + const [owner, repo] = repoName.split("/"); + + // If Code Scanning or Secret Scanning need to be enabled, let's go ahead and enable GHAS first + enableCodeScanning || enableSecretScanning + ? await enableGHAS(owner, repo, client) + : null; + + // If they want to enable Dependabot, and they are NOT on GHES (as that currently isn't GA yet), enable Dependabot + enableDependabot && process.env.GHES != "true" + ? await enableDependabotAlerts(owner, repo, client) + : null; + + // If they want to enable Dependabot Security Updates, and they are NOT on GHES (as that currently isn't GA yet), enable Dependabot Security Updates + enableDependabotUpdates && process.env.GHES != "true" + ? await enableDependabotFixes(owner, repo, client) + : null; + + // Kick off the process for enabling Secret Scanning + enableSecretScanning + ? await enableSecretScanningAlerts( + owner, + repo, + client, + enablePushProtection, + ) + : null; + + // If they want to enable Actions + enableActions ? await enableActionsOnRepo(owner, repo, client) : null; + + // Kick off the process for enabling Code Scanning only if it is set to be enabled AND the primary language for the repo exists. If it doesn't exist that means CodeQL doesn't support it. + if (enableCodeScanning && primaryLanguage != "no-language") { + // First, let's check and see if CodeQL has already ran on that repository. If it has, we don't need to do anything. + const codeQLAlreadyRan = await checkIfCodeQLHasAlreadyRanOnRepo( + owner, + repo, + client, + ); + + inform( + `Has ${owner}/${repo} had a CodeQL scan uploaded? ${codeQLAlreadyRan}`, + ); + + if (!codeQLAlreadyRan) { + inform( + `As ${owner}/${repo} hasn't had a CodeQL Scan, going to run CodeQL enablement`, + ); + const defaultBranch = await findDefaultBranch(owner, repo, client); + const defaultBranchSHA = await findDefaultBranchSHA( + defaultBranch, + owner, + repo, + client, + ); + const ref = await createBranch(defaultBranchSHA, owner, repo, client); + const authToken = (await generateAuth()) as string; + await commitFileMac(owner, repo, primaryLanguage, ref, authToken); + const pullRequestURL = await createPullRequest( + defaultBranch, + ref, + owner, + repo, + client, + ); + if (createIssue) { + await enableIssueCreation(pullRequestURL, owner, repo, client); + } + await writeToFile(pullRequestURL); + } + } +}; diff --git a/src/utils/findDefaultBranch.ts b/src/utils/findDefaultBranch.ts index cc17ba8..820ec14 100644 --- a/src/utils/findDefaultBranch.ts +++ b/src/utils/findDefaultBranch.ts @@ -7,7 +7,7 @@ import { listDefaultBranchResponse, } from "./octokitTypes"; -export const findDefulatBranch = async ( +export const findDefaultBranch = async ( owner: string, repo: string, octokit: Octokit, diff --git a/src/utils/findDefaultBranchSHA.ts b/src/utils/findDefaultBranchSHA.ts index 64778f7..36b529b 100644 --- a/src/utils/findDefaultBranchSHA.ts +++ b/src/utils/findDefaultBranchSHA.ts @@ -7,7 +7,7 @@ import { listDefaultBranchSHAResponse, } from "./octokitTypes"; -export const findDefulatBranchSHA = async ( +export const findDefaultBranchSHA = async ( defaultBranch: string, owner: string, repo: string, diff --git a/src/utils/worker.ts b/src/utils/worker.ts index 8dddee1..2ea7a85 100644 --- a/src/utils/worker.ts +++ b/src/utils/worker.ts @@ -1,24 +1,10 @@ /* eslint-disable no-alert, no-await-in-loop */ import { readFileSync } from "node:fs"; - -import { findDefulatBranch } from "./findDefaultBranch.js"; -import { findDefulatBranchSHA } from "./findDefaultBranchSHA.js"; -import { createBranch } from "./createBranch.js"; -import { enableSecretScanningAlerts } from "./enableSecretScanning"; -import { createPullRequest } from "./createPullRequest.js"; -import { writeToFile } from "./writeToFile.js"; -import { client as octokit } from "./clients"; -import { commitFileMac } from "./commitFile.js"; -import { enableGHAS } from "./enableGHAS.js"; -import { enableDependabotAlerts } from "./enableDependabotAlerts"; -import { enableDependabotFixes } from "./enableDependabotUpdates"; -import { enableIssueCreation } from "./enableIssueCreation"; -import { enableActionsOnRepo } from "./enableActions"; -import { auth as generateAuth } from "./clients"; -import { checkIfCodeQLHasAlreadyRanOnRepo } from "./checkCodeQLEnablement"; - import { Octokit } from "@octokit/core"; + +import { enableFeaturesForRepository } from "./enableFeaturesForRepository"; +import { client as octokit, auth as generateAuth } from "./clients"; import { inform, reposFileLocation } from "./globals.js"; import { reposFile } from "../../types/common/index.js"; @@ -28,7 +14,9 @@ export const worker = async (): Promise => { let repoIndex: number; let repos: reposFile; let file: string; + const client = (await octokit()) as Octokit; + // Read the repos.json file and get the list of repos using fs.readFileSync, handle errors, if empty file return error, if file exists and is not empty JSON.parse it and return the list of repos try { file = readFileSync(reposFileLocation, "utf8"); @@ -57,87 +45,15 @@ export const worker = async (): Promise => { repos[orgIndex].repos.length }. The repo name is: ${repos[orgIndex].repos[repoIndex].repo}`, ); - const { - repo: repoName, - enableDependabot, - enableDependabotUpdates, - enableSecretScanning, - enablePushProtection, - primaryLanguage, - createIssue, - enableCodeScanning, - enableActions, - } = repos[orgIndex].repos[repoIndex]; - - const [owner, repo] = repoName.split("/"); - - // If Code Scanning or Secret Scanning need to be enabled, let's go ahead and enable GHAS first - enableCodeScanning || enableSecretScanning - ? await enableGHAS(owner, repo, client) - : null; - // If they want to enable Dependabot, and they are NOT on GHES (as that currently isn't GA yet), enable Dependabot - enableDependabot && process.env.GHES != "true" - ? await enableDependabotAlerts(owner, repo, client) - : null; - - // If they want to enable Dependabot Security Updates, and they are NOT on GHES (as that currently isn't GA yet), enable Dependabot Security Updates - enableDependabotUpdates && process.env.GHES != "true" - ? await enableDependabotFixes(owner, repo, client) - : null; - - // Kick off the process for enabling Secret Scanning - enableSecretScanning - ? await enableSecretScanningAlerts( - owner, - repo, - client, - enablePushProtection, - ) - : null; - - // If they want to enable Actions - enableActions ? await enableActionsOnRepo(owner, repo, client) : null; - - // Kick off the process for enabling Code Scanning only if it is set to be enabled AND the primary language for the repo exists. If it doesn't exist that means CodeQL doesn't support it. - if (enableCodeScanning && primaryLanguage != "no-language") { - // First, let's check and see if CodeQL has already ran on that repository. If it has, we don't need to do anything. - const codeQLAlreadyRan = await checkIfCodeQLHasAlreadyRanOnRepo( - owner, - repo, + try { + await enableFeaturesForRepository({ + repository: repos[orgIndex].repos[repoIndex], client, - ); - - inform( - `Has ${owner}/${repo} had a CodeQL scan uploaded? ${codeQLAlreadyRan}`, - ); - - if (!codeQLAlreadyRan) { - inform( - `As ${owner}/${repo} hasn't had a CodeQL Scan, going to run CodeQL enablement`, - ); - const defaultBranch = await findDefulatBranch(owner, repo, client); - const defaultBranchSHA = await findDefulatBranchSHA( - defaultBranch, - owner, - repo, - client, - ); - const ref = await createBranch(defaultBranchSHA, owner, repo, client); - const authToken = (await generateAuth()) as string; - await commitFileMac(owner, repo, primaryLanguage, ref, authToken); - const pullRequestURL = await createPullRequest( - defaultBranch, - ref, - owner, - repo, - client, - ); - if (createIssue) { - await enableIssueCreation(pullRequestURL, owner, repo, client); - } - await writeToFile(pullRequestURL); - } + generateAuth, + }); + } catch (err) { + // boo } } } diff --git a/tests/enableFeaturesForRepository.test.ts b/tests/enableFeaturesForRepository.test.ts new file mode 100644 index 0000000..054cbcd --- /dev/null +++ b/tests/enableFeaturesForRepository.test.ts @@ -0,0 +1,219 @@ +import { + enableFeaturesForRepository, + RepositoryFeatures, +} from "../src/utils/enableFeaturesForRepository"; + +import { Octokit } from "@octokit/core"; +jest.mock("@octokit/core", () => { + return { + __esModule: true, + ...(jest.requireActual("@octokit/core") as any), + Octokit: jest.fn(), + }; +}); + +import { findDefaultBranch } from "../src/utils/findDefaultBranch"; +jest.mock("../src/utils/findDefaultBranch", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/findDefaultBranch") as any), + findDefaultBranch: jest.fn(), + }; +}); + +import { findDefaultBranchSHA } from "../src/utils/findDefaultBranchSHA"; +jest.mock("../src/utils/findDefaultBranchSHA", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/findDefaultBranchSHA") as any), + findDefaultBranchSHA: jest.fn(), + }; +}); + +import { createBranch } from "../src/utils/createBranch"; +jest.mock("../src/utils/createBranch", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/createBranch") as any), + createBranch: jest.fn(), + }; +}); + +import { enableSecretScanningAlerts } from "../src/utils/enableSecretScanning"; +jest.mock("../src/utils/enableSecretScanning", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/enableSecretScanning") as any), + enableSecretScanningAlerts: jest.fn(), + }; +}); + +import { createPullRequest } from "../src/utils/createPullRequest"; +jest.mock("../src/utils/createPullRequest", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/createPullRequest") as any), + createPullRequest: jest.fn(), + }; +}); + +import { commitFileMac } from "../src/utils/commitFile"; +jest.mock("../src/utils/commitFile", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/commitFile") as any), + commitFileMac: jest.fn(), + }; +}); + +import { enableGHAS } from "../src/utils/enableGHAS"; +jest.mock("../src/utils/enableGHAS", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/enableGHAS") as any), + enableGHAS: jest.fn(), + }; +}); + +import { enableDependabotAlerts } from "../src/utils/enableDependabotAlerts"; +jest.mock("../src/utils/enableDependabotAlerts", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/enableDependabotAlerts") as any), + enableDependabotAlerts: jest.fn(), + }; +}); + +import { enableDependabotFixes } from "../src/utils/enableDependabotUpdates"; +jest.mock("../src/utils/enableDependabotUpdates", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/enableDependabotUpdates") as any), + enableDependabotFixes: jest.fn(), + }; +}); + +import { enableActionsOnRepo } from "../src/utils/enableActions"; +jest.mock("../src/utils/enableActions", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/enableActions") as any), + enableActionsOnRepo: jest.fn(), + }; +}); + +import { client as octokit, auth as generateAuth } from "../src/utils/clients"; +jest.mock("../src/utils/clients", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/clients") as any), + client: jest.fn(), + auth: jest.fn().mockResolvedValue("token"), + }; +}); + +import { checkIfCodeQLHasAlreadyRanOnRepo } from "../src/utils/checkCodeQLEnablement"; +jest.mock("../src/utils/checkCodeQLEnablement", () => { + return { + __esModule: true, + ...(jest.requireActual("../src/utils/checkCodeQLEnablement") as any), + checkIfCodeQLHasAlreadyRanOnRepo: jest.fn(), + }; +}); + +describe("enableFeaturesForRepository", () => { + let repository: RepositoryFeatures; + let client: Octokit; + + const owner: string = "owner"; + const repo: string = "repo"; + + beforeEach(async () => { + repository = { + repo: `${owner}/${repo}`, + enableDependabot: false, + enableDependabotUpdates: false, + enableSecretScanning: false, + enablePushProtection: false, + primaryLanguage: "no-language", + createIssue: false, + enableCodeScanning: false, + enableActions: false, + }; + client = await octokit(); + }); + + it("should enable GHAS if Code Scanning need to be enabled", async () => { + repository.enableCodeScanning = true; + await enableFeaturesForRepository({ repository, client, generateAuth }); + expect(enableGHAS).toHaveBeenCalledWith(owner, repo, client); + }); + + it("should enable GHAS if Secret Scanning need to be enabled", async () => { + repository.enableSecretScanning = true; + await enableFeaturesForRepository({ repository, client, generateAuth }); + expect(enableGHAS).toHaveBeenCalledWith(owner, repo, client); + }); + + it("should enable Dependabot and Security Updates on GHEC if required", async () => { + repository.enableDependabot = true; + repository.enableDependabotUpdates = true; + process.env.GHES = "false"; + await enableFeaturesForRepository({ repository, client, generateAuth }); + expect(enableDependabotAlerts).toHaveBeenCalledWith(owner, repo, client); + expect(enableDependabotFixes).toHaveBeenCalledWith(owner, repo, client); + }); + + it("should enable Secret Scanning if required", async () => { + repository.enableSecretScanning = true; + repository.enablePushProtection = true; + await enableFeaturesForRepository({ repository, client, generateAuth }); + expect(enableSecretScanningAlerts).toHaveBeenCalledWith( + owner, + repo, + client, + repository.enablePushProtection, + ); + }); + + it("should enable Actions if required", async () => { + repository.enableActions = true; + await enableFeaturesForRepository({ repository, client, generateAuth }); + expect(enableActionsOnRepo).toHaveBeenCalledWith(owner, repo, client); + }); + + it("should enable Code Scanning if primary language is supported", async () => { + repository.enableCodeScanning = true; + repository.primaryLanguage = "javascript"; + + const defaultBranch = "main"; + const defaultBranchSHA = "123"; + + (checkIfCodeQLHasAlreadyRanOnRepo as jest.Mock).mockResolvedValue(false); + (findDefaultBranch as jest.Mock).mockResolvedValue(defaultBranch); + (findDefaultBranchSHA as jest.Mock).mockResolvedValue(defaultBranchSHA); + + await enableFeaturesForRepository({ repository, client, generateAuth }); + + expect(checkIfCodeQLHasAlreadyRanOnRepo).toHaveBeenCalledWith( + owner, + repo, + client, + ); + expect(findDefaultBranch).toHaveBeenCalledWith(owner, repo, client); + expect(findDefaultBranchSHA).toHaveBeenCalledWith( + defaultBranch, + owner, + repo, + client, + ); + expect(createBranch).toHaveBeenCalledWith( + defaultBranchSHA, + owner, + repo, + client, + ); + expect(commitFileMac).toHaveBeenCalled(); + expect(createPullRequest).toHaveBeenCalled(); + }); +}); diff --git a/tests/findDefaultBranch.test.ts b/tests/findDefaultBranch.test.ts index 03fc5ed..074b567 100644 --- a/tests/findDefaultBranch.test.ts +++ b/tests/findDefaultBranch.test.ts @@ -1,4 +1,4 @@ -import { findDefulatBranch } from "../src/utils/findDefaultBranch"; +import { findDefaultBranch } from "../src/utils/findDefaultBranch"; import { octokit } from "../src/utils/octokit"; @@ -38,7 +38,7 @@ describe("Default Branch", () => { }), ); - const response = await findDefulatBranch(repo, client); + const response = await findDefaultBranch(repo, client); expect(response).toStrictEqual(data.data.default_branch); }); it("Error in getting default branch", async () => { @@ -54,7 +54,7 @@ describe("Default Branch", () => { ); try { - await findDefulatBranch(repo, client); + await findDefaultBranch(repo, client); } catch (error: any) { expect(error.message).toEqual("Error finding default branch"); } diff --git a/tests/findDefaultBranchSHA.test.ts b/tests/findDefaultBranchSHA.test.ts index 79ed47b..5797236 100644 --- a/tests/findDefaultBranchSHA.test.ts +++ b/tests/findDefaultBranchSHA.test.ts @@ -1,4 +1,4 @@ -import { findDefulatBranchSHA } from "../src/utils/findDefaultBranchSHA"; +import { findDefaultBranchSHA } from "../src/utils/findDefaultBranchSHA"; import { octokit } from "../src/utils/octokit"; @@ -41,7 +41,7 @@ describe("Default Branch SHA", () => { }), ); - const response = await findDefulatBranchSHA(defaultBranch, repo, client); + const response = await findDefaultBranchSHA(defaultBranch, repo, client); expect(response).toStrictEqual(data.data.object.sha); }); @@ -58,7 +58,7 @@ describe("Default Branch SHA", () => { ); try { - await findDefulatBranchSHA(defaultBranch, repo, client); + await findDefaultBranchSHA(defaultBranch, repo, client); } catch (error: any) { expect(error.message).toEqual("Error finding default branch SHA"); } diff --git a/tsconfig.json b/tsconfig.json index c69172c..69dee4b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,6 @@ "noUnusedParameters": true, "removeComments": true, "preserveConstEnums": true, - "resolveJsonModule": true - } + "resolveJsonModule": true, + }, }