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/worker.ts b/src/utils/worker.ts index 3b44f3d..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 { findDefaultBranch } from "./findDefaultBranch.js"; -import { findDefaultBranchSHA } 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 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); - } + 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/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, + }, }