aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/turbo-ignore/src
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/src
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'packages/turbo-ignore/src')
-rw-r--r--packages/turbo-ignore/src/args.ts89
-rw-r--r--packages/turbo-ignore/src/checkCommit.ts104
-rw-r--r--packages/turbo-ignore/src/errors.ts43
-rw-r--r--packages/turbo-ignore/src/getComparison.ts39
-rw-r--r--packages/turbo-ignore/src/getTask.ts13
-rw-r--r--packages/turbo-ignore/src/getWorkspace.ts37
-rw-r--r--packages/turbo-ignore/src/ignore.ts125
-rw-r--r--packages/turbo-ignore/src/index.ts6
-rw-r--r--packages/turbo-ignore/src/logger.ts16
-rw-r--r--packages/turbo-ignore/src/types.ts23
10 files changed, 495 insertions, 0 deletions
diff --git a/packages/turbo-ignore/src/args.ts b/packages/turbo-ignore/src/args.ts
new file mode 100644
index 0000000..8d6015c
--- /dev/null
+++ b/packages/turbo-ignore/src/args.ts
@@ -0,0 +1,89 @@
+import pkg from "../package.json";
+import { TurboIgnoreArgs } from "./types";
+import {
+ skipAllCommits,
+ forceAllCommits,
+ skipWorkspaceCommits,
+ forceWorkspaceCommits,
+} from "./checkCommit";
+
+export const help = `
+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
+ --help, -h Show this help message
+ --version, -v Show the version of this script
+
+---
+
+turbo-ignore will also check for special commit messages to indicate if a build should be skipped or not.
+
+Skip turbo-ignore check and automatically ignore:
+${[...skipAllCommits, ...skipWorkspaceCommits({ workspace: "<workspace>" })]
+ .map((msg) => ` - ${msg}`)
+ .join("\n")}
+
+Skip turbo-ignore check and automatically deploy:
+${[...forceAllCommits, ...forceWorkspaceCommits({ workspace: "<workspace>" })]
+ .map((msg) => ` - ${msg}`)
+ .join("\n")}
+`;
+
+// simple args parser because we don't want to pull in a dependency
+// and we don't need many features
+export default function parseArgs({
+ argv,
+}: {
+ argv: Array<string>;
+}): TurboIgnoreArgs {
+ const args: TurboIgnoreArgs = { directory: process.cwd() };
+
+ // find all flags
+ const flags = new Set(
+ argv
+ .filter((args) => args.startsWith("-"))
+ .map((flag) => flag.replace(/-/g, ""))
+ );
+
+ // handle help flag and exit
+ if (flags.has("help") || flags.has("h")) {
+ console.log(help);
+ process.exit(0);
+ }
+ // handle version flag and exit
+ if (flags.has("version") || flags.has("v")) {
+ console.log(pkg.version);
+ process.exit(0);
+ }
+
+ // set workspace (if provided)
+ if (argv.length && !argv[0].startsWith("-")) {
+ args.workspace = argv[0];
+ }
+
+ // set task (if provided)
+ const taskArgSentinel = "--task=";
+ const taskArg = argv.find((arg) => arg.startsWith(taskArgSentinel));
+ if (taskArg && taskArg.length > taskArgSentinel.length) {
+ args.task = taskArg.split("=")[1];
+ }
+
+ // set fallback (if provided)
+ const fallbackSentinel = "--fallback=";
+ const fallbackArg = argv.find((arg) => arg.startsWith(fallbackSentinel));
+ if (fallbackArg && fallbackArg.length > fallbackSentinel.length) {
+ args.fallback = fallbackArg.split("=")[1];
+ }
+
+ return args;
+}
diff --git a/packages/turbo-ignore/src/checkCommit.ts b/packages/turbo-ignore/src/checkCommit.ts
new file mode 100644
index 0000000..af6108e
--- /dev/null
+++ b/packages/turbo-ignore/src/checkCommit.ts
@@ -0,0 +1,104 @@
+import { execSync } from "child_process";
+
+export const skipAllCommits = [
+ `[skip ci]`,
+ `[ci skip]`,
+ `[no ci]`,
+ `[skip vercel]`,
+ `[vercel skip]`,
+];
+
+export const forceAllCommits = [`[vercel deploy]`, `[vercel build]`];
+
+export function skipWorkspaceCommits({ workspace }: { workspace: string }) {
+ return [`[vercel skip ${workspace}]`];
+}
+
+export function forceWorkspaceCommits({ workspace }: { workspace: string }) {
+ return [`[vercel deploy ${workspace}]`, `[vercel build ${workspace}]`];
+}
+
+export function getCommitDetails() {
+ // if we're on Vercel, use the provided commit message
+ if (process.env.VERCEL === "1") {
+ if (process.env.VERCEL_GIT_COMMIT_MESSAGE) {
+ return process.env.VERCEL_GIT_COMMIT_MESSAGE;
+ }
+ }
+ return execSync("git show -s --format=%B").toString();
+}
+
+export function checkCommit({ workspace }: { workspace: string }): {
+ result: "skip" | "deploy" | "continue" | "conflict";
+ scope: "global" | "workspace";
+ reason: string;
+} {
+ const commitMessage = getCommitDetails();
+ const findInCommit = (commit: string) => commitMessage.includes(commit);
+
+ // check workspace specific messages first
+ const forceWorkspaceDeploy = forceWorkspaceCommits({ workspace }).find(
+ findInCommit
+ );
+ const forceWorkspaceSkip = skipWorkspaceCommits({ workspace }).find(
+ findInCommit
+ );
+
+ if (forceWorkspaceDeploy && forceWorkspaceSkip) {
+ return {
+ result: "conflict",
+ scope: "workspace",
+ reason: `Conflicting commit messages found: ${forceWorkspaceDeploy} and ${forceWorkspaceSkip}`,
+ };
+ }
+
+ if (forceWorkspaceDeploy) {
+ return {
+ result: "deploy",
+ scope: "workspace",
+ reason: `Found commit message: ${forceWorkspaceDeploy}`,
+ };
+ }
+
+ if (forceWorkspaceSkip) {
+ return {
+ result: "skip",
+ scope: "workspace",
+ reason: `Found commit message: ${forceWorkspaceSkip}`,
+ };
+ }
+
+ // check global messages last
+ const forceDeploy = forceAllCommits.find(findInCommit);
+ const forceSkip = skipAllCommits.find(findInCommit);
+
+ if (forceDeploy && forceSkip) {
+ return {
+ result: "conflict",
+ scope: "global",
+ reason: `Conflicting commit messages found: ${forceDeploy} and ${forceSkip}`,
+ };
+ }
+
+ if (forceDeploy) {
+ return {
+ result: "deploy",
+ scope: "global",
+ reason: `Found commit message: ${forceDeploy}`,
+ };
+ }
+
+ if (forceSkip) {
+ return {
+ result: "skip",
+ scope: "global",
+ reason: `Found commit message: ${forceSkip}`,
+ };
+ }
+
+ return {
+ result: "continue",
+ scope: "global",
+ reason: `No deploy or skip string found in commit message.`,
+ };
+}
diff --git a/packages/turbo-ignore/src/errors.ts b/packages/turbo-ignore/src/errors.ts
new file mode 100644
index 0000000..f600dfb
--- /dev/null
+++ b/packages/turbo-ignore/src/errors.ts
@@ -0,0 +1,43 @@
+import { NonFatalErrorKey, NonFatalErrors } from "./types";
+
+export const NON_FATAL_ERRORS: NonFatalErrors = {
+ MISSING_LOCKFILE: {
+ regex:
+ /reading (yarn.lock|package-lock.json|pnpm-lock.yaml):.*?no such file or directory/,
+ message: `turbo-ignore could not complete - no lockfile found, please commit one to your repository`,
+ },
+ NO_PACKAGE_MANAGER: {
+ regex:
+ /run failed: We did not detect an in-use package manager for your project/,
+ message: `turbo-ignore could not complete - no package manager detected, please commit a lockfile, or set "packageManager" in your root "package.json"`,
+ },
+ UNREACHABLE_PARENT: {
+ regex: /failed to resolve packages to run: commit HEAD\^ does not exist/,
+ message: `turbo-ignore could not complete - parent commit does not exist or is unreachable`,
+ },
+ UNREACHABLE_COMMIT: {
+ regex: /commit \S+ does not exist/,
+ message: `turbo-ignore could not complete - commit does not exist or is unreachable`,
+ },
+};
+
+export function shouldWarn({ err }: { err: string }): {
+ level: "warn" | "error";
+ message: string;
+ code: NonFatalErrorKey | "UNKNOWN_ERROR";
+} {
+ const knownError = Object.keys(NON_FATAL_ERRORS).find((key) => {
+ const { regex } = NON_FATAL_ERRORS[key as NonFatalErrorKey];
+ return regex.test(err);
+ });
+
+ if (knownError) {
+ return {
+ level: "warn",
+ message: NON_FATAL_ERRORS[knownError as NonFatalErrorKey].message,
+ code: knownError as NonFatalErrorKey,
+ };
+ }
+
+ return { level: "error", message: err, code: "UNKNOWN_ERROR" };
+}
diff --git a/packages/turbo-ignore/src/getComparison.ts b/packages/turbo-ignore/src/getComparison.ts
new file mode 100644
index 0000000..a2ad61a
--- /dev/null
+++ b/packages/turbo-ignore/src/getComparison.ts
@@ -0,0 +1,39 @@
+import { info } from "./logger";
+import { TurboIgnoreArgs } from "./types";
+
+export interface GetComparisonArgs extends TurboIgnoreArgs {
+ // the workspace to check for changes
+ workspace: string;
+ // A ref/head to compare against if no previously deployed SHA is available
+ fallback?: string;
+}
+
+export function getComparison(args: GetComparisonArgs): {
+ ref: string;
+ type: "previousDeploy" | "headRelative" | "customFallback";
+} | null {
+ const { fallback, workspace } = args;
+ if (process.env.VERCEL === "1") {
+ if (process.env.VERCEL_GIT_PREVIOUS_SHA) {
+ // use the commit SHA of the last successful deployment for this project / branch
+ info(
+ `Found previous deployment ("${process.env.VERCEL_GIT_PREVIOUS_SHA}") for "${workspace}" on branch "${process.env.VERCEL_GIT_COMMIT_REF}"`
+ );
+ return {
+ ref: process.env.VERCEL_GIT_PREVIOUS_SHA,
+ type: "previousDeploy",
+ };
+ } else {
+ info(
+ `No previous deployments found for "${workspace}" on branch "${process.env.VERCEL_GIT_COMMIT_REF}".`
+ );
+ if (fallback) {
+ info(`Falling back to ref ${fallback}`);
+ return { ref: fallback, type: "customFallback" };
+ }
+
+ return null;
+ }
+ }
+ return { ref: "HEAD^", type: "headRelative" };
+}
diff --git a/packages/turbo-ignore/src/getTask.ts b/packages/turbo-ignore/src/getTask.ts
new file mode 100644
index 0000000..9e95e35
--- /dev/null
+++ b/packages/turbo-ignore/src/getTask.ts
@@ -0,0 +1,13 @@
+import { info } from "./logger";
+import { TurboIgnoreArgs } from "./types";
+
+export function getTask(args: TurboIgnoreArgs): string | null {
+ if (args.task) {
+ info(`Using "${args.task}" as the task from the arguments`);
+ return `"${args.task}"`;
+ }
+
+ info('Using "build" as the task as it was unspecified');
+
+ return "build";
+}
diff --git a/packages/turbo-ignore/src/getWorkspace.ts b/packages/turbo-ignore/src/getWorkspace.ts
new file mode 100644
index 0000000..e0b3167
--- /dev/null
+++ b/packages/turbo-ignore/src/getWorkspace.ts
@@ -0,0 +1,37 @@
+import fs from "fs";
+import path from "path";
+import { error, info } from "./logger";
+import { TurboIgnoreArgs } from "./types";
+
+export function getWorkspace(args: TurboIgnoreArgs): string | null {
+ const { directory = process.cwd(), workspace } = args;
+
+ // if the workspace is provided via args, use that
+ if (workspace) {
+ info(`Using "${workspace}" as workspace from arguments`);
+ return workspace;
+ }
+
+ // otherwise, try and infer it from a package.json in the current directory
+ const packageJsonPath = path.join(directory, "package.json");
+ try {
+ const raw = fs.readFileSync(packageJsonPath, "utf8");
+ const packageJsonContent: Record<string, string> & { name: string } =
+ JSON.parse(raw);
+
+ if (!packageJsonContent.name) {
+ error(`"${packageJsonPath}" is missing the "name" field (required).`);
+ return null;
+ }
+
+ info(
+ `Inferred "${packageJsonContent.name}" as workspace from "package.json"`
+ );
+ return packageJsonContent.name;
+ } catch (e) {
+ error(
+ `"${packageJsonPath}" could not be found. turbo-ignore inferencing failed`
+ );
+ return null;
+ }
+}
diff --git a/packages/turbo-ignore/src/ignore.ts b/packages/turbo-ignore/src/ignore.ts
new file mode 100644
index 0000000..a6f8f2e
--- /dev/null
+++ b/packages/turbo-ignore/src/ignore.ts
@@ -0,0 +1,125 @@
+import { exec } from "child_process";
+import path from "path";
+import { getTurboRoot } from "@turbo/utils";
+import { getComparison } from "./getComparison";
+import { getTask } from "./getTask";
+import { getWorkspace } from "./getWorkspace";
+import { info, warn, error } from "./logger";
+import { shouldWarn } from "./errors";
+import { TurboIgnoreArgs } from "./types";
+import { checkCommit } from "./checkCommit";
+
+function ignoreBuild() {
+ console.log("⏭ Ignoring the change");
+ return process.exit(0);
+}
+
+function continueBuild() {
+ console.log("✓ Proceeding with deployment");
+ return process.exit(1);
+}
+
+export default function turboIgnore({ args }: { args: TurboIgnoreArgs }) {
+ info(
+ `Using Turborepo to determine if this project is affected by the commit...\n`
+ );
+
+ // set default directory
+ args.directory = args.directory
+ ? path.resolve(args.directory)
+ : process.cwd();
+
+ // check for TURBO_FORCE and bail early if it's set
+ if (process.env.TURBO_FORCE === "true") {
+ info("`TURBO_FORCE` detected");
+ return continueBuild();
+ }
+
+ // find the monorepo root
+ const root = getTurboRoot(args.directory);
+ if (!root) {
+ error("Monorepo root not found. turbo-ignore inferencing failed");
+ return continueBuild();
+ }
+
+ // Find the workspace from the command-line args, or the package.json at the current directory
+ const workspace = getWorkspace(args);
+ if (!workspace) {
+ return continueBuild();
+ }
+
+ // Identify which task to execute from the command-line args
+ let task = getTask(args);
+
+ // check the commit message
+ const parsedCommit = checkCommit({ workspace });
+ if (parsedCommit.result === "skip") {
+ info(parsedCommit.reason);
+ return ignoreBuild();
+ }
+ if (parsedCommit.result === "deploy") {
+ info(parsedCommit.reason);
+ return continueBuild();
+ }
+ if (parsedCommit.result === "conflict") {
+ info(parsedCommit.reason);
+ }
+
+ // Get the start of the comparison (previous deployment when available, or previous commit by default)
+ const comparison = getComparison({ workspace, fallback: args.fallback });
+ if (!comparison) {
+ // This is either the first deploy of the project, or the first deploy for the branch, either way - build it.
+ return continueBuild();
+ }
+
+ // Build, and execute the command
+ const command = `npx turbo run ${task} --filter=${workspace}...[${comparison.ref}] --dry=json`;
+ info(`Analyzing results of \`${command}\``);
+ exec(
+ command,
+ {
+ cwd: root,
+ },
+ (err, stdout) => {
+ if (err) {
+ const { level, code, message } = shouldWarn({ err: err.message });
+ if (level === "warn") {
+ warn(message);
+ } else {
+ error(`${code}: ${err}`);
+ }
+ return continueBuild();
+ }
+
+ try {
+ const parsed = JSON.parse(stdout);
+ if (parsed == null) {
+ error(`Failed to parse JSON output from \`${command}\`.`);
+ return continueBuild();
+ }
+ const { packages } = parsed;
+ if (packages && packages.length > 0) {
+ if (packages.length === 1) {
+ info(`This commit affects "${workspace}"`);
+ } else {
+ // subtract 1 because the first package is the workspace itself
+ info(
+ `This commit affects "${workspace}" and ${packages.length - 1} ${
+ packages.length - 1 === 1 ? "dependency" : "dependencies"
+ } (${packages.slice(1).join(", ")})`
+ );
+ }
+
+ return continueBuild();
+ } else {
+ info(`This project and its dependencies are not affected`);
+ return ignoreBuild();
+ }
+ } catch (e) {
+ error(`Failed to parse JSON output from \`${command}\`.`);
+ error(e);
+ return continueBuild();
+ }
+ }
+ );
+}
diff --git a/packages/turbo-ignore/src/index.ts b/packages/turbo-ignore/src/index.ts
new file mode 100644
index 0000000..0c34d3a
--- /dev/null
+++ b/packages/turbo-ignore/src/index.ts
@@ -0,0 +1,6 @@
+#!/usr/bin/env node
+
+import turboIgnore from "./ignore";
+import parseArgs from "./args";
+
+turboIgnore({ args: parseArgs({ argv: process.argv.slice(2) }) });
diff --git a/packages/turbo-ignore/src/logger.ts b/packages/turbo-ignore/src/logger.ts
new file mode 100644
index 0000000..a7903ac
--- /dev/null
+++ b/packages/turbo-ignore/src/logger.ts
@@ -0,0 +1,16 @@
+// ≫
+const TURBO_IGNORE_PREFIX = "\u226B ";
+
+function info(...args: any[]) {
+ console.log(TURBO_IGNORE_PREFIX, ...args);
+}
+
+function error(...args: any[]) {
+ console.error(TURBO_IGNORE_PREFIX, ...args);
+}
+
+function warn(...args: any[]) {
+ console.warn(TURBO_IGNORE_PREFIX, ...args);
+}
+
+export { info, warn, error };
diff --git a/packages/turbo-ignore/src/types.ts b/packages/turbo-ignore/src/types.ts
new file mode 100644
index 0000000..07fac3f
--- /dev/null
+++ b/packages/turbo-ignore/src/types.ts
@@ -0,0 +1,23 @@
+export type NonFatalErrorKey =
+ | "MISSING_LOCKFILE"
+ | "NO_PACKAGE_MANAGER"
+ | "UNREACHABLE_PARENT"
+ | "UNREACHABLE_COMMIT";
+
+export interface NonFatalError {
+ regex: RegExp;
+ message: string;
+}
+
+export type NonFatalErrors = Record<NonFatalErrorKey, NonFatalError>;
+
+export interface TurboIgnoreArgs {
+ // the working directory to use when looking for a workspace
+ directory?: string;
+ // the workspace to check for changes
+ workspace?: string;
+ // the task to run, if not build
+ task?: string;
+ // A ref/head to compare against if no previously deployed SHA is available
+ fallback?: string;
+}