Skip to content

Commit

Permalink
feat(turbo-ignore): args and refactor (vercel#2671)
Browse files Browse the repository at this point in the history
  • Loading branch information
tknickman authored Nov 16, 2022
1 parent 8c0dc97 commit 3c3da87
Show file tree
Hide file tree
Showing 26 changed files with 921 additions and 212 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ target/
tests_output/
vendor/
/out/
coverage/

*.tsbuildinfo
.eslintcache
Expand Down
87 changes: 86 additions & 1 deletion packages/turbo-ignore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,89 @@ $ npx turbo-ignore

This uses `turbo` to automatically determine if the current app has new changes that need to be deployed.

For more information about Turborepo, [visit turbo.build](https://turbo.build) and follow us on Twitter ([@turborepo](https://twitter.com/turborepo))!
## Usage

Use `npx turbo-ignore --help` to see list of options:

```sh
turbo-ignore

Automatically ignore builds that have no changes

Usage:
$ npx turbo-ignore [<workspace>] [flags...]

If <workspace> is not provided, it will be inferred from the "name"
field of the "package.json" located at the current working directory.

Flags:
--fallback=<ref> On Vercel, if no previously deployed SHA is available to compare against,
fallback to comparing against the provided ref, or use "false" to
disable [default: HEAD^]
--help, -h Show this help message
--version, -v Show the version of this script
```
### Examples
```sh
npx turbo-ignore
```
> Only build if there are changes to the workspace in the current working directory, or any of it's dependencies. On Vercel, compare against the last successful deployment for the current branch. If this does not exist (first deploy of the branch), compare against the previous commit. When not on Vercel, always compare against the previous commit.
---
```sh
npx turbo-ignore docs
```
> Only build if there are changes to the `docs` workspace, or any of it's dependencies. On Vercel, compare against the last successful deployment for the current branch. If this does not exist (first deploy of the branch), compare against the previous commit. When not on Vercel, always compare against the previous commit.
---
```sh
npx turbo-ignore --fallback=false
```
> Only build if there are changes to the workspace in the current working directory, or any of it's dependencies. On Vercel, compare against the last successful deployment for the current branch. If this does not exist, (first deploy of the branch), the build will proceed. When not on Vercel, always compare against the previous commit.
---
```sh
npx turbo-ignore --fallback=HEAD~10
```
> Only build if there are changes to the workspace in the current working directory, or any of it's dependencies. On Vercel, compare against the last successful deployment for the current branch. If this does not exist (first deploy of the branch), compare against the previous 10 commits. When not on Vercel, always compare against the previous commit.
---
```sh
npx turbo-ignore app --fallback=develop
```
> Only build if there are changes to the `app` workspace, or any of it's dependencies. On Vercel, compare against the last successful deployment for the current branch. If this does not exist (first deploy of the branch), compare against the `develop` branch. When not on Vercel, always compare against the previous commit.
## How it Works
`turbo-ignore` determines if a build should continue by analyzing the package dependency graph of the given workspace.
The _given workspace_ is determined by reading the "name" field in the "package.json" file located at the current working directory, or by passing in a workspace name as the first argument to `turbo-ignore`.
Next, it uses `turbo run build --dry` to determine if the given workspace, _or any dependencies of the workspace_, have changed since the previous commit.
**NOTE:** `turbo` determines dependencies from reading the dependency graph of the given workspace. This means a workspace **must** be listed as a `dependency` (or `devDependency`) in the given workspaces `package.json` for `turbo` to recognize it.
When deploying on [Vercel](https://vercel.com), `turbo-ignore` can make a more accurate decision by comparing between the current commit, and the last successfully deployed commit for the current branch.
**NOTE:** By default on Vercel, if the branch has not been deployed, `turbo-ignore` will fall back to comparing against the previous commit. To always deploy the first commit to a new branch, this fallback behavior can be disabled with `--filter-fallback=false`.
## Releasing
```sh
pnpm release
```
---
For more information about Turborepo, visit [turbo.build](https://turbo.build) and follow us on Twitter ([@turborepo](https://twitter.com/turborepo))!
11 changes: 11 additions & 0 deletions packages/turbo-ignore/__fixtures__/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "test-app",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "vercel"
}
10 changes: 10 additions & 0 deletions packages/turbo-ignore/__fixtures__/invalid-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "vercel"
}
Empty file.
68 changes: 68 additions & 0 deletions packages/turbo-ignore/__tests__/args.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import parseArgs, { help } from "../src/args";
import pkg from "../package.json";
import { spyConsole, spyExit } from "./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);
});

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(mockExit.exit).toHaveBeenCalledTimes(0);
});

it("correctly finds fallback", async () => {
const result = parseArgs({ argv: ["--fallback=false"] });
expect(result.workspace).toBe(undefined);
expect(result.fallback).toBe("false");
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(mockExit.exit).toHaveBeenCalledTimes(0);
});

it("correctly finds fallback and workspace", async () => {
const result = parseArgs({
argv: ["this-workspace", "--fallback=false"],
});
expect(result.workspace).toBe("this-workspace");
expect(result.fallback).toBe("false");
expect(mockExit.exit).toHaveBeenCalledTimes(0);
});
});
82 changes: 82 additions & 0 deletions packages/turbo-ignore/__tests__/getComparison.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { getComparison } from "../src/getComparison";
import { spyConsole, validateLogs } from "./test-utils";

describe("getComparison()", () => {
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 and fallback disabled", async () => {
process.env.VERCEL = "1";
process.env.VERCEL_GIT_COMMIT_REF = "my-branch";
expect(
getComparison({ workspace: "test-workspace", fallback: "false" })
).toBeNull();
expect(mockConsole.log).toHaveBeenCalledWith(
"≫ ",
'no previous deployments found for "test-workspace" on "my-branch".'
);
});

it("uses default 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" }))
.toMatchInlineSnapshot(`
Object {
"ref": "HEAD^",
"type": "customFallback",
}
`);

validateLogs(
[
'no previous deployments found for "test-workspace" on "my-branch".',
"falling back to HEAD^",
],
mockConsole.log
);
});

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 "my-branch".'
);
expect(mockConsole.log).toHaveBeenNthCalledWith(
2,
"≫ ",
"falling back to 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",
}
`);
});
});
61 changes: 61 additions & 0 deletions packages/turbo-ignore/__tests__/getWorkspace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { getWorkspace } from "../src/getWorkspace";
import { spyConsole, validateLogs } from "./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
);
});

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'
);
});
});
Loading

0 comments on commit 3c3da87

Please sign in to comment.