Repository: atlassian/changesets
Pull request #371 (2020-05-19 01:35:42)

Add support for `ignored` config
Implements https://github.com/atlassian/changesets/issues/242

@Andarist Can you please take a look? Need to add tests, but want to confirm that this approach looks reasonable to folks first.

atlassian-cla-bot[bot]: Thank you for your submission! Like many open source projects, we ask that you sign our <a href=https://opensource.atlassian.com/cla>CLA (Contributor License Agreement)</a> before we can accept your contribution.</br>
      <strong>The following users still need to sign our CLA:</strong></br>❌Feiyang1
<p><sub>Already signed the CLA? To re-check, try refreshing the page.</sub></p>
changeset-bot[bot]: ###  💥  No Changeset

Latest commit: 5543707846a9d84e9a6fe497780309d667f57321

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

**If these changes should be published to npm, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/Feiyang1/changesets/new/fei-ignore?filename=.changeset/funny-walls-hunt.md&value=---%0A%22%40changesets%2Fapply-release-plan%22%3A%20patch%0A%22%40changesets%2Fassemble-release-plan%22%3A%20patch%0A%22%40changesets%2Fconfig%22%3A%20patch%0A%22%40changesets%2Ftypes%22%3A%20patch%0A---%0A%0AAdd%20support%20for%20%60ignored%60%20config%0A)


Andarist: Thanks for doing this! I will get to reviewing this soon.
Feiyang1: Added changeset
Feiyang1: Addressed all comments. I think the PR is ready to merge. Thanks @Andarist for your thorough review!
Andarist: @Feiyang1 could you resolve conflicts?

@Noviny @mitchellhamilton could you do a second round of reviews? It would be great to land this soon!
Feiyang1: @mitchellhamilton gentle ping

Patch:
diff --git a/.changeset/proud-tips-allow.md b/.changeset/proud-tips-allow.md
new file mode 100644
index 0000000..ebe7c78
--- /dev/null
+++ b/.changeset/proud-tips-allow.md
@@ -0,0 +1,6 @@
+---
+"@changesets/config": minor
+"@changesets/types": minor
+---
+
+Add `ignore` config option to configure ignored packages. The versions of ignored packages will not be bumped during a release, but their dependencies will still be bumped normally. 
diff --git a/.changeset/rich-melons-press.md b/.changeset/rich-melons-press.md
new file mode 100644
index 0000000..04262f2
--- /dev/null
+++ b/.changeset/rich-melons-press.md
@@ -0,0 +1,20 @@
+---
+"@changesets/apply-release-plan": minor
+"@changesets/assemble-release-plan": minor
+"@changesets/cli": minor
+---
+
+Added support for ignoring packages in the `version` command. The version of ignored packages will not be bumped, but their dependencies will still be bumped normally. This is useful when you have private packages, e.g. packages under development. It allows you to make releases for the public packages without changing the version of your private packages. To use the feature, you can define the `ignore` array in the config file with the name of the packages:
+
+```
+{
+  ...
+  "ignore": ["pkg-a", "pkg-b"]
+  ...
+}
+```
+
+or you can pass the package names to the `--ignore` flag when using cli:
+```
+yarn changeset version --ignore pkg-a --ignore --pkg-b
+```
diff --git a/__fixtures__/simple-project-with-ignore-config/.changeset/README.md b/__fixtures__/simple-project-with-ignore-config/.changeset/README.md
new file mode 100644
index 0000000..fd188f5
--- /dev/null
+++ b/__fixtures__/simple-project-with-ignore-config/.changeset/README.md
@@ -0,0 +1,3 @@
+# We just want a file in here so git collects it
+
+For this we have deliberately not included a config file, as we want to test the defaults
\ No newline at end of file
diff --git a/__fixtures__/simple-project-with-ignore-config/.changeset/config.json b/__fixtures__/simple-project-with-ignore-config/.changeset/config.json
new file mode 100644
index 0000000..4004d00
--- /dev/null
+++ b/__fixtures__/simple-project-with-ignore-config/.changeset/config.json
@@ -0,0 +1,3 @@
+{
+    "ignore": ["pkg-a"]
+}
diff --git a/__fixtures__/simple-project-with-ignore-config/package.json b/__fixtures__/simple-project-with-ignore-config/package.json
new file mode 100644
index 0000000..2e742d8
--- /dev/null
+++ b/__fixtures__/simple-project-with-ignore-config/package.json
@@ -0,0 +1,11 @@
+{
+  "private": true,
+  "name": "simple-project",
+  "description": "three projects, each depending on one other",
+  "version": "1.0.0",
+  "bolt": {
+    "workspaces": [
+      "packages/*"
+    ]
+  }
+}
diff --git a/__fixtures__/simple-project-with-ignore-config/packages/pkg-a/package.json b/__fixtures__/simple-project-with-ignore-config/packages/pkg-a/package.json
new file mode 100644
index 0000000..205afc4
--- /dev/null
+++ b/__fixtures__/simple-project-with-ignore-config/packages/pkg-a/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "pkg-a",
+  "version": "1.0.0",
+  "dependencies": {
+    "pkg-b": "1.0.0"
+  }
+}
diff --git a/__fixtures__/simple-project-with-ignore-config/packages/pkg-b/package.json b/__fixtures__/simple-project-with-ignore-config/packages/pkg-b/package.json
new file mode 100644
index 0000000..be4760e
--- /dev/null
+++ b/__fixtures__/simple-project-with-ignore-config/packages/pkg-b/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "pkg-b",
+  "version": "1.0.0"
+}
diff --git a/packages/apply-release-plan/src/index.ts b/packages/apply-release-plan/src/index.ts
index ade7a2f..15c81e0 100644
--- a/packages/apply-release-plan/src/index.ts
+++ b/packages/apply-release-plan/src/index.ts
@@ -135,8 +135,19 @@ export default async function applyReleasePlan(
         let changesetPath = path.resolve(changesetFolder, `${changeset.id}.md`);
         let changesetFolderPath = path.resolve(changesetFolder, changeset.id);
         if (await fs.pathExists(changesetPath)) {
-          touchedFiles.push(changesetPath);
-          await fs.remove(changesetPath);
+          // DO NOT remove changeset for ignored packages
+          // Mixed changeset that contains both ignored packages and not ignored packages are disallowed
+          // At this point, we know there is no such changeset, because otherwise the program would've already failed,
+          // so we just check if any ignored package exists in this changeset, and only remove it if none exists
+          // Ignored list is added in v2, so we don't need to do it for v1 changesets
+          if (
+            !changeset.releases.find(release =>
+              config.ignore.includes(release.name)
+            )
+          ) {
+            touchedFiles.push(changesetPath);
+            await fs.remove(changesetPath);
+          }
           // TO REMOVE LOGIC - this works to remove v1 changesets. We should be removed in the future
         } else if (await fs.pathExists(changesetFolderPath)) {
           touchedFiles.push(changesetFolderPath);
diff --git a/packages/assemble-release-plan/src/determine-dependents.ts b/packages/assemble-release-plan/src/determine-dependents.ts
index ce6bd6c..6a8f69f 100644
--- a/packages/assemble-release-plan/src/determine-dependents.ts
+++ b/packages/assemble-release-plan/src/determine-dependents.ts
@@ -26,12 +26,14 @@ export default function getDependents({
   packagesByName,
   dependencyGraph,
   preInfo,
+  ignoredPackages,
   onlyUpdatePeerDependentsWhenOutOfRange
 }: {
   releases: Map<string, InternalRelease>;
   packagesByName: Map<string, Package>;
   dependencyGraph: Map<string, string[]>;
   preInfo: PreInfo | undefined;
+  ignoredPackages: Readonly<string[]>;
   onlyUpdatePeerDependentsWhenOutOfRange: boolean;
 }): boolean {
   let updated = false;
@@ -62,8 +64,12 @@ export default function getDependents({
           nextRelease.name
         );
 
-        // Firstly we check if it is a peerDependency because if it is, our dependent bump type needs to be major.
-        if (
+        // If the dependent is an ignored package, we want to bump its dependencies without a release, so setting type to "none"
+        if (ignoredPackages.includes(dependent)) {
+          type = "none";
+        }
+        // we check if it is a peerDependency because if it is, our dependent bump type needs to be major.
+        else if (
           depTypes.includes("peerDependencies") &&
           nextRelease.type !== "patch" &&
           (!onlyUpdatePeerDependentsWhenOutOfRange ||
diff --git a/packages/assemble-release-plan/src/flatten-releases.ts b/packages/assemble-release-plan/src/flatten-releases.ts
index b323fc1..e0d799a 100644
--- a/packages/assemble-release-plan/src/flatten-releases.ts
+++ b/packages/assemble-release-plan/src/flatten-releases.ts
@@ -7,42 +7,47 @@ import { InternalRelease } from "./types";
 
 export default function flattenReleases(
   changesets: NewChangeset[],
-  packagesByName: Map<string, Package>
+  packagesByName: Map<string, Package>,
+  ignoredPackages: Readonly<string[]>
 ): Map<string, InternalRelease> {
   let releases: Map<string, InternalRelease> = new Map();
 
   changesets.forEach(changeset => {
-    changeset.releases.forEach(({ name, type }) => {
-      let release = releases.get(name);
-      let pkg = packagesByName.get(name);
-      if (!pkg) {
-        throw new Error(`Could not find package information for ${name}`);
-      }
-      if (!release) {
-        release = {
-          name,
-          type,
-          oldVersion: pkg.packageJson.version,
-          changesets: [changeset.id]
-        };
-      } else {
-        // If the type was already major, we never need to update it
-        if (release.type === "minor" && type === "major") {
-          release.type = type;
-        } else if (
-          release.type === "patch" &&
-          (type === "major" || type === "minor")
-        ) {
-          release.type = type;
+    changeset.releases
+      // Filter out ignored packages because they should not trigger a release
+      // If their dependencies need updates, they will be added to releases by `determineDependents()` with release type `none`
+      .filter(({ name }) => !ignoredPackages.includes(name))
+      .forEach(({ name, type }) => {
+        let release = releases.get(name);
+        let pkg = packagesByName.get(name);
+        if (!pkg) {
+          throw new Error(`Could not find package information for ${name}`);
+        }
+        if (!release) {
+          release = {
+            name,
+            type,
+            oldVersion: pkg.packageJson.version,
+            changesets: [changeset.id]
+          };
+        } else {
+          // If the type was already major, we never need to update it
+          if (release.type === "minor" && type === "major") {
+            release.type = type;
+          } else if (
+            release.type === "patch" &&
+            (type === "major" || type === "minor")
+          ) {
+            release.type = type;
+          }
+          // Check whether the bumpType will change
+          // If the bumpType has changed recalc newVersion
+          // push new changeset to releases
+          release.changesets.push(changeset.id);
         }
-        // Check whether the bumpType will change
-        // If the bumpType has changed recalc newVersion
-        // push new changeset to releases
-        release.changesets.push(changeset.id);
-      }
 
-      releases.set(name, release);
-    });
+        releases.set(name, release);
+      });
   });
 
   return releases;
diff --git a/packages/assemble-release-plan/src/increment.ts b/packages/assemble-release-plan/src/increment.ts
index 1f54b90..befd189 100644
--- a/packages/assemble-release-plan/src/increment.ts
+++ b/packages/assemble-release-plan/src/increment.ts
@@ -6,10 +6,11 @@ export function incrementVersion(
   release: InternalRelease,
   preInfo: PreInfo | undefined
 ) {
-  let version =
-    release.type === "none"
-      ? release.oldVersion
-      : semver.inc(release.oldVersion, release.type)!;
+  if (release.type === "none") {
+    return release.oldVersion;
+  }
+
+  let version = semver.inc(release.oldVersion, release.type)!;
   if (preInfo !== undefined && preInfo.state.mode !== "exit") {
     let preVersion = preInfo.preVersions.get(release.name);
     if (preVersion === undefined) {
diff --git a/packages/assemble-release-plan/src/index.ts b/packages/assemble-release-plan/src/index.ts
index dd42d74..9146269 100644
--- a/packages/assemble-release-plan/src/index.ts
+++ b/packages/assemble-release-plan/src/index.ts
@@ -74,6 +74,8 @@ function assembleReleasePlan(
   preState: PreState | undefined,
   snapshot?: string | boolean
 ): ReleasePlan {
+  validateChangesets(changesets, config.ignore);
+
   let updatedPreState: PreState | undefined =
     preState === undefined
       ? undefined
@@ -147,7 +149,7 @@ function assembleReleasePlan(
   // releases is, at this point a list of all packages we are going to releases,
   // flattened down to one release per package, having a reference back to their
   // changesets, and with a calculated new versions
-  let releases = flattenReleases(changesets, packagesByName);
+  let releases = flattenReleases(changesets, packagesByName, config.ignore);
 
   if (updatedPreState !== undefined) {
     if (updatedPreState.mode === "exit") {
@@ -174,7 +176,8 @@ function assembleReleasePlan(
       // because if they're not being released, the version will already have been bumped with the highest bump type
       let releasesFromUnfilteredChangesets = flattenReleases(
         unfilteredChangesets,
-        packagesByName
+        packagesByName,
+        config.ignore
       );
 
       releases.forEach((value, key) => {
@@ -217,6 +220,7 @@ function assembleReleasePlan(
       packagesByName,
       dependencyGraph,
       preInfo,
+      ignoredPackages: config.ignore,
       onlyUpdatePeerDependentsWhenOutOfRange:
         config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH
           .onlyUpdatePeerDependentsWhenOutOfRange
@@ -247,4 +251,35 @@ function assembleReleasePlan(
   };
 }
 
+// Changesets that contains both ignored and not ignored packages are not allowed
+function validateChangesets(
+  changesets: NewChangeset[],
+  ignored: Readonly<string[]>
+): void {
+  for (const changeset of changesets) {
+    // Using the following 2 arrays to decide whether a changeset
+    // contains both ignored and not ignored packages
+    const ignoredPackages = [];
+    const notIgnoredPackages = [];
+    for (const release of changeset.releases) {
+      if (
+        ignored.find(ignoredPackageName => ignoredPackageName === release.name)
+      ) {
+        ignoredPackages.push(release.name);
+      } else {
+        notIgnoredPackages.push(release.name);
+      }
+    }
+
+    if (ignoredPackages.length > 0 && notIgnoredPackages.length > 0) {
+      throw new Error(
+        `Found mixed changeset ${changeset.id}\n` +
+          `Found ignored packages: ${ignoredPackages.join(" ")}\n` +
+          `Found not ignored packages: ${notIgnoredPackages.join(" ")}\n` +
+          "Mixed changesets that contain both ignored and not ignored packages are not allowed"
+      );
+    }
+  }
+}
+
 export default assembleReleasePlan;
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 8f8c393..9c841d0 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -28,6 +28,7 @@
     "@changesets/assemble-release-plan": "^2.1.0",
     "@changesets/config": "^1.1.0",
     "@changesets/errors": "^0.1.4",
+    "@changesets/get-dependents-graph": "^1.1.3",
     "@changesets/get-release-plan": "^1.0.4",
     "@changesets/git": "^1.0.5",
     "@changesets/logger": "^0.0.5",
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 7f548bb..f30bf82 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -1,20 +1,8 @@
 import meow from "meow";
-import { read } from "@changesets/config";
 import { ExitError, InternalError } from "@changesets/errors";
 import { error } from "@changesets/logger";
-import { Config } from "@changesets/types";
-import fs from "fs-extra";
-import path from "path";
-import { getPackages } from "@manypkg/get-packages";
-
-import init from "./commands/init";
-import add from "./commands/add";
-import version from "./commands/version";
-import publish from "./commands/publish";
-import status from "./commands/status";
-import pre from "./commands/pre";
-import { CliOptions } from "./types";
 import { format } from "util";
+import { run } from "./run";
 
 const { input, flags } = meow(
   `
@@ -23,7 +11,7 @@ const { input, flags } = meow(
   Commands
     init
     add [--empty]
-    version
+    version [--ignore]
     publish [--otp=code]
     status [--since-master --verbose --output=JSON_FILE.json]
     prerelease <tag>
@@ -52,6 +40,11 @@ const { input, flags } = meow(
         type: "string",
         default: undefined
       },
+      ignore: {
+        type: "string",
+        default: undefined,
+        isMultiple: true
+      },
       tag: {
         type: "string"
       }
@@ -61,146 +54,7 @@ const { input, flags } = meow(
 
 const cwd = process.cwd();
 
-(async () => {
-  if (input[0] === "init") {
-    await init(cwd);
-    return;
-  }
-
-  if (!fs.existsSync(path.resolve(cwd, ".changeset"))) {
-    error("There is no .changeset folder. ");
-    error(
-      "If this is the first time `changesets` have been used in this project, run `yarn changeset init` to get set up."
-    );
-    error(
-      "If you expected there to be changesets, you should check git history for when the folder was removed to ensure you do not lose any configuration."
-    );
-    throw new ExitError(1);
-  }
-
-  const packages = await getPackages(cwd);
-
-  let config: Config;
-  try {
-    config = await read(cwd, packages);
-  } catch (e) {
-    let oldConfigExists = await fs.pathExists(
-      path.resolve(cwd, ".changeset/config.js")
-    );
-    if (oldConfigExists) {
-      error(
-        "It looks like you're using the version 1 `.changeset/config.js` file"
-      );
-      error("You'll need to convert it to a `.changeset/config.json` file");
-      error(
-        "The format of the config object has significantly changed in v2 as well"
-      );
-      error(
-        " - we thoroughly recommend looking at the changelog for this package for what has changed"
-      );
-      throw new ExitError(1);
-    } else {
-      throw e;
-    }
-  }
-
-  if (input.length < 1) {
-    const { empty }: CliOptions = flags;
-    // @ts-ignore if this is undefined, we have already exited
-    await add(cwd, { empty }, config);
-  } else if (input[0] !== "pre" && input.length > 1) {
-    error(
-      "Too many arguments passed to changesets - we only accept the command name as an argument"
-    );
-  } else {
-    const {
-      sinceMaster,
-      since,
-      verbose,
-      output,
-      otp,
-      empty,
-      snapshot,
-      tag
-    }: CliOptions = flags;
-    const deadFlags = ["updateChangelog", "isPublic", "skipCI", "commit"];
-
-    deadFlags.forEach(flag => {
-      if (flags[flag]) {
-        error(
-          `the flag ${flag} has been removed from changesets for version 2`
-        );
-        error(`Please encode the desired value into your config`);
-        error(`See our changelog for more details`);
-        throw new ExitError(1);
-      }
-    });
-
-    // Command line options need to be undefined, otherwise their
-    // default value overrides the user's provided config in their
-    // config file. For this reason, we only assign them to this
-    // object as and when they exist.
-
-    switch (input[0]) {
-      case "add": {
-        // @ts-ignore if this is undefined, we have already exited
-        await add(cwd, { empty }, config);
-        return;
-      }
-      case "version": {
-        await version(cwd, { snapshot }, config);
-        return;
-      }
-      case "publish": {
-        await publish(cwd, { otp, tag }, config);
-        return;
-      }
-      case "status": {
-        await status(cwd, { sinceMaster, since, verbose, output }, config);
-        return;
-      }
-      case "pre": {
-        let command = input[1];
-        if (command !== "enter" && command !== "exit") {
-          error(
-            "`enter`, `exit` or `snapshot` must be passed after prerelease"
-          );
-          throw new ExitError(1);
-        }
-        let tag = input[2];
-        if (command === "enter" && typeof tag !== "string") {
-          error(`A tag must be passed when using prerelese enter`);
-          throw new ExitError(1);
-        }
-        // @ts-ignore
-        await pre(cwd, { command, tag });
-        return;
-      }
-      case "bump": {
-        error(
-          'In version 2 of changesets, "bump" has been renamed to "version" - see our changelog for an explanation'
-        );
-        error(
-          "To fix this, use `changeset version` instead, and update any scripts that use changesets"
-        );
-        throw new ExitError(1);
-      }
-      case "release": {
-        error(
-          'In version 2 of changesets, "release" has been renamed to "publish" - see our changelog for an explanation'
-        );
-        error(
-          "To fix this, use `changeset publish` instead, and update any scripts that use changesets"
-        );
-        throw new ExitError(1);
-      }
-      default: {
-        error(`Invalid command ${input[0]} was provided`);
-        throw new ExitError(1);
-      }
-    }
-  }
-})().catch(err => {
+run(input, flags, cwd).catch(err => {
   if (err instanceof InternalError) {
     error(
       "The following error is an internal unexpected error, these should never happen."
diff --git a/packages/cli/src/run.ts b/packages/cli/src/run.ts
new file mode 100644
index 0000000..db44886
--- /dev/null
+++ b/packages/cli/src/run.ts
@@ -0,0 +1,212 @@
+import { Config } from "@changesets/types";
+import fs from "fs-extra";
+import path from "path";
+import { getPackages } from "@manypkg/get-packages";
+import { getDependentsGraph } from "@changesets/get-dependents-graph";
+import { error } from "@changesets/logger";
+import { read } from "@changesets/config";
+import { ExitError } from "@changesets/errors";
+
+import init from "./commands/init";
+import add from "./commands/add";
+import version from "./commands/version";
+import publish from "./commands/publish";
+import status from "./commands/status";
+import pre from "./commands/pre";
+import { CliOptions } from "./types";
+
+export async function run(
+  input: string[],
+  flags: { [name: string]: any },
+  cwd: string
+) {
+  if (input[0] === "init") {
+    await init(cwd);
+    return;
+  }
+
+  if (!fs.existsSync(path.resolve(cwd, ".changeset"))) {
+    error("There is no .changeset folder. ");
+    error(
+      "If this is the first time `changesets` have been used in this project, run `yarn changeset init` to get set up."
+    );
+    error(
+      "If you expected there to be changesets, you should check git history for when the folder was removed to ensure you do not lose any configuration."
+    );
+    throw new ExitError(1);
+  }
+
+  const packages = await getPackages(cwd);
+
+  let config: Config;
+  try {
+    config = await read(cwd, packages);
+  } catch (e) {
+    let oldConfigExists = await fs.pathExists(
+      path.resolve(cwd, ".changeset/config.js")
+    );
+    if (oldConfigExists) {
+      error(
+        "It looks like you're using the version 1 `.changeset/config.js` file"
+      );
+      error("You'll need to convert it to a `.changeset/config.json` file");
+      error(
+        "The format of the config object has significantly changed in v2 as well"
+      );
+      error(
+        " - we thoroughly recommend looking at the changelog for this package for what has changed"
+      );
+      throw new ExitError(1);
+    } else {
+      throw e;
+    }
+  }
+
+  if (input.length < 1) {
+    const { empty }: CliOptions = flags;
+    // @ts-ignore if this is undefined, we have already exited
+    await add(cwd, { empty }, config);
+  } else if (input[0] !== "pre" && input.length > 1) {
+    error(
+      "Too many arguments passed to changesets - we only accept the command name as an argument"
+    );
+  } else {
+    const {
+      sinceMaster,
+      since,
+      verbose,
+      output,
+      otp,
+      empty,
+      ignore,
+      snapshot,
+      tag
+    }: CliOptions = flags;
+    const deadFlags = ["updateChangelog", "isPublic", "skipCI", "commit"];
+
+    deadFlags.forEach(flag => {
+      if (flags[flag]) {
+        error(
+          `the flag ${flag} has been removed from changesets for version 2`
+        );
+        error(`Please encode the desired value into your config`);
+        error(`See our changelog for more details`);
+        throw new ExitError(1);
+      }
+    });
+
+    // Command line options need to be undefined, otherwise their
+    // default value overrides the user's provided config in their
+    // config file. For this reason, we only assign them to this
+    // object as and when they exist.
+
+    switch (input[0]) {
+      case "add": {
+        // @ts-ignore if this is undefined, we have already exited
+        await add(cwd, { empty }, config);
+        return;
+      }
+      case "version": {
+        let ignoreArrayFromCmd: undefined | string[];
+        if (typeof ignore === "string") {
+          ignoreArrayFromCmd = [ignore];
+        } else {
+          // undefined or an array
+          ignoreArrayFromCmd = ignore;
+        }
+
+        // Validate that items in ignoreArrayFromCmd are valid project names
+        let pkgNames = new Set(
+          packages.packages.map(({ packageJson }) => packageJson.name)
+        );
+
+        const messages = [];
+        for (const pkgName of ignoreArrayFromCmd || []) {
+          if (!pkgNames.has(pkgName)) {
+            messages.push(
+              `The package "${pkgName}" is passed to the \`--ignore\` option but it is not found in the project. You may have misspelled the package name.`
+            );
+          }
+        }
+
+        if (config.ignore.length > 0 && ignoreArrayFromCmd) {
+          messages.push(
+            `It looks like you are trying to use the \`--ignore\` option while ignore is defined in the config file. This is currently not allowed, you can only use one of them at a time.`
+          );
+        } else if (ignoreArrayFromCmd) {
+          // use the ignore flags from cli
+          config.ignore = ignoreArrayFromCmd;
+        }
+
+        // Validate that all dependents of ignored packages are listed in the ignore list
+        const dependentsGraph = getDependentsGraph(packages);
+        for (const ignoredPackage of config.ignore) {
+          const dependents = dependentsGraph.get(ignoredPackage) || [];
+          for (const dependent of dependents) {
+            if (!config.ignore.includes(dependent)) {
+              messages.push(
+                `The package "${dependent}" depends on the ignored package "${ignoredPackage}", but "${dependent}" is not being ignored. Please pass "${dependent}" to the \`--ignore\` flag.`
+              );
+            }
+          }
+        }
+
+        if (messages.length > 0) {
+          error(messages.join("\n"));
+
+          throw new ExitError(1);
+        }
+
+        await version(cwd, { snapshot }, config);
+        return;
+      }
+      case "publish": {
+        await publish(cwd, { otp, tag }, config);
+        return;
+      }
+      case "status": {
+        await status(cwd, { sinceMaster, since, verbose, output }, config);
+        return;
+      }
+      case "pre": {
+        let command = input[1];
+        if (command !== "enter" && command !== "exit") {
+          error(
+            "`enter`, `exit` or `snapshot` must be passed after prerelease"
+          );
+          throw new ExitError(1);
+        }
+        let tag = input[2];
+        if (command === "enter" && typeof tag !== "string") {
+          error(`A tag must be passed when using prerelese enter`);
+          throw new ExitError(1);
+        }
+        // @ts-ignore
+        await pre(cwd, { command, tag });
+        return;
+      }
+      case "bump": {
+        error(
+          'In version 2 of changesets, "bump" has been renamed to "version" - see our changelog for an explanation'
+        );
+        error(
+          "To fix this, use `changeset version` instead, and update any scripts that use changesets"
+        );
+        throw new ExitError(1);
+      }
+      case "release": {
+        error(
+          'In version 2 of changesets, "release" has been renamed to "publish" - see our changelog for an explanation'
+        );
+        error(
+          "To fix this, use `changeset publish` instead, and update any scripts that use changesets"
+        );
+        throw new ExitError(1);
+      }
+      default: {
+        error(`Invalid command ${input[0]} was provided`);
+        throw new ExitError(1);
+      }
+    }
+  }
+}
diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts
index ec50d66..7a889db 100644
--- a/packages/cli/src/types.ts
+++ b/packages/cli/src/types.ts
@@ -10,6 +10,7 @@ export type CliOptions = {
   otp?: string;
   empty?: boolean;
   since?: string;
+  ignore?: string | string[];
   snapshot?: string | boolean;
   tag?: string;
 };
diff --git a/packages/config/package.json b/packages/config/package.json
index a3814dd..aa90d82 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -12,6 +12,7 @@
   ],
   "dependencies": {
     "@changesets/errors": "^0.1.4",
+    "@changesets/get-dependents-graph": "^1.1.3",
     "@changesets/logger": "^0.0.5",
     "@changesets/types": "^3.0.0",
     "@manypkg/get-packages": "^1.0.1",
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
index 717bec4..7430e37 100644
--- a/packages/config/src/index.ts
+++ b/packages/config/src/index.ts
@@ -5,6 +5,7 @@ import { warn } from "@changesets/logger";
 import { Packages } from "@manypkg/get-packages";
 import { Config, WrittenConfig } from "@changesets/types";
 import packageJson from "../package.json";
+import { getDependentsGraph } from "@changesets/get-dependents-graph";
 
 export let defaultWrittenConfig = {
   $schema: `https://unpkg.com/@changesets/config@${packageJson.version}/schema.json`,
@@ -14,6 +15,7 @@ export let defaultWrittenConfig = {
   access: "restricted",
   baseBranch: "master",
   updateInternalDependencies: "patch",
+  ignore: [] as ReadonlyArray<string>,
   ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
     onlyUpdatePeerDependentsWhenOutOfRange: false,
     useCalculatedVersionForSnapshots: false
@@ -155,6 +157,46 @@ export let parse = (json: WrittenConfig, packages: Packages): Config => {
       )} but can only be 'patch' or 'minor'`
     );
   }
+  if (json.ignore) {
+    if (
+      !(
+        Array.isArray(json.ignore) &&
+        json.ignore.every(pkgName => typeof pkgName === "string")
+      )
+    ) {
+      messages.push(
+        `The \`ignore\` option is set as ${JSON.stringify(
+          json.ignore,
+          null,
+          2
+        )} when the only valid values are undefined or an array of package names`
+      );
+    } else {
+      let pkgNames = new Set(
+        packages.packages.map(({ packageJson }) => packageJson.name)
+      );
+      for (let pkgName of json.ignore) {
+        if (!pkgNames.has(pkgName)) {
+          messages.push(
+            `The package "${pkgName}" is specified in the \`ignore\` option but it is not found in the project. You may have misspelled the package name.`
+          );
+        }
+      }
+
+      // Validate that all dependents of ignored packages are listed in the ignore list
+      const dependentsGraph = getDependentsGraph(packages);
+      for (const ignoredPackage of json.ignore) {
+        const dependents = dependentsGraph.get(ignoredPackage) || [];
+        for (const dependent of dependents) {
+          if (!json.ignore.includes(dependent)) {
+            messages.push(
+              `The package "${dependent}" depends on the ignored package "${ignoredPackage}", but "${dependent}" is not being ignored. Please add "${dependent}" to the \`ignore\` option.`
+            );
+          }
+        }
+      }
+    }
+  }
 
   if (json.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH !== undefined) {
     const {
@@ -192,6 +234,7 @@ export let parse = (json: WrittenConfig, packages: Packages): Config => {
         messages.join("\n")
     );
   }
+
   let config: Config = {
     changelog: getNormalisedChangelogOption(
       json.changelog === undefined
@@ -216,6 +259,9 @@ export let parse = (json: WrittenConfig, packages: Packages): Config => {
         ? defaultWrittenConfig.updateInternalDependencies
         : json.updateInternalDependencies,
 
+    ignore:
+      json.ignore === undefined ? defaultWrittenConfig.ignore : json.ignore,
+
     ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
       onlyUpdatePeerDependentsWhenOutOfRange:
         json.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH === undefined ||
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index aa74cb9..691c59c 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -63,6 +63,7 @@ export type Config = {
   baseBranch: string;
   /** The minimum bump type to trigger automatic update of internal dependencies that are part of the same release */
   updateInternalDependencies: "patch" | "minor";
+  ignore: ReadonlyArray<string>;
   ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: Required<
     ExperimentalOptions
   >;
@@ -76,6 +77,7 @@ export type WrittenConfig = {
   baseBranch?: string;
   /** The minimum bump type to trigger automatic update of internal dependencies that are part of the same release */
   updateInternalDependencies?: "patch" | "minor";
+  ignore?: ReadonlyArray<string>;
   ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH?: ExperimentalOptions;
 };
 


Test patch:
diff --git a/packages/apply-release-plan/src/index.test.ts b/packages/apply-release-plan/src/index.test.ts
index da14b92..e265076 100644
--- a/packages/apply-release-plan/src/index.test.ts
+++ b/packages/apply-release-plan/src/index.test.ts
@@ -44,6 +44,7 @@ class FakeReleasePlan {
       access: "restricted",
       baseBranch: "master",
       updateInternalDependencies: "patch",
+      ignore: [],
       ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
         onlyUpdatePeerDependentsWhenOutOfRange: false,
         useCalculatedVersionForSnapshots: false
@@ -77,6 +78,7 @@ async function testSetup(
       access: "restricted",
       baseBranch: "master",
       updateInternalDependencies: "patch",
+      ignore: [],
       ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
         onlyUpdatePeerDependentsWhenOutOfRange: false,
         useCalculatedVersionForSnapshots: false
@@ -283,6 +285,7 @@ describe("apply release plan", () => {
           access: "restricted",
           baseBranch: "master",
           updateInternalDependencies: "patch",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
@@ -343,6 +346,7 @@ describe("apply release plan", () => {
           access: "restricted",
           baseBranch: "master",
           updateInternalDependencies: "patch",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
@@ -407,6 +411,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -489,6 +494,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -563,6 +569,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -637,6 +644,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -714,6 +722,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -796,6 +805,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -870,6 +880,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -944,6 +955,7 @@ describe("apply release plan", () => {
               access: "restricted",
               baseBranch: "master",
               updateInternalDependencies,
+              ignore: [],
               ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
                 onlyUpdatePeerDependentsWhenOutOfRange: false,
                 useCalculatedVersionForSnapshots: false
@@ -1022,6 +1034,7 @@ describe("apply release plan", () => {
             access: "restricted",
             baseBranch: "master",
             updateInternalDependencies: "patch",
+            ignore: [],
             ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
               onlyUpdatePeerDependentsWhenOutOfRange: true,
               useCalculatedVersionForSnapshots: false
@@ -1179,6 +1192,7 @@ describe("apply release plan", () => {
             null
           ],
           updateInternalDependencies: "patch",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
@@ -1281,6 +1295,7 @@ describe("apply release plan", () => {
           access: "restricted",
           baseBranch: "master",
           updateInternalDependencies: "patch",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
@@ -1361,6 +1376,7 @@ describe("apply release plan", () => {
           access: "restricted",
           baseBranch: "master",
           updateInternalDependencies: "minor",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
@@ -1445,6 +1461,7 @@ describe("apply release plan", () => {
           access: "restricted",
           baseBranch: "master",
           updateInternalDependencies: "minor",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
@@ -1542,6 +1559,7 @@ describe("apply release plan", () => {
           access: "restricted",
           baseBranch: "master",
           updateInternalDependencies: "minor",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
@@ -1739,6 +1757,32 @@ describe("apply release plan", () => {
       let pathExists = await fs.pathExists(changesetPath);
       expect(pathExists).toEqual(false);
     });
+    it("should NOT delete changesets for ignored packages", async () => {
+      const releasePlan = new FakeReleasePlan();
+
+      let changesetPath: string;
+
+      const setupFunc = (tempDir: string) =>
+        Promise.all(
+          releasePlan.getReleasePlan().changesets.map(({ id, summary }) => {
+            const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`);
+            changesetPath = thisPath;
+            const content = `---\n---\n${summary}`;
+            fs.writeFile(thisPath, content);
+          })
+        );
+
+      await testSetup(
+        "simple-project",
+        releasePlan.getReleasePlan(),
+        { ...releasePlan.config, ignore: ["pkg-a"] },
+        setupFunc
+      );
+
+      // @ts-ignore this is possibly bad
+      let pathExists = await fs.pathExists(changesetPath);
+      expect(pathExists).toEqual(true);
+    });
     it("should delete an old format changeset if it is applied", async () => {
       const releasePlan = new FakeReleasePlan();
 
@@ -1914,6 +1958,7 @@ describe("apply release plan", () => {
           access: "restricted",
           baseBranch: "master",
           updateInternalDependencies: "patch",
+          ignore: [],
           ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
             onlyUpdatePeerDependentsWhenOutOfRange: false,
             useCalculatedVersionForSnapshots: false
diff --git a/packages/assemble-release-plan/src/increment.test.ts b/packages/assemble-release-plan/src/increment.test.ts
new file mode 100644
index 0000000..ac474cc
--- /dev/null
+++ b/packages/assemble-release-plan/src/increment.test.ts
@@ -0,0 +1,28 @@
+import { incrementVersion } from "./increment";
+import { InternalRelease, PreInfo } from "./types";
+
+describe("incrementVersion", () => {
+  describe("pre mode", () => {
+    it("should not bump version for releases with release type 'none'", () => {
+      const fakeRelease: InternalRelease = {
+        name: "pkg-a",
+        type: "none",
+        changesets: [],
+        oldVersion: "1.0.0"
+      };
+
+      const fakePreInfo: PreInfo = {
+        preVersions: new Map(),
+        state: {
+          mode: "pre",
+          tag: "next",
+          initialVersions: {},
+          changesets: []
+        }
+      };
+
+      const nextVersion = incrementVersion(fakeRelease, fakePreInfo);
+      expect(nextVersion).toBe("1.0.0");
+    });
+  });
+});
diff --git a/packages/assemble-release-plan/src/index.test.ts b/packages/assemble-release-plan/src/index.test.ts
index 879054e..4c56c06 100644
--- a/packages/assemble-release-plan/src/index.test.ts
+++ b/packages/assemble-release-plan/src/index.test.ts
@@ -348,6 +348,138 @@ describe("assemble-release-plan", () => {
     expect(releases[1].name).toEqual("pkg-b");
     expect(releases[1].newVersion).toEqual("2.0.0");
   });
+  it("should assemble release plan without ignored packages", () => {
+    setup.addChangeset({
+      id: "big-cats-delight",
+      releases: [{ name: "pkg-a", type: "major" }]
+    });
+    setup.addChangeset({
+      id: "small-dogs-sad",
+      releases: [{ name: "pkg-b", type: "minor" }]
+    });
+    const { releases } = assembleReleasePlan(
+      setup.changesets,
+      setup.packages,
+      {
+        ...defaultConfig,
+        ignore: ["pkg-b"]
+      },
+      undefined
+    );
+
+    expect(releases.length).toEqual(1);
+    expect(releases[0].name).toEqual("pkg-a");
+    expect(releases[0].newVersion).toEqual("2.0.0");
+  });
+  it("should generate releases with 'none' release type for ignored packages through dependencies", () => {
+    setup.updateDependency("pkg-b", "pkg-a", "1.0.0");
+    setup.addChangeset({
+      id: "big-cats-delight",
+      releases: [{ name: "pkg-a", type: "major" }]
+    });
+    setup.addChangeset({
+      id: "small-dogs-sad",
+      releases: [{ name: "pkg-b", type: "minor" }]
+    });
+    const { releases } = assembleReleasePlan(
+      setup.changesets,
+      setup.packages,
+      {
+        ...defaultConfig,
+        ignore: ["pkg-b"]
+      },
+      undefined
+    );
+
+    expect(releases.length).toEqual(2);
+    expect(releases[0].name).toEqual("pkg-a");
+    expect(releases[0].newVersion).toEqual("2.0.0");
+    expect(releases[1].name).toEqual("pkg-b");
+    expect(releases[1].type).toEqual("none");
+    expect(releases[1].newVersion).toEqual("1.0.0");
+  });
+  it("should generate releases with 'none' release type for ignored packages through peerDependencies", () => {
+    setup.updatePeerDep("pkg-b", "pkg-a", "1.0.0");
+    setup.addChangeset({
+      id: "big-cats-delight",
+      releases: [{ name: "pkg-a", type: "major" }]
+    });
+    setup.addChangeset({
+      id: "small-dogs-sad",
+      releases: [{ name: "pkg-b", type: "minor" }]
+    });
+    const { releases } = assembleReleasePlan(
+      setup.changesets,
+      setup.packages,
+      {
+        ...defaultConfig,
+        ignore: ["pkg-b"]
+      },
+      undefined
+    );
+
+    expect(releases.length).toEqual(2);
+    expect(releases[0].name).toEqual("pkg-a");
+    expect(releases[0].newVersion).toEqual("2.0.0");
+    expect(releases[1].name).toEqual("pkg-b");
+    expect(releases[1].type).toEqual("none");
+    expect(releases[1].newVersion).toEqual("1.0.0");
+  });
+  it("should generate releases with 'none' release type for ignored packages through devDependencies", () => {
+    setup.updateDevDependency("pkg-b", "pkg-a", "1.0.0");
+    setup.addChangeset({
+      id: "big-cats-delight",
+      releases: [{ name: "pkg-a", type: "major" }]
+    });
+    setup.addChangeset({
+      id: "small-dogs-sad",
+      releases: [{ name: "pkg-b", type: "minor" }]
+    });
+    const { releases } = assembleReleasePlan(
+      setup.changesets,
+      setup.packages,
+      {
+        ...defaultConfig,
+        ignore: ["pkg-b"]
+      },
+      undefined
+    );
+
+    expect(releases.length).toEqual(2);
+    expect(releases[0].name).toEqual("pkg-a");
+    expect(releases[0].newVersion).toEqual("2.0.0");
+    expect(releases[1].name).toEqual("pkg-b");
+    expect(releases[1].type).toEqual("none");
+    expect(releases[1].newVersion).toEqual("1.0.0");
+  });
+  // Mixed changesets are the ones that contains both ignored packages and not ignored packages
+  it("should throw for mixed changesets", () => {
+    setup.addChangeset({
+      id: "big-cats-delight",
+      releases: [
+        { name: "pkg-a", type: "major" },
+        { name: "pkg-b", type: "minor" }
+      ]
+    });
+
+    expect(() =>
+      assembleReleasePlan(
+        setup.changesets,
+        setup.packages,
+        {
+          ...defaultConfig,
+          ignore: ["pkg-b"]
+        },
+        undefined
+      )
+    ).toThrowErrorMatchingInlineSnapshot(`
+"Found mixed changeset big-cats-delight
+Found ignored packages: pkg-b
+Found not ignored packages: pkg-a
+Mixed changesets that contain both ignored and not ignored packages are not allowed"
+`);
+  });
+
   describe("pre mode exit", () => {
     it("should not generate a release for package that has no changesets and is not a dependent of any packages being released", () => {
       const { releases } = assembleReleasePlan(
diff --git a/packages/cli/src/commands/version/version.test.ts b/packages/cli/src/commands/version/version.test.ts
index ff78397..2590713 100644
--- a/packages/cli/src/commands/version/version.test.ts
+++ b/packages/cli/src/commands/version/version.test.ts
@@ -64,6 +64,12 @@ const simpleChangeset2: NewChangeset = {
   id: "wouldnit-be-nice"
 };
 
+const simpleChangeset3: NewChangeset = {
+  summary: "This is not a summary",
+  releases: [{ name: "pkg-b", type: "patch" }],
+  id: "hot-day-today"
+};
+
 const writeChangesets = (changesets: NewChangeset[], cwd: string) => {
   return Promise.all(
     changesets.map(changeset => writeChangeset(changeset, cwd))
@@ -155,6 +161,39 @@ describe("running version in a simple project", () => {
     );
   });
 
+  it("should not touch package.json of an ignored package when it is not a dependent of any releasedPackages ", async () => {
+    await writeChangesets([simpleChangeset], cwd);
+    const spy = jest.spyOn(fs, "writeFile");
+
+    await versionCommand(cwd, defaultOptions, {
+      ...modifiedDefaultConfig,
+      ignore: ["pkg-a"]
+    });
+
+    const bumpedPackageA = !!spy.mock.calls.find((call: string[]) =>
+      call[0].endsWith(`pkg-a${path.sep}package.json`)
+    );
+
+    expect(bumpedPackageA).toBe(false);
+  });
+
+  it("should not bump ignored packages", async () => {
+    await writeChangesets([simpleChangeset, simpleChangeset3], cwd);
+    const spy = jest.spyOn(fs, "writeFile");
+
+    await versionCommand(cwd, defaultOptions, {
+      ...modifiedDefaultConfig,
+      ignore: ["pkg-a"]
+    });
+
+    expect(getPkgJSON("pkg-b", spy.mock.calls)).toEqual(
+      expect.objectContaining({ name: "pkg-b", version: "1.0.1" })
+    );
+    expect(getPkgJSON("pkg-a", spy.mock.calls)).toEqual(
+      expect.objectContaining({ name: "pkg-a", version: "1.0.0" })
+    );
+  });
+
   describe("when there are multiple changeset commits", () => {
     it("should bump releasedPackages", async () => {
       await writeChangesets([simpleChangeset, simpleChangeset2], cwd);
@@ -791,4 +830,67 @@ describe("pre", () => {
       }
     ]);
   });
+  it("should not bump packages through devDependencies", async () => {
+    let cwd = f.copy("simple-dev-dep");
+    await writeChangeset(
+      {
+        releases: [{ name: "pkg-b", type: "major" }],
+        summary: "a very useful summary for the first change"
+      },
+      cwd
+    );
+
+    await pre(cwd, { command: "enter", tag: "next" });
+    await version(cwd, defaultOptions, modifiedDefaultConfig);
+    let packages = (await getPackages(cwd))!;
+
+    expect(packages.packages.map(x => x.packageJson)).toEqual([
+      {
+        devDependencies: { "pkg-b": "2.0.0-next.0" },
+        name: "pkg-a",
+        version: "1.0.0"
+      },
+      {
+        name: "pkg-b",
+        version: "2.0.0-next.0"
+      }
+    ]);
+  });
+  it("should not bump ignored packages through dependencies", async () => {
+    let cwd = f.copy("simple-project");
+    await writeChangeset(
+      {
+        releases: [{ name: "pkg-a", type: "major" }],
+        summary: "a very useful summary for the first change"
+      },
+      cwd
+    );
+
+    await writeChangeset(
+      {
+        releases: [{ name: "pkg-b", type: "major" }],
+        summary: "a very useful summary for the first change"
+      },
+      cwd
+    );
+
+    await pre(cwd, { command: "enter", tag: "next" });
+    await version(cwd, defaultOptions, {
+      ...modifiedDefaultConfig,
+      ignore: ["pkg-a"]
+    });
+    let packages = (await getPackages(cwd))!;
+
+    expect(packages.packages.map(x => x.packageJson)).toEqual([
+      {
+        dependencies: { "pkg-b": "2.0.0-next.0" },
+        name: "pkg-a",
+        version: "1.0.0"
+      },
+      {
+        name: "pkg-b",
+        version: "2.0.0-next.0"
+      }
+    ]);
+  });
 });
diff --git a/packages/cli/src/run.test.ts b/packages/cli/src/run.test.ts
new file mode 100644
index 0000000..4a6b6c3
--- /dev/null
+++ b/packages/cli/src/run.test.ts
@@ -0,0 +1,60 @@
+import fixtures from "fixturez";
+import { error } from "@changesets/logger";
+
+import { run } from "./run";
+
+const f = fixtures(__dirname);
+jest.mock("@changesets/logger");
+jest.mock("./commands/version");
+
+describe("cli", () => {
+  describe("version", () => {
+    let cwd: string;
+    beforeEach(async () => {
+      cwd = await f.copy("simple-project");
+    });
+
+    it("should validate package name passed in from --ignore flag", async () => {
+      try {
+        await run(["version"], { ignore: "pkg-c" }, cwd);
+      } catch (e) {
+        // ignore errors. We just want to validate the error message
+      }
+
+      const loggerErrorCalls = (error as any).mock.calls;
+      expect(loggerErrorCalls.length).toEqual(1);
+      expect(loggerErrorCalls[0][0]).toEqual(
+        `The package "pkg-c" is passed to the \`--ignore\` option but it is not found in the project. You may have misspelled the package name.`
+      );
+    });
+
+    it("should throw if dependents of ignored packages are not explicitly listed in the ignore array", async () => {
+      try {
+        await run(["version"], { ignore: ["pkg-b"] }, cwd);
+      } catch (e) {
+        // ignore the error. We just want to validate the error message
+      }
+
+      const loggerErrorCalls = (error as any).mock.calls;
+      expect(loggerErrorCalls.length).toEqual(1);
+      expect(loggerErrorCalls[0][0]).toEqual(
+        `The package "pkg-a" depends on the ignored package "pkg-b", but "pkg-a" is not being ignored. Please pass "pkg-a" to the \`--ignore\` flag.`
+      );
+    });
+
+    it("should throw if `--ignore` flag is used while ignore array is also defined in the config file ", async () => {
+      cwd = await f.copy("simple-project-with-ignore-config");
+      try {
+        await run(["version"], { ignore: "pkg-b" }, cwd);
+      } catch (e) {
+        // ignore errors. We just want to validate the error message
+      }
+
+      const loggerErrorCalls = (error as any).mock.calls;
+      expect(loggerErrorCalls.length).toEqual(1);
+      expect(loggerErrorCalls[0][0]).toEqual(
+        `It looks like you are trying to use the \`--ignore\` option while ignore is defined in the config file. This is currently not allowed, you can only use one of them at a time.`
+      );
+    });
+  });
+});
diff --git a/packages/config/src/index.test.ts b/packages/config/src/index.test.ts
index 68ca44d..86ba786 100644
--- a/packages/config/src/index.test.ts
+++ b/packages/config/src/index.test.ts
@@ -27,6 +27,7 @@ test("read reads the config", async () => {
     access: "restricted",
     baseBranch: "master",
     updateInternalDependencies: "patch",
+    ignore: [],
     ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
       onlyUpdatePeerDependentsWhenOutOfRange: false,
       useCalculatedVersionForSnapshots: false
@@ -41,6 +42,7 @@ let defaults = {
   access: "restricted",
   baseBranch: "master",
   updateInternalDependencies: "patch",
+  ignore: [],
   ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
     onlyUpdatePeerDependentsWhenOutOfRange: false,
     useCalculatedVersionForSnapshots: false
@@ -141,6 +143,15 @@ let correctCases = {
       ...defaults,
       updateInternalDependencies: "patch"
     }
+  },
+  ignore: {
+    input: {
+      ignore: ["pkg-a", "pkg-b"]
+    },
+    output: {
+      ...defaults,
+      ignore: ["pkg-a", "pkg-b"]
+    }
   }
 } as const;
 
@@ -288,6 +299,72 @@ The package \\"pkg-a\\" is in multiple sets of linked packages. Packages can onl
 The \`updateInternalDependencies\` option is set as \\"major\\" but can only be 'patch' or 'minor'"
 `);
   });
+  test("ignore non-array", () => {
+    expect(() =>
+      unsafeParse({
+        ignore: "string value"
+      })
+    ).toThrowErrorMatchingInlineSnapshot(`
+"Some errors occurred when validating the changesets config:
+The \`ignore\` option is set as \\"string value\\" when the only valid values are undefined or an array of package names"
+`);
+  });
+  test("ignore array of non-string", () => {
+    expect(() =>
+      unsafeParse({
+        ignore: [123, "pkg-a"]
+      })
+    ).toThrowErrorMatchingInlineSnapshot(`
+"Some errors occurred when validating the changesets config:
+The \`ignore\` option is set as [
+  123,
+  \\"pkg-a\\"
+] when the only valid values are undefined or an array of package names"
+`);
+  });
+  test("ignore package that does not exist", () => {
+    expect(() =>
+      parse(
+        {
+          ignore: ["pkg-a"]
+        },
+        defaultPackages
+      )
+    ).toThrowErrorMatchingInlineSnapshot(`
+"Some errors occurred when validating the changesets config:
+The package \\"pkg-a\\" is specified in the \`ignore\` option but it is not found in the project. You may have misspelled the package name."
+`);
+  });
+  test("ingore missing dependent packages", async () => {
+    expect(() =>
+      parse(
+        {
+          ignore: ["pkg-b"]
+        },
+        {
+          ...defaultPackages,
+          packages: [
+            {
+              packageJson: {
+                name: "pkg-a",
+                version: "1.0.0",
+                dependencies: { "pkg-b": "1.0.0" }
+              },
+              dir: "dir"
+            },
+            {
+              packageJson: { name: "pkg-b", version: "1.0.0" },
+              dir: "dir"
+            }
+          ]
+        }
+      )
+    ).toThrowErrorMatchingInlineSnapshot(`
+"Some errors occurred when validating the changesets config:
+The package \\"pkg-a\\" depends on the ignored package \\"pkg-b\\", but \\"pkg-a\\" is not being ignored. Please add \\"pkg-a\\" to the \`ignore\` option."
+`);
+  });
+
   test("onlyUpdatePeerDependentsWhenOutOfRange non-boolean", () => {
     expect(() => {
       unsafeParse({