aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/turbo-ignore/__tests__
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /packages/turbo-ignore/__tests__
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'packages/turbo-ignore/__tests__')
-rw-r--r--packages/turbo-ignore/__tests__/args.test.ts109
-rw-r--r--packages/turbo-ignore/__tests__/checkCommit.test.ts229
-rw-r--r--packages/turbo-ignore/__tests__/errors.test.ts46
-rw-r--r--packages/turbo-ignore/__tests__/getComparison.test.ts61
-rw-r--r--packages/turbo-ignore/__tests__/getTask.test.ts27
-rw-r--r--packages/turbo-ignore/__tests__/getWorkspace.test.ts62
-rw-r--r--packages/turbo-ignore/__tests__/ignore.test.ts578
7 files changed, 1112 insertions, 0 deletions
diff --git a/packages/turbo-ignore/__tests__/args.test.ts b/packages/turbo-ignore/__tests__/args.test.ts
new file mode 100644
index 0000000..f546247
--- /dev/null
+++ b/packages/turbo-ignore/__tests__/args.test.ts
@@ -0,0 +1,109 @@
+import parseArgs, { help } from "../src/args";
+import pkg from "../package.json";
+import { spyConsole, spyExit } from "@turbo/test-utils";
+
+describe("parseArgs()", () => {
+ const mockConsole = spyConsole();
+ const mockExit = spyExit();
+
+ it("does not throw with no args", async () => {
+ const result = parseArgs({ argv: [] });
+ expect(result.workspace).toBe(undefined);
+ expect(result.fallback).toBe(undefined);
+ expect(result.task).toBe(undefined);
+ });
+
+ it("outputs help text (--help)", async () => {
+ parseArgs({ argv: ["--help"] });
+ expect(mockExit.exit).toHaveBeenCalledWith(0);
+ expect(mockConsole.log).toHaveBeenCalledWith(help);
+ });
+
+ it("outputs help text (-h)", async () => {
+ parseArgs({ argv: ["-h"] });
+ expect(mockExit.exit).toHaveBeenCalledWith(0);
+ expect(mockConsole.log).toHaveBeenCalledWith(help);
+ });
+
+ it("outputs version text (--version)", async () => {
+ parseArgs({ argv: ["--version"] });
+ expect(mockExit.exit).toHaveBeenCalledWith(0);
+ expect(mockConsole.log).toHaveBeenCalledWith(pkg.version);
+ });
+
+ it("outputs version text (-v)", async () => {
+ parseArgs({ argv: ["-v"] });
+ expect(mockExit.exit).toHaveBeenCalledWith(0);
+ expect(mockConsole.log).toHaveBeenCalledWith(pkg.version);
+ });
+
+ it("correctly finds workspace", async () => {
+ const result = parseArgs({ argv: ["this-workspace"] });
+ expect(result.workspace).toBe("this-workspace");
+ expect(result.fallback).toBe(undefined);
+ expect(result.task).toBe(undefined);
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+
+ it("correctly finds fallback", async () => {
+ const result = parseArgs({ argv: ["--fallback=HEAD^"] });
+ expect(result.workspace).toBe(undefined);
+ expect(result.fallback).toBe("HEAD^");
+ expect(result.task).toBe(undefined);
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+
+ it("correctly finds task", async () => {
+ const result = parseArgs({ argv: ["--task=some-workspace#build"] });
+ expect(result.workspace).toBe(undefined);
+ expect(result.fallback).toBe(undefined);
+ expect(result.task).toBe("some-workspace#build");
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+
+ it("uses default fallback if incorrectly specified", async () => {
+ const result = parseArgs({ argv: ["--fallback"] });
+ expect(result.workspace).toBe(undefined);
+ expect(result.fallback).toBe(undefined);
+ expect(result.task).toBe(undefined);
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+
+ it("uses default fallback if empty string", async () => {
+ const result = parseArgs({ argv: ["--fallback="] });
+ expect(result.workspace).toBe(undefined);
+ expect(result.fallback).toBe(undefined);
+ expect(result.task).toBe(undefined);
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+
+ it("uses default task if incorrectly specified", async () => {
+ const result = parseArgs({ argv: ["--task"] });
+ expect(result.workspace).toBe(undefined);
+ expect(result.fallback).toBe(undefined);
+ expect(result.task).toBe(undefined);
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+
+ it("uses default task if empty string", async () => {
+ const result = parseArgs({ argv: ["--task="] });
+ expect(result.workspace).toBe(undefined);
+ expect(result.fallback).toBe(undefined);
+ expect(result.task).toBe(undefined);
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+
+ it("correctly finds fallback and workspace", async () => {
+ const result = parseArgs({
+ argv: [
+ "this-workspace",
+ "--fallback=HEAD~10",
+ "--task=some-workspace#build",
+ ],
+ });
+ expect(result.workspace).toBe("this-workspace");
+ expect(result.fallback).toBe("HEAD~10");
+ expect(result.task).toBe("some-workspace#build");
+ expect(mockExit.exit).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/packages/turbo-ignore/__tests__/checkCommit.test.ts b/packages/turbo-ignore/__tests__/checkCommit.test.ts
new file mode 100644
index 0000000..e7e4a5f
--- /dev/null
+++ b/packages/turbo-ignore/__tests__/checkCommit.test.ts
@@ -0,0 +1,229 @@
+import child_process from "child_process";
+import { checkCommit } from "../src/checkCommit";
+import { mockEnv } from "@turbo/test-utils";
+
+describe("checkCommit()", () => {
+ describe("on Vercel", () => {
+ mockEnv();
+
+ describe("for all workspaces", () => {
+ it("results in continue when no special commit messages are found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE = "fixing a test";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "continue",
+ scope: "global",
+ reason: "No deploy or skip string found in commit message.",
+ });
+ });
+
+ it("results in conflict when deploy and skip commit messages are found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE =
+ "deploying [vercel deploy] and skipping [vercel skip]";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "conflict",
+ scope: "global",
+ reason:
+ "Conflicting commit messages found: [vercel deploy] and [vercel skip]",
+ });
+ });
+
+ it("results in deploy when deploy commit message is found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE = "deploying [vercel deploy]";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "deploy",
+ scope: "global",
+ reason: "Found commit message: [vercel deploy]",
+ });
+ });
+
+ it("results in skip when skip commit message is found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE = "skip deployment [vercel skip]";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "skip",
+ scope: "global",
+ reason: "Found commit message: [vercel skip]",
+ });
+ });
+ });
+
+ describe("for specific workspaces", () => {
+ it("results in continue when no special commit messages are found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE =
+ "fixing a test in test-workspace";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "continue",
+ scope: "global",
+ reason: "No deploy or skip string found in commit message.",
+ });
+ });
+
+ it("results in conflict when deploy and skip commit messages are found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE =
+ "deploying [vercel deploy test-workspace] and skipping [vercel skip test-workspace]";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "conflict",
+ scope: "workspace",
+ reason:
+ "Conflicting commit messages found: [vercel deploy test-workspace] and [vercel skip test-workspace]",
+ });
+ });
+
+ it("results in deploy when deploy commit message is found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE =
+ "deploying [vercel deploy test-workspace]";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "deploy",
+ scope: "workspace",
+ reason: "Found commit message: [vercel deploy test-workspace]",
+ });
+ });
+
+ it("results in skip when skip commit message is found", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE =
+ "skip deployment [vercel skip test-workspace]";
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "skip",
+ scope: "workspace",
+ reason: "Found commit message: [vercel skip test-workspace]",
+ });
+ });
+ });
+ });
+ describe("Not on Vercel", () => {
+ describe("for all workspaces", () => {
+ it("results in continue when no special commit messages are found", async () => {
+ const commitBody = "fixing a test";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "continue",
+ scope: "global",
+ reason: "No deploy or skip string found in commit message.",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+
+ it("results in conflict when deploy and skip commit messages are found", async () => {
+ const commitBody =
+ "deploying [vercel deploy] and skipping [vercel skip]";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "conflict",
+ scope: "global",
+ reason:
+ "Conflicting commit messages found: [vercel deploy] and [vercel skip]",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+
+ it("results in deploy when deploy commit message is found", async () => {
+ const commitBody = "deploying [vercel deploy]";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "deploy",
+ scope: "global",
+ reason: "Found commit message: [vercel deploy]",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+
+ it("results in skip when skip commit message is found", async () => {
+ const commitBody = "skip deployment [vercel skip]";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "skip",
+ scope: "global",
+ reason: "Found commit message: [vercel skip]",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+ });
+
+ describe("for specific workspaces", () => {
+ it("results in continue when no special commit messages are found", async () => {
+ const commitBody = "fixing a test in test-workspace";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "continue",
+ scope: "global",
+ reason: "No deploy or skip string found in commit message.",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+
+ it("results in conflict when deploy and skip commit messages are found", async () => {
+ const commitBody =
+ "deploying [vercel deploy test-workspace] and skipping [vercel skip test-workspace]";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "conflict",
+ scope: "workspace",
+ reason:
+ "Conflicting commit messages found: [vercel deploy test-workspace] and [vercel skip test-workspace]",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+
+ it("results in deploy when deploy commit message is found", async () => {
+ const commitBody = "deploying [vercel deploy test-workspace]";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "deploy",
+ scope: "workspace",
+ reason: "Found commit message: [vercel deploy test-workspace]",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+
+ it("results in skip when skip commit message is found", async () => {
+ const commitBody = "skip deployment [vercel skip test-workspace]";
+ const mockExecSync = jest
+ .spyOn(child_process, "execSync")
+ .mockImplementation((_) => commitBody);
+
+ expect(checkCommit({ workspace: "test-workspace" })).toEqual({
+ result: "skip",
+ scope: "workspace",
+ reason: "Found commit message: [vercel skip test-workspace]",
+ });
+ expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B");
+ mockExecSync.mockRestore();
+ });
+ });
+ });
+});
diff --git a/packages/turbo-ignore/__tests__/errors.test.ts b/packages/turbo-ignore/__tests__/errors.test.ts
new file mode 100644
index 0000000..18f26bd
--- /dev/null
+++ b/packages/turbo-ignore/__tests__/errors.test.ts
@@ -0,0 +1,46 @@
+import { shouldWarn, NON_FATAL_ERRORS } from "../src/errors";
+
+describe("shouldWarn()", () => {
+ it("it detects errors when packageManager is missing", async () => {
+ const result = shouldWarn({
+ err: `run failed: We did not detect an in-use package manager for your project. Please set the "packageManager" property in your root package.json (https://nodejs.org/api/packages.html#packagemanager) or run \`npx @turbo/codemod add-package-manager\` in the root of your monorepo.`,
+ });
+ expect(result.code).toBe("NO_PACKAGE_MANAGER");
+ expect(result.level).toBe("warn");
+ expect(result.message).toBe(NON_FATAL_ERRORS.NO_PACKAGE_MANAGER.message);
+ });
+
+ it("it detects errors when yarn lockfile is missing", async () => {
+ const result = shouldWarn({
+ err: `* reading yarn.lock: open /test/../yarn.lock: no such file or directory`,
+ });
+ expect(result.code).toBe("MISSING_LOCKFILE");
+ expect(result.level).toBe("warn");
+ expect(result.message).toBe(NON_FATAL_ERRORS.MISSING_LOCKFILE.message);
+ });
+
+ it("it detects errors when pnpm lockfile is missing", async () => {
+ const result = shouldWarn({
+ err: `* reading pnpm-lock.yaml: open /test/../pnpm-lock.yaml: no such file or directory`,
+ });
+ expect(result.code).toBe("MISSING_LOCKFILE");
+ expect(result.level).toBe("warn");
+ expect(result.message).toBe(NON_FATAL_ERRORS.MISSING_LOCKFILE.message);
+ });
+
+ it("it detects errors when npm lockfile is missing", async () => {
+ const result = shouldWarn({
+ err: `* reading package-lock.json: open /test/../package-lock.json: no such file or directory`,
+ });
+ expect(result.code).toBe("MISSING_LOCKFILE");
+ expect(result.level).toBe("warn");
+ expect(result.message).toBe(NON_FATAL_ERRORS.MISSING_LOCKFILE.message);
+ });
+
+ it("it returns unknown errors", async () => {
+ const result = shouldWarn({ err: `something bad happened` });
+ expect(result.code).toBe("UNKNOWN_ERROR");
+ expect(result.level).toBe("error");
+ expect(result.message).toBe(`something bad happened`);
+ });
+});
diff --git a/packages/turbo-ignore/__tests__/getComparison.test.ts b/packages/turbo-ignore/__tests__/getComparison.test.ts
new file mode 100644
index 0000000..b5c74c7
--- /dev/null
+++ b/packages/turbo-ignore/__tests__/getComparison.test.ts
@@ -0,0 +1,61 @@
+import { getComparison } from "../src/getComparison";
+import { spyConsole, mockEnv } from "@turbo/test-utils";
+
+describe("getComparison()", () => {
+ mockEnv();
+ const mockConsole = spyConsole();
+ it("uses headRelative comparison when not running Vercel CI", async () => {
+ expect(getComparison({ workspace: "test-workspace" }))
+ .toMatchInlineSnapshot(`
+ Object {
+ "ref": "HEAD^",
+ "type": "headRelative",
+ }
+ `);
+ });
+
+ it("returns null when running in Vercel CI with no VERCEL_GIT_PREVIOUS_SHA", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ expect(getComparison({ workspace: "test-workspace" })).toBeNull();
+ expect(mockConsole.log).toHaveBeenCalledWith(
+ "≫ ",
+ 'No previous deployments found for "test-workspace" on branch "my-branch".'
+ );
+ });
+
+ it("uses custom fallback when running in Vercel CI with no VERCEL_GIT_PREVIOUS_SHA", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ expect(getComparison({ workspace: "test-workspace", fallback: "HEAD^2" }))
+ .toMatchInlineSnapshot(`
+ Object {
+ "ref": "HEAD^2",
+ "type": "customFallback",
+ }
+ `);
+ expect(mockConsole.log).toHaveBeenNthCalledWith(
+ 1,
+ "≫ ",
+ 'No previous deployments found for "test-workspace" on branch "my-branch".'
+ );
+ expect(mockConsole.log).toHaveBeenNthCalledWith(
+ 2,
+ "≫ ",
+ "Falling back to ref HEAD^2"
+ );
+ });
+
+ it("uses previousDeploy when running in Vercel CI with VERCEL_GIT_PREVIOUS_SHA", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "mygitsha";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ expect(getComparison({ workspace: "test-workspace" }))
+ .toMatchInlineSnapshot(`
+ Object {
+ "ref": "mygitsha",
+ "type": "previousDeploy",
+ }
+ `);
+ });
+});
diff --git a/packages/turbo-ignore/__tests__/getTask.test.ts b/packages/turbo-ignore/__tests__/getTask.test.ts
new file mode 100644
index 0000000..a184893
--- /dev/null
+++ b/packages/turbo-ignore/__tests__/getTask.test.ts
@@ -0,0 +1,27 @@
+import { getTask } from "../src/getTask";
+import { spyConsole, validateLogs } from "@turbo/test-utils";
+
+describe("getWorkspace()", () => {
+ const mockConsole = spyConsole();
+ it("getTask defaults to build", async () => {
+ expect(getTask({})).toEqual("build");
+ validateLogs(
+ ['Using "build" as the task as it was unspecified'],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+ });
+
+ it("getTask returns a quoted task if user-supplied", async () => {
+ expect(
+ getTask({
+ task: "workspace#task",
+ })
+ ).toEqual(`"workspace#task"`);
+ validateLogs(
+ ['Using "workspace#task" as the task from the arguments'],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+ });
+});
diff --git a/packages/turbo-ignore/__tests__/getWorkspace.test.ts b/packages/turbo-ignore/__tests__/getWorkspace.test.ts
new file mode 100644
index 0000000..6d97fe2
--- /dev/null
+++ b/packages/turbo-ignore/__tests__/getWorkspace.test.ts
@@ -0,0 +1,62 @@
+import { getWorkspace } from "../src/getWorkspace";
+import { spyConsole, validateLogs } from "@turbo/test-utils";
+
+describe("getWorkspace()", () => {
+ const mockConsole = spyConsole();
+ it("getWorkspace returns workspace from arg", async () => {
+ expect(
+ getWorkspace({
+ workspace: "test-workspace",
+ })
+ ).toEqual("test-workspace");
+ validateLogs(
+ ['Using "test-workspace" as workspace from arguments'],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+ });
+
+ it("getWorkspace returns workspace from package.json", async () => {
+ expect(
+ getWorkspace({
+ directory: "./__fixtures__/app",
+ })
+ ).toEqual("test-app");
+ expect(mockConsole.log).toHaveBeenCalledWith(
+ "≫ ",
+ 'Inferred "test-app" as workspace from "package.json"'
+ );
+ });
+
+ it("getWorkspace used current directory if not specified", async () => {
+ expect(getWorkspace({})).toEqual("turbo-ignore");
+ expect(mockConsole.log).toHaveBeenCalledWith(
+ "≫ ",
+ 'Inferred "turbo-ignore" as workspace from "package.json"'
+ );
+ });
+
+ it("getWorkspace returns null when no arg is provided and package.json is missing name field", async () => {
+ expect(
+ getWorkspace({
+ directory: "./__fixtures__/invalid-app",
+ })
+ ).toEqual(null);
+ expect(mockConsole.error).toHaveBeenCalledWith(
+ "≫ ",
+ '"__fixtures__/invalid-app/package.json" is missing the "name" field (required).'
+ );
+ });
+
+ it("getWorkspace returns null when no arg is provided and package.json can be found", async () => {
+ expect(
+ getWorkspace({
+ directory: "./__fixtures__/no-app",
+ })
+ ).toEqual(null);
+ expect(mockConsole.error).toHaveBeenCalledWith(
+ "≫ ",
+ '"__fixtures__/no-app/package.json" could not be found. turbo-ignore inferencing failed'
+ );
+ });
+});
diff --git a/packages/turbo-ignore/__tests__/ignore.test.ts b/packages/turbo-ignore/__tests__/ignore.test.ts
new file mode 100644
index 0000000..37908c5
--- /dev/null
+++ b/packages/turbo-ignore/__tests__/ignore.test.ts
@@ -0,0 +1,578 @@
+import child_process, { ChildProcess, ExecException } from "child_process";
+import turboIgnore from "../src/ignore";
+import {
+ spyConsole,
+ spyExit,
+ SpyExit,
+ mockEnv,
+ validateLogs,
+} from "@turbo/test-utils";
+
+function expectBuild(mockExit: SpyExit) {
+ expect(mockExit.exit).toHaveBeenCalledWith(1);
+}
+
+function expectIgnore(mockExit: SpyExit) {
+ expect(mockExit.exit).toHaveBeenCalledWith(0);
+}
+
+describe("turboIgnore()", () => {
+ mockEnv();
+ const mockExit = spyExit();
+ const mockConsole = spyConsole();
+
+ it("throws error and allows build when exec fails", async () => {
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ "error" as unknown as ExecException,
+ "stdout",
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+
+ turboIgnore({
+ args: { workspace: "test-workspace" },
+ });
+
+ expect(mockExec).toHaveBeenCalledWith(
+ "npx turbo run build --filter=test-workspace...[HEAD^] --dry=json",
+ expect.anything(),
+ expect.anything()
+ );
+
+ validateLogs(["UNKNOWN_ERROR: error"], mockConsole.error, {
+ prefix: "≫ ",
+ });
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("throws pretty error and allows build when exec fails", async () => {
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ {
+ message:
+ "run failed: We did not detect an in-use package manager for your project",
+ } as unknown as ExecException,
+ "stdout",
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+
+ turboIgnore({
+ args: { workspace: "test-workspace" },
+ });
+
+ expect(mockExec).toHaveBeenCalledWith(
+ "npx turbo run build --filter=test-workspace...[HEAD^] --dry=json",
+ expect.anything(),
+ expect.anything()
+ );
+
+ validateLogs(
+ [
+ `turbo-ignore could not complete - no package manager detected, please commit a lockfile, or set "packageManager" in your root "package.json"`,
+ ],
+ mockConsole.warn,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("throws pretty error and allows build when can't find previous sha", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "too-far-back";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ {
+ message:
+ " ERROR run failed: failed to resolve packages to run: commit too-far-back does not exist",
+ } as unknown as ExecException,
+ "stdout",
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+
+ turboIgnore({
+ args: { workspace: "test-workspace" },
+ });
+
+ expect(mockExec).toHaveBeenCalledWith(
+ "npx turbo run build --filter=test-workspace...[too-far-back] --dry=json",
+ expect.anything(),
+ expect.anything()
+ );
+
+ validateLogs(
+ [
+ `turbo-ignore could not complete - commit does not exist or is unreachable`,
+ ],
+ mockConsole.warn,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("throws pretty error and allows build when fallback fails", async () => {
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ {
+ message:
+ "ERROR run failed: failed to resolve packages to run: commit HEAD^ does not exist",
+ } as unknown as ExecException,
+ "stdout",
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+
+ turboIgnore({
+ args: { workspace: "test-workspace", fallback: "HEAD^" },
+ });
+
+ expect(mockExec).toHaveBeenCalledWith(
+ "npx turbo run build --filter=test-workspace...[HEAD^] --dry=json",
+ expect.anything(),
+ expect.anything()
+ );
+
+ validateLogs(
+ [
+ `turbo-ignore could not complete - parent commit does not exist or is unreachable`,
+ ],
+ mockConsole.warn,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("skips checks and allows build when no workspace can be found", async () => {
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/no-app",
+ },
+ });
+ validateLogs(
+ [
+ () => [
+ "≫ ",
+ expect.stringContaining(
+ " could not be found. turbo-ignore inferencing failed"
+ ),
+ ],
+ ],
+ mockConsole.error,
+ { prefix: "≫ " }
+ );
+ expectBuild(mockExit);
+ });
+
+ it("skips checks and allows build when a workspace with no name is found", async () => {
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/invalid-app",
+ },
+ });
+ validateLogs(
+ [
+ () => [
+ "≫ ",
+ expect.stringContaining(' is missing the "name" field (required).'),
+ ],
+ ],
+ mockConsole.error,
+ { prefix: "≫ " }
+ );
+ expectBuild(mockExit);
+ });
+
+ it("skips checks and allows build when no monorepo root can be found", async () => {
+ turboIgnore({
+ args: { directory: "/" },
+ });
+ expectBuild(mockExit);
+ expect(mockConsole.error).toHaveBeenLastCalledWith(
+ "≫ ",
+ "Monorepo root not found. turbo-ignore inferencing failed"
+ );
+ });
+
+ it("skips checks and allows build when TURBO_FORCE is set", async () => {
+ process.env.TURBO_FORCE = "true";
+ turboIgnore({
+ args: { workspace: "test-workspace" },
+ });
+ expect(mockConsole.log).toHaveBeenNthCalledWith(
+ 2,
+ "≫ ",
+ "`TURBO_FORCE` detected"
+ );
+ expectBuild(mockExit);
+ });
+
+ it("allows build when no comparison is returned", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ turboIgnore({
+ args: {
+ workspace: "test-app",
+ directory: "__fixtures__/app",
+ },
+ });
+ expect(mockConsole.log).toHaveBeenNthCalledWith(
+ 4,
+ "≫ ",
+ 'No previous deployments found for "test-app" on branch "my-branch".'
+ );
+ expectBuild(mockExit);
+ });
+
+ it("skips build for `previousDeploy` comparison with no changes", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ null,
+ '{"packages":[],"tasks":[]}',
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+ validateLogs(
+ [
+ "Using Turborepo to determine if this project is affected by the commit...\n",
+ 'Inferred "test-app" as workspace from "package.json"',
+ 'Using "build" as the task as it was unspecified',
+ `Found previous deployment ("last-deployed-sha") for \"test-app\" on branch \"my-branch\"`,
+ "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`",
+ "This project and its dependencies are not affected",
+ () => expect.stringContaining("⏭ Ignoring the change"),
+ ],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+
+ expectIgnore(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("allows build for `previousDeploy` comparison with changes", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ null,
+ '{"packages":["test-app"],"tasks":[]}',
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+ turboIgnore({
+ args: {
+ task: "workspace#build",
+ directory: "__fixtures__/app",
+ },
+ });
+ validateLogs(
+ [
+ "Using Turborepo to determine if this project is affected by the commit...\n",
+ 'Inferred "test-app" as workspace from "package.json"',
+ 'Using "workspace#build" as the task from the arguments',
+ 'Found previous deployment ("last-deployed-sha") for "test-app" on branch "my-branch"',
+ 'Analyzing results of `npx turbo run "workspace#build" --filter=test-app...[last-deployed-sha] --dry=json`',
+ 'This commit affects "test-app"',
+ () => expect.stringContaining("✓ Proceeding with deployment"),
+ ],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("allows build for `previousDeploy` comparison with single dependency change", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ null,
+ '{"packages":["test-app", "ui"],"tasks":[]}',
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+ validateLogs(
+ [
+ "Using Turborepo to determine if this project is affected by the commit...\n",
+ 'Inferred "test-app" as workspace from "package.json"',
+ 'Using "build" as the task as it was unspecified',
+ 'Found previous deployment ("last-deployed-sha") for "test-app" on branch "my-branch"',
+ "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`",
+ 'This commit affects "test-app" and 1 dependency (ui)',
+ () => expect.stringContaining("✓ Proceeding with deployment"),
+ ],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("allows build for `previousDeploy` comparison with multiple dependency changes", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ null,
+ '{"packages":["test-app", "ui", "tsconfig"],"tasks":[]}',
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+ validateLogs(
+ [
+ "Using Turborepo to determine if this project is affected by the commit...\n",
+ 'Inferred "test-app" as workspace from "package.json"',
+ 'Using "build" as the task as it was unspecified',
+ 'Found previous deployment ("last-deployed-sha") for "test-app" on branch "my-branch"',
+ "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`",
+ 'This commit affects "test-app" and 2 dependencies (ui, tsconfig)',
+ () => expect.stringContaining("✓ Proceeding with deployment"),
+ ],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("throws error and allows build when json cannot be parsed", async () => {
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(null, "stdout", "stderr") as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+
+ expect(mockExec).toHaveBeenCalledWith(
+ "npx turbo run build --filter=test-app...[HEAD^] --dry=json",
+ expect.anything(),
+ expect.anything()
+ );
+ validateLogs(
+ [
+ "Failed to parse JSON output from `npx turbo run build --filter=test-app...[HEAD^] --dry=json`.",
+ ],
+ mockConsole.error,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("throws error and allows build when stdout is null", async () => {
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ null,
+ null as unknown as string,
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+
+ expect(mockExec).toHaveBeenCalledWith(
+ "npx turbo run build --filter=test-app...[HEAD^] --dry=json",
+ expect.anything(),
+ expect.anything()
+ );
+ validateLogs(
+ [
+ "Failed to parse JSON output from `npx turbo run build --filter=test-app...[HEAD^] --dry=json`.",
+ ],
+ mockConsole.error,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ mockExec.mockRestore();
+ });
+
+ it("skips when commit message contains a skip string", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE = "[vercel skip]";
+
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+
+ validateLogs(
+ [
+ "Using Turborepo to determine if this project is affected by the commit...\n",
+ 'Inferred "test-app" as workspace from "package.json"',
+ 'Using "build" as the task as it was unspecified',
+ "Found commit message: [vercel skip]",
+ () => expect.stringContaining("⏭ Ignoring the change"),
+ ],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+
+ expectIgnore(mockExit);
+ });
+
+ it("deploys when commit message contains a deploy string", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE = "[vercel deploy]";
+
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+
+ validateLogs(
+ [
+ "Using Turborepo to determine if this project is affected by the commit...\n",
+ 'Inferred "test-app" as workspace from "package.json"',
+ 'Using "build" as the task as it was unspecified',
+ "Found commit message: [vercel deploy]",
+ () => expect.stringContaining("✓ Proceeding with deployment"),
+ ],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+
+ expectBuild(mockExit);
+ });
+
+ it("runs full turbo-ignore check when commit message contains a conflicting string", async () => {
+ process.env.VERCEL = "1";
+ process.env.VERCEL_GIT_COMMIT_MESSAGE = "[vercel deploy] [vercel skip]";
+ process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha";
+ process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
+
+ const mockExec = jest
+ .spyOn(child_process, "exec")
+ .mockImplementation((command, options, callback) => {
+ if (callback) {
+ return callback(
+ null,
+ '{"packages":[],"tasks":[]}',
+ "stderr"
+ ) as unknown as ChildProcess;
+ }
+ return {} as unknown as ChildProcess;
+ });
+
+ turboIgnore({
+ args: {
+ directory: "__fixtures__/app",
+ },
+ });
+
+ validateLogs(
+ [
+ "Using Turborepo to determine if this project is affected by the commit...\n",
+ 'Inferred "test-app" as workspace from "package.json"',
+ 'Using "build" as the task as it was unspecified',
+ "Conflicting commit messages found: [vercel deploy] and [vercel skip]",
+ `Found previous deployment ("last-deployed-sha") for \"test-app\" on branch \"my-branch\"`,
+ "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`",
+ "This project and its dependencies are not affected",
+ () => expect.stringContaining("⏭ Ignoring the change"),
+ ],
+ mockConsole.log,
+ { prefix: "≫ " }
+ );
+
+ expectIgnore(mockExit);
+ mockExec.mockRestore();
+ });
+});