From cc47465dc190d9cd02a2424cf10e7c26d8a868f7 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Mon, 4 Dec 2023 13:51:04 -0400 Subject: [PATCH] feat: Actually use user provided options (#15) Use user provided options when bundler is bundling. --- .../bundleAnalysisPluginFactory.test.ts | 42 ++++++++++++ .../bundleAnalysisPluginFactory.ts | 20 +++--- .../src/errors/InvalidSlugError.ts | 5 ++ .../src/errors/NoCommitShaError.ts | 5 ++ packages/bundler-plugin-core/src/types.ts | 15 ----- .../utils/__tests__/getPreSignedURL.test.ts | 64 +++++++++++++++---- .../utils/__tests__/preProcessBody.test.ts | 44 +++++++++++++ .../src/utils/constants.ts | 3 + .../src/utils/getPreSignedURL.ts | 13 ++-- .../src/utils/isProgramInstalled.ts | 2 +- .../src/utils/preProcessBody.ts | 24 +++++++ .../utils/providers/__tests__/Local.test.ts | 20 ++++++ .../utils/providers/__tests__/index.test.ts | 34 ++++++---- 13 files changed, 239 insertions(+), 52 deletions(-) create mode 100644 packages/bundler-plugin-core/src/bundle-analysis/__tests__/bundleAnalysisPluginFactory.test.ts create mode 100644 packages/bundler-plugin-core/src/errors/InvalidSlugError.ts create mode 100644 packages/bundler-plugin-core/src/errors/NoCommitShaError.ts diff --git a/packages/bundler-plugin-core/src/bundle-analysis/__tests__/bundleAnalysisPluginFactory.test.ts b/packages/bundler-plugin-core/src/bundle-analysis/__tests__/bundleAnalysisPluginFactory.test.ts new file mode 100644 index 00000000..8bf70d2a --- /dev/null +++ b/packages/bundler-plugin-core/src/bundle-analysis/__tests__/bundleAnalysisPluginFactory.test.ts @@ -0,0 +1,42 @@ +import { bundleAnalysisPluginFactory } from "../bundleAnalysisPluginFactory"; + +describe("bundleAnalysisPluginFactory", () => { + it("returns a build start function", () => { + const plugin = bundleAnalysisPluginFactory({ + userOptions: {}, + bundleAnalysisUploadPlugin: () => ({ + version: "1", + name: "plugin-name", + pluginVersion: "1.0.0", + }), + }); + + expect(plugin.buildStart).toEqual(expect.any(Function)); + }); + + it("returns a build end function", () => { + const plugin = bundleAnalysisPluginFactory({ + userOptions: {}, + bundleAnalysisUploadPlugin: () => ({ + version: "1", + name: "plugin-name", + pluginVersion: "1.0.0", + }), + }); + + expect(plugin.buildEnd).toEqual(expect.any(Function)); + }); + + it("returns a write bundle function", () => { + const plugin = bundleAnalysisPluginFactory({ + userOptions: {}, + bundleAnalysisUploadPlugin: () => ({ + version: "1", + name: "plugin-name", + pluginVersion: "1.0.0", + }), + }); + + expect(plugin.writeBundle).toEqual(expect.any(Function)); + }); +}); diff --git a/packages/bundler-plugin-core/src/bundle-analysis/bundleAnalysisPluginFactory.ts b/packages/bundler-plugin-core/src/bundle-analysis/bundleAnalysisPluginFactory.ts index c02f13e7..eb5dff2c 100644 --- a/packages/bundler-plugin-core/src/bundle-analysis/bundleAnalysisPluginFactory.ts +++ b/packages/bundler-plugin-core/src/bundle-analysis/bundleAnalysisPluginFactory.ts @@ -52,25 +52,25 @@ export const bundleAnalysisPluginFactory = ({ const inputs: ProviderUtilInputs = { envs, args }; const provider = await detectProvider(inputs); - let sendStats = true; let url = ""; try { url = await getPreSignedURL({ - apiURL: "http://localhost:3000", - globalUploadToken: "123", + apiURL: userOptions?.apiUrl ?? "https://api.codecov.io", + globalUploadToken: userOptions?.globalUploadToken, + repoToken: userOptions?.repoToken, serviceParams: provider, + retryCount: userOptions?.retryCount, }); } catch (error) { - sendStats = false; + return; } try { - if (sendStats) { - await uploadStats({ - preSignedUrl: url, - message: JSON.stringify(output), - }); - } + await uploadStats({ + preSignedUrl: url, + message: JSON.stringify(output), + retryCount: userOptions?.retryCount, + }); } catch {} }, }; diff --git a/packages/bundler-plugin-core/src/errors/InvalidSlugError.ts b/packages/bundler-plugin-core/src/errors/InvalidSlugError.ts new file mode 100644 index 00000000..c33ac778 --- /dev/null +++ b/packages/bundler-plugin-core/src/errors/InvalidSlugError.ts @@ -0,0 +1,5 @@ +export class InvalidSlugError extends Error { + constructor(msg: string) { + super(msg); + } +} diff --git a/packages/bundler-plugin-core/src/errors/NoCommitShaError.ts b/packages/bundler-plugin-core/src/errors/NoCommitShaError.ts new file mode 100644 index 00000000..3c5f6151 --- /dev/null +++ b/packages/bundler-plugin-core/src/errors/NoCommitShaError.ts @@ -0,0 +1,5 @@ +export class NoCommitShaError extends Error { + constructor(msg: string) { + super(msg); + } +} diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugin-core/src/types.ts index 04747b76..af13418c 100644 --- a/packages/bundler-plugin-core/src/types.ts +++ b/packages/bundler-plugin-core/src/types.ts @@ -57,13 +57,6 @@ export interface Options { */ globalUploadToken?: string; - /** - * The name of the repository to upload the bundle analysis information to. - * - * `globalUploadToken` and `repoName` must be set if this is not set. - */ - repoName?: string; - /** * The upload token to use for uploading the bundle analysis information. * @@ -71,14 +64,6 @@ export interface Options { */ repoToken?: string; - /** - * The commit hash to use for uploading the bundle analysis information. - * - * Defaults package.json name field. - */ - namespace?: string; - - // TODO: Update the default value here /** * The api url used to fetch the upload url. * diff --git a/packages/bundler-plugin-core/src/utils/__tests__/getPreSignedURL.test.ts b/packages/bundler-plugin-core/src/utils/__tests__/getPreSignedURL.test.ts index 6bfd1890..fa8553c0 100644 --- a/packages/bundler-plugin-core/src/utils/__tests__/getPreSignedURL.test.ts +++ b/packages/bundler-plugin-core/src/utils/__tests__/getPreSignedURL.test.ts @@ -5,6 +5,7 @@ import { getPreSignedURL } from "../getPreSignedURL.ts"; import { FailedFetchError } from "../../errors/FailedFetchError.ts"; import { NoUploadTokenError } from "../../errors/NoUploadTokenError.ts"; import { UploadLimitReachedError } from "../../errors/UploadLimitReachedError.ts"; +import { NoCommitShaError } from "../../errors/NoCommitShaError.ts"; const server = setupServer(); @@ -56,6 +57,25 @@ describe("getPreSignedURL", () => { describe("successful request", () => { describe("when the initial response is successful", () => { + describe('"globalUploadToken" is provided and "repoToken" is', () => { + it("returns the pre-signed URL", async () => { + setup({ + data: { url: "http://example.com" }, + }); + + const url = await getPreSignedURL({ + apiURL: "http://localhost", + globalUploadToken: "super-cool-token", + repoToken: "super-repo-token", + serviceParams: { + commit: "123", + }, + }); + + expect(url).toEqual("http://example.com"); + }); + }); + describe('"globalUploadToken" is provided and "repoToken" is not', () => { it("returns the pre-signed URL", async () => { setup({ @@ -95,17 +115,16 @@ describe("getPreSignedURL", () => { }); describe("unsuccessful request", () => { - describe("return body does not match schema", () => { + describe("no upload token found", () => { it("throws an error", async () => { const { consoleSpy } = setup({ - data: { randomValue: "random" }, + data: { url: "http://example.com" }, }); let error; try { await getPreSignedURL({ apiURL: "http://localhost", - globalUploadToken: "cool-upload-token", serviceParams: { commit: "123", }, @@ -115,11 +134,16 @@ describe("getPreSignedURL", () => { } expect(consoleSpy).toHaveBeenCalled(); - expect(error).toBeInstanceOf(FailedFetchError); + // for some reason, this test fails even tho it's the same values + // Expected: "No upload token found" + // Received: "No upload token found" + // Number of calls: 1 + // expect(consoleSpy).toHaveBeenCalledWith("No upload token found"); + expect(error).toBeInstanceOf(NoUploadTokenError); }); }); - describe("no upload token found", () => { + describe("no commit sha found", () => { it("throws an error", async () => { const { consoleSpy } = setup({ data: { url: "http://example.com" }, @@ -129,6 +153,29 @@ describe("getPreSignedURL", () => { try { await getPreSignedURL({ apiURL: "http://localhost", + globalUploadToken: "global-upload-token", + serviceParams: {}, + }); + } catch (e) { + error = e; + } + + expect(consoleSpy).toHaveBeenCalled(); + expect(error).toBeInstanceOf(NoCommitShaError); + }); + }); + + describe("return body does not match schema", () => { + it("throws an error", async () => { + const { consoleSpy } = setup({ + data: { randomValue: "random" }, + }); + + let error; + try { + await getPreSignedURL({ + apiURL: "http://localhost", + globalUploadToken: "cool-upload-token", serviceParams: { commit: "123", }, @@ -138,12 +185,7 @@ describe("getPreSignedURL", () => { } expect(consoleSpy).toHaveBeenCalled(); - // for some reason, this test fails even tho it's the same values - // Expected: "No upload token found" - // Received: "No upload token found" - // Number of calls: 1 - // expect(consoleSpy).toHaveBeenCalledWith("No upload token found"); - expect(error).toBeInstanceOf(NoUploadTokenError); + expect(error).toBeInstanceOf(FailedFetchError); }); }); diff --git a/packages/bundler-plugin-core/src/utils/__tests__/preProcessBody.test.ts b/packages/bundler-plugin-core/src/utils/__tests__/preProcessBody.test.ts index c32ccffa..22ef09aa 100644 --- a/packages/bundler-plugin-core/src/utils/__tests__/preProcessBody.test.ts +++ b/packages/bundler-plugin-core/src/utils/__tests__/preProcessBody.test.ts @@ -1,6 +1,17 @@ +import { InvalidSlugError } from "../../errors/InvalidSlugError"; import { preProcessBody } from "../preProcessBody"; describe("preProcessBody", () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, "log").mockImplementation(() => null); + }); + + afterEach(() => { + consoleSpy.mockReset(); + }); + describe("there is a valid value", () => { it("does not change the `string`", () => { const body = { @@ -11,6 +22,39 @@ describe("preProcessBody", () => { expect(result).toEqual({ key: "value" }); }); + + describe('the key is "slug"', () => { + describe("value is not an empty string", () => { + it('encodes the "slug" value', () => { + const body = { + slug: "codecov/engineering/applications-team/gazebo", + }; + + const result = preProcessBody(body); + + expect(result).toEqual({ + slug: "codecov:::engineering:::applications-team::::gazebo", + }); + }); + }); + + describe("value is an empty string", () => { + it('throws an "InvalidSlugError"', () => { + const body = { + slug: "", + }; + + let error; + try { + preProcessBody(body); + } catch (e) { + error = e; + } + + expect(error).toBeInstanceOf(InvalidSlugError); + }); + }); + }); }); describe("there is an empty string", () => { diff --git a/packages/bundler-plugin-core/src/utils/constants.ts b/packages/bundler-plugin-core/src/utils/constants.ts index 6df94dbc..b3770ec2 100644 --- a/packages/bundler-plugin-core/src/utils/constants.ts +++ b/packages/bundler-plugin-core/src/utils/constants.ts @@ -2,3 +2,6 @@ export const SPAWN_PROCESS_BUFFER_SIZE = 1_048_576 * 100; // 100 MiB export const DEFAULT_RETRY_COUNT = 3; export const DEFAULT_RETRY_DELAY = 1000; + +export const OWNER_SLUG_JOIN = ":::"; +export const REPO_SLUG_JOIN = "::::"; diff --git a/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts b/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts index 57fa7317..96d2c3fc 100644 --- a/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts +++ b/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts @@ -8,6 +8,7 @@ import { DEFAULT_RETRY_COUNT } from "./constants.ts"; import { fetchWithRetry } from "./fetchWithRetry.ts"; import { green, red, yellow } from "./logging.ts"; import { preProcessBody } from "./preProcessBody.ts"; +import { NoCommitShaError } from "../errors/NoCommitShaError.ts"; interface GetPreSignedURLArgs { apiURL: string; @@ -34,10 +35,14 @@ export const getPreSignedURL = async ({ throw new NoUploadTokenError("No upload token found"); } - const commitSha = serviceParams?.commit ?? ""; - const url = `${apiURL}/upload/service/commits/${commitSha}/bundle_analysis`; + const commitSha = serviceParams?.commit; + + if (!commitSha) { + red("No commit found"); + throw new NoCommitShaError("No commit found"); + } - const sentServiceParams = preProcessBody({ token, ...serviceParams }); + const url = `${apiURL}/upload/service/commits/${commitSha}/bundle_analysis`; let response: Response; try { @@ -51,7 +56,7 @@ export const getPreSignedURL = async ({ "Content-Type": "application/json", Authorization: `token ${token}`, }, - body: JSON.stringify(sentServiceParams), + body: JSON.stringify(preProcessBody(serviceParams)), }, }); } catch (e) { diff --git a/packages/bundler-plugin-core/src/utils/isProgramInstalled.ts b/packages/bundler-plugin-core/src/utils/isProgramInstalled.ts index 2f3f5fdd..8a1d0765 100644 --- a/packages/bundler-plugin-core/src/utils/isProgramInstalled.ts +++ b/packages/bundler-plugin-core/src/utils/isProgramInstalled.ts @@ -1,5 +1,5 @@ import childprocess from "child_process"; export function isProgramInstalled(programName: string): boolean { - return !childprocess.spawnSync(programName).error; + return !childprocess?.spawnSync(programName)?.error; } diff --git a/packages/bundler-plugin-core/src/utils/preProcessBody.ts b/packages/bundler-plugin-core/src/utils/preProcessBody.ts index 35bc5151..50b74b05 100644 --- a/packages/bundler-plugin-core/src/utils/preProcessBody.ts +++ b/packages/bundler-plugin-core/src/utils/preProcessBody.ts @@ -1,7 +1,15 @@ +import { InvalidSlugError } from "../errors/InvalidSlugError"; +import { OWNER_SLUG_JOIN, REPO_SLUG_JOIN } from "./constants"; +import { red } from "./logging"; + export const preProcessBody = ( body: Record, ) => { for (const [key, value] of Object.entries(body)) { + if (key === "slug" && typeof value === "string") { + body[key] = encodeSlug(value); + } + if (!value || value === "") { body[key] = null; } @@ -9,3 +17,19 @@ export const preProcessBody = ( return body; }; + +export const encodeSlug = (slug: string): string => { + const repoIndex = slug.lastIndexOf("/") + 1; + const owner = slug.substring(0, repoIndex).trimEnd(); + const repo = slug.substring(repoIndex, slug.length); + + if (owner === "" || repo === "") { + red("Invalid owner and/or repo"); + throw new InvalidSlugError("Invalid owner and/or repo"); + } + + const encodedOwner = owner?.split("/").join(OWNER_SLUG_JOIN).slice(0, -3); + const encodedSlug = [encodedOwner, repo].join(REPO_SLUG_JOIN); + + return encodedSlug; +}; diff --git a/packages/bundler-plugin-core/src/utils/providers/__tests__/Local.test.ts b/packages/bundler-plugin-core/src/utils/providers/__tests__/Local.test.ts index 97f8b639..deec7f49 100644 --- a/packages/bundler-plugin-core/src/utils/providers/__tests__/Local.test.ts +++ b/packages/bundler-plugin-core/src/utils/providers/__tests__/Local.test.ts @@ -20,11 +20,17 @@ describe("Local Params", () => { td.when(spawnSync("git")).thenReturn({ error: new Error("Git is not installed!"), }); + const detected = Local.detect(); expect(detected).toBeFalsy(); }); it("does run with git installed", () => { + const spawnSync = td.replace(childProcess, "spawnSync"); + td.when(spawnSync("git")).thenReturn({ + error: undefined, + }); + const detected = Local.detect(); expect(detected).toBeTruthy(); }); @@ -43,6 +49,7 @@ describe("Local Params", () => { }, envs: {}, }; + const expected: ProviderServiceParams = { branch: "main", build: "", @@ -71,6 +78,7 @@ describe("Local Params", () => { GIT_BRANCH: "main", }, }; + const expected: ProviderServiceParams = { branch: "main", build: "", @@ -81,6 +89,7 @@ describe("Local Params", () => { service: "", slug: "owner/repo", }; + const params = await Local.getServiceParams(inputs); expect(params).toMatchObject(expected); }); @@ -120,6 +129,7 @@ describe("Local Params", () => { it("can get the slug from a git url", async () => { const spawnSync = td.replace(childProcess, "spawnSync"); + td.when( spawnSync("git", ["config", "--get", "remote.origin.url"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -127,6 +137,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("git@github.com:testOrg/testRepo.git"), }); + td.when( spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -134,6 +145,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("main"), }); + td.when( spawnSync("git", ["rev-parse", "HEAD"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -141,6 +153,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("testSHA"), }); + const params = await Local.getServiceParams(inputs); expect(params.slug).toBe("testOrg/testRepo"); }); @@ -154,6 +167,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("notaurl"), }); + td.when( spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -161,6 +175,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("main"), }); + td.when( spawnSync("git", ["rev-parse", "HEAD"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -168,11 +183,13 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("testSHA"), }); + await expect(Local.getServiceParams(inputs)).rejects.toThrow(); }); it("errors on a malformed slug", async () => { const spawnSync = td.replace(childProcess, "spawnSync"); + td.when( spawnSync("git", ["config", "--get", "remote.origin.url"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -180,6 +197,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("http://github.com/testOrg/testRepo.git"), }); + td.when( spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -187,6 +205,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("main"), }); + td.when( spawnSync("git", ["rev-parse", "HEAD"], { maxBuffer: SPAWN_PROCESS_BUFFER_SIZE, @@ -194,6 +213,7 @@ describe("Local Params", () => { ).thenReturn({ stdout: Buffer.from("testSHA"), }); + const params = await Local.getServiceParams(inputs); expect(params.slug).toBe("testOrg/testRepo"); }); diff --git a/packages/bundler-plugin-core/src/utils/providers/__tests__/index.test.ts b/packages/bundler-plugin-core/src/utils/providers/__tests__/index.test.ts index 65e784e3..b935c4f4 100644 --- a/packages/bundler-plugin-core/src/utils/providers/__tests__/index.test.ts +++ b/packages/bundler-plugin-core/src/utils/providers/__tests__/index.test.ts @@ -50,19 +50,19 @@ describe("CI Providers", () => { }); describe(`${provider.getServiceName()} can return a ProviderServiceParams object that`, () => { - const inputs: ProviderUtilInputs = { - envs: {}, - args: { - ...createEmptyArgs(), - ...{ - branch: "main", - sha: "123", - slug: "testOrg/testRepo", + it("has a sha", async () => { + const inputs: ProviderUtilInputs = { + envs: {}, + args: { + ...createEmptyArgs(), + ...{ + branch: "main", + sha: "123", + slug: "testOrg/testRepo", + }, }, - }, - }; + }; - it("has a sha", async () => { const serviceParams = await provider.getServiceParams(inputs); expect(serviceParams).not.toBeNull(); @@ -70,6 +70,18 @@ describe("CI Providers", () => { }); it("has a slug", async () => { + const inputs: ProviderUtilInputs = { + envs: {}, + args: { + ...createEmptyArgs(), + ...{ + branch: "main", + sha: "123", + slug: "testOrg/testRepo", + }, + }, + }; + const serviceParams = await provider.getServiceParams(inputs); expect(serviceParams).not.toBeNull();