Repository: atlassian/changesets (ts)

Issue:
`changeset status` errors when no publishable package has been modified
## Affected Packages

`@changesets/cli`

## Problem

Hello and thank you for changesets! It looks great and I'm excited to adopt it where I work 🙂 
I'm migrating our versioning/publishing automation from [rush](https://rushjs.io/pages/commands/rush_change/) to changesets, and changesets is a much lighter solution for my goals.

The only thing I miss from rush is their equivalent of `changeset status`, [`rush change -v`](https://rushjs.io/pages/commands/rush_change/). It automatically detects if any changes in your working branch actually affect a publishable package before erroring. That means if I have a branch that only makes changes to i.e. monorepo tooling, `rush change -v` does not error.

**TL;DR; if I modify e.g. the root `package.json` of a monorepo and run `changeset status --since=main`, it will error, even though I didn't modify any publishable packages.**

## Proposed solution

It looks like currently, `changeset status --since=branch` only checks for new changeset files in `.changeset/` to determine if it should error.

Ideally, it would also check if the diff since `branch` includes changes to files within packages folders before erroring.




Pull request description:
Dont fail `changeset status` command if there are no changed packages
Fixes #496.

Add a check to the status command, so `process.exit(1)` is only called if there are no changesets _and_ a package has been changed. This prevents a few false-positives that can hurt DX. See #496 for more details.

@Andarist I wasn't sure if you wanted this to be a major bump or not.. IMO this should be a minor bump, since the new version will produce _less_ errors than before, not more (meaning, no CI checks should fail that were not already failing before). Let me know if you agree or not, and I'll add the changeset accordingly.

Patch:
diff --git a/.changeset/chatty-plums-watch.md b/.changeset/chatty-plums-watch.md
new file mode 100644
index 0000000..9be3afd
--- /dev/null
+++ b/.changeset/chatty-plums-watch.md
@@ -0,0 +1,5 @@
+---
+"@changesets/cli": minor
+---
+
+`changeset status` command no longer errors when no packages have been changed.
diff --git a/packages/cli/src/commands/add/createChangeset.ts b/packages/cli/src/commands/add/createChangeset.ts
index e52ebed..527a8c8 100644
--- a/packages/cli/src/commands/add/createChangeset.ts
+++ b/packages/cli/src/commands/add/createChangeset.ts
@@ -218,7 +218,6 @@ export default async function createChangeset(
       )}? (current version is ${pkg.packageJson.version})`,
       ["patch", "minor", "major"]
     );
-    console.log(type);
     if (type === "major") {
       let shouldReleaseAsMajor = await confirmMajorRelease(pkg.packageJson);
       if (!shouldReleaseAsMajor) {
diff --git a/packages/cli/src/commands/status/index.ts b/packages/cli/src/commands/status/index.ts
index f53eba2..0dcecae 100644
--- a/packages/cli/src/commands/status/index.ts
+++ b/packages/cli/src/commands/status/index.ts
@@ -2,6 +2,8 @@ import chalk from "chalk";
 import table from "tty-table";
 import fs from "fs-extra";
 import path from "path";
+
+import * as git from "@changesets/git";
 import getReleasePlan from "@changesets/get-release-plan";
 import { error, log, info, warn } from "@changesets/logger";
 import {
@@ -32,16 +34,22 @@ export default async function getStatus(
     );
     warn("Use --since=master instead");
   }
-  const releasePlan = await getReleasePlan(
-    cwd,
-    since === undefined ? (sinceMaster ? "master" : undefined) : since,
-    config
-  );
-
+  const sinceBranch =
+    since === undefined ? (sinceMaster ? "master" : undefined) : since;
+  const releasePlan = await getReleasePlan(cwd, sinceBranch, config);
   const { changesets, releases } = releasePlan;
+  const changedPackages = await git.getChangedPackagesSinceRef({
+    cwd,
+    ref: sinceBranch || config.baseBranch
+  });
 
-  if (changesets.length < 1) {
-    error("No changesets present");
+  if (changedPackages.length > 0 && changesets.length === 0) {
+    error(
+      "Some packages have been changed but no changesets were found. Run `changeset add` to resolve this error."
+    );
+    error(
+      "If this change doesn't need a release, run `changeset add --empty`."
+    );
     process.exit(1);
   }
 


Test patch:
diff --git a/packages/cli/src/commands/publish/__tests__/index.test.ts b/packages/cli/src/commands/publish/__tests__/index.test.ts
index bf33cf4..44a7240 100644
--- a/packages/cli/src/commands/publish/__tests__/index.test.ts
+++ b/packages/cli/src/commands/publish/__tests__/index.test.ts
@@ -4,6 +4,7 @@ import { defaultConfig } from "@changesets/config";
 import * as path from "path";
 import * as pre from "@changesets/pre";
 import { Config } from "@changesets/types";
+import { temporarilySilenceLogs } from "@changesets/test-utils";
 
 let changelogPath = path.resolve(__dirname, "../../changelog");
 let modifiedDefaultConfig: Config = {
@@ -18,6 +19,7 @@ jest.mock("../publishPackages.ts");
 jest.mock("@changesets/pre");
 
 describe("Publish command", () => {
+  temporarilySilenceLogs();
   let cwd: string;
 
   beforeEach(async () => {
diff --git a/packages/cli/src/commands/publish/__tests__/publishPackages.test.ts b/packages/cli/src/commands/publish/__tests__/publishPackages.test.ts
index ad1533b..ac3ffe1 100644
--- a/packages/cli/src/commands/publish/__tests__/publishPackages.test.ts
+++ b/packages/cli/src/commands/publish/__tests__/publishPackages.test.ts
@@ -3,6 +3,7 @@ import fixtures from "fixturez";
 import publishPackages from "../publishPackages";
 import * as npmUtils from "../npm-utils";
 import { getPackages } from "@manypkg/get-packages";
+import { temporarilySilenceLogs } from "@changesets/test-utils";
 
 jest.mock("../npm-utils");
 jest.mock("is-ci", () => true);
@@ -10,6 +11,7 @@ jest.mock("is-ci", () => true);
 const f = fixtures(__dirname);
 
 describe("publishPackages", () => {
+  temporarilySilenceLogs();
   let cwd: string;
 
   beforeEach(async () => {
diff --git a/packages/cli/src/commands/status/__tests__/status.ts b/packages/cli/src/commands/status/__tests__/status.ts
index 1a57df6..376f1e0 100644
--- a/packages/cli/src/commands/status/__tests__/status.ts
+++ b/packages/cli/src/commands/status/__tests__/status.ts
@@ -1,6 +1,7 @@
 import fixtures from "fixturez";
 import fs from "fs-extra";
 import path from "path";
+import * as git from "@changesets/git";
 import { defaultConfig } from "@changesets/config";
 import { temporarilySilenceLogs } from "@changesets/test-utils";
 import writeChangeset from "@changesets/write";
@@ -9,8 +10,10 @@ import status from "..";
 
 import humanId from "human-id";
 import { NewChangeset, ReleasePlan } from "@changesets/types";
+import { Package } from "@manypkg/get-packages";
 
 jest.mock("human-id");
+jest.mock("@changesets/git");
 
 const f = fixtures(__dirname);
 
@@ -53,7 +56,16 @@ const simpleReleasePlan: ReleasePlan = {
   preState: undefined
 };
 
-jest.mock("@changesets/git");
+const simpleChangedPackagesList: Package[] = [
+  {
+    packageJson: { name: "pkg-a", version: "1.0.0", dependencies: {} },
+    dir: "/fake/folder/doesnt/matter"
+  },
+  {
+    packageJson: { name: "pkg-b", version: "1.0.0" },
+    dir: "/fake/folder/doesnt/matter"
+  }
+];
 
 const writeChangesets = (changesets: NewChangeset[], cwd: string) => {
   return Promise.all(changesets.map(commit => writeChangeset(commit, cwd)));
@@ -71,16 +83,61 @@ describe("status", () => {
     const changesetID = "ascii";
     // @ts-ignore
     humanId.mockReturnValueOnce(changesetID);
+    // @ts-ignore
+    git.getChangedPackagesSinceRef.mockImplementation(
+      () => simpleChangedPackagesList
+    );
 
     await writeChangesets([simpleChangeset], cwd);
     const releaseObj = await status(cwd, {}, defaultConfig);
     expect(releaseObj).toEqual(simpleReleasePlan);
   });
-  it("should exit with a non-zero error code when there are no changesets", async () => {
+
+  it("should exit early with a non-zero error code when there are changed packages but no changesets", async () => {
     // @ts-ignore
-    const mockExit = jest.spyOn(process, "exit").mockImplementation(() => {});
+    jest.spyOn(process, "exit").mockImplementation(() => {});
+    // @ts-ignore
+    git.getChangedPackagesSinceRef.mockImplementation(
+      () => simpleChangedPackagesList
+    );
+
     await status(cwd, {}, defaultConfig);
-    expect(mockExit).toHaveBeenCalledWith(1);
+
+    expect(process.exit).toHaveBeenCalledWith(1);
+  });
+
+  it("should not exit early with a non-zero error code when there are no changed packages", async () => {
+    // @ts-ignore
+    jest.spyOn(process, "exit").mockImplementation(() => {});
+    // @ts-ignore
+    git.getChangedPackagesSinceRef.mockImplementation(() => []);
+
+    const releaseObj = await status(cwd, {}, defaultConfig);
+
+    expect(process.exit).not.toHaveBeenCalled();
+    expect(releaseObj).toEqual({
+      changesets: [],
+      releases: [],
+      preState: undefined
+    });
+  });
+
+  it("should not exit early with a non-zero code when there are changed packages and also a changeset", async () => {
+    // @ts-ignore
+    jest.spyOn(process, "exit").mockImplementation(() => {});
+    // @ts-ignore
+    git.getChangedPackagesSinceRef.mockImplementation(
+      () => simpleChangedPackagesList
+    );
+    const changesetID = "ascii";
+    // @ts-ignore
+    humanId.mockReturnValueOnce(changesetID);
+
+    await writeChangesets([simpleChangeset], cwd);
+    const releaseObj = await status(cwd, {}, defaultConfig);
+
+    expect(releaseObj).toEqual(simpleReleasePlan);
+    expect(process.exit).not.toHaveBeenCalled();
   });
 
   it.skip("should respect since master flag", () => false);
@@ -88,6 +145,10 @@ describe("status", () => {
   it("should respect the output flag", async () => {
     const output = "nonsense.json";
 
+    // @ts-ignore
+    git.getChangedPackagesSinceRef.mockImplementation(
+      () => simpleChangedPackagesList
+    );
     const changesetID = "ascii";
     // @ts-ignore
     humanId.mockReturnValueOnce(changesetID);