Repository: atlassian/changesets (ts)

Issue:
Dependencies are upgraded to new major versions during release
## Affected Packages

Probably:
* assemble-release-plan
* apply-release-plan

## Problem

Let's say we have a repo with three projects: foo@2.0.0, bar@2.0.0, qar@2.0.0

foo has qar@2.0.0 in the dependencies. Which is the same as in the monorepo.

bar has an older version of qar in dependencies: 1.0.0.

Now if I release a new version of qar (v2.0.1), I only want to get it bumped inside foo. I want bar to keep using qar@1.0.0

However, seems like in some cases qar is bumped in bar as well. The reason is probably that assemble-release-plan is checking the semver ranges when finding dependents but apply-release-plan is not checking the semver ranges.

## Proposed solution

Maybe we should pass more state to apply-release-plan, to prevent it from making breaking/unwanted bumps


Pull request description:
feat: bumpVersionsWithWorkspaceProtocolOnly
close #506

Patch:
diff --git a/.changeset/great-dancers-kneel.md b/.changeset/great-dancers-kneel.md
new file mode 100644
index 0000000..c53123a
--- /dev/null
+++ b/.changeset/great-dancers-kneel.md
@@ -0,0 +1,10 @@
+---
+"@changesets/apply-release-plan": minor
+"@changesets/assemble-release-plan": minor
+"@changesets/cli": minor
+"@changesets/config": minor
+"@changesets/get-dependents-graph": minor
+"@changesets/types": minor
+---
+
+New setting added: bumpVersionsWithWorkspaceProtocolOnly. When it is set to `true`, versions are bumped in `dependencies`, only if those versions are prefixed by the workspace protocol. For instance, `"foo": "workspace:^1.0.0"`.
diff --git a/__fixtures__/workspace-and-other-range-dep/.changeset/README.md b/__fixtures__/workspace-and-other-range-dep/.changeset/README.md
new file mode 100644
index 0000000..fd188f5
--- /dev/null
+++ b/__fixtures__/workspace-and-other-range-dep/.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__/workspace-and-other-range-dep/.changeset/config.json b/__fixtures__/workspace-and-other-range-dep/.changeset/config.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/__fixtures__/workspace-and-other-range-dep/.changeset/config.json
@@ -0,0 +1,1 @@
+{}
diff --git a/__fixtures__/workspace-and-other-range-dep/package.json b/__fixtures__/workspace-and-other-range-dep/package.json
new file mode 100644
index 0000000..ebb8fb8
--- /dev/null
+++ b/__fixtures__/workspace-and-other-range-dep/package.json
@@ -0,0 +1,9 @@
+{
+  "private": true,
+  "name": "simple-project",
+  "description": "three projects, each depending on one other",
+  "version": "1.0.0",
+  "workspaces": [
+    "packages/*"
+  ]
+}
diff --git a/__fixtures__/workspace-and-other-range-dep/packages/pkg-a/package.json b/__fixtures__/workspace-and-other-range-dep/packages/pkg-a/package.json
new file mode 100644
index 0000000..74bdb1f
--- /dev/null
+++ b/__fixtures__/workspace-and-other-range-dep/packages/pkg-a/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "pkg-a",
+  "version": "1.0.0",
+  "dependencies": {
+    "pkg-b": "workspace:1.0.0"
+  }
+}
diff --git a/__fixtures__/workspace-and-other-range-dep/packages/pkg-b/package.json b/__fixtures__/workspace-and-other-range-dep/packages/pkg-b/package.json
new file mode 100644
index 0000000..be4760e
--- /dev/null
+++ b/__fixtures__/workspace-and-other-range-dep/packages/pkg-b/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "pkg-b",
+  "version": "1.0.0"
+}
diff --git a/__fixtures__/workspace-and-other-range-dep/packages/pkg-c/package.json b/__fixtures__/workspace-and-other-range-dep/packages/pkg-c/package.json
new file mode 100644
index 0000000..181af8e
--- /dev/null
+++ b/__fixtures__/workspace-and-other-range-dep/packages/pkg-c/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "pkg-c",
+  "version": "1.0.0",
+  "dependencies": {
+    "pkg-b": "1.0.0"
+  }
+}
diff --git a/packages/apply-release-plan/src/index.ts b/packages/apply-release-plan/src/index.ts
index 5438cbc..7d02055 100644
--- a/packages/apply-release-plan/src/index.ts
+++ b/packages/apply-release-plan/src/index.ts
@@ -118,7 +118,9 @@ export default async function applyReleasePlan(
       updateInternalDependencies: config.updateInternalDependencies,
       onlyUpdatePeerDependentsWhenOutOfRange:
         config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH
-          .onlyUpdatePeerDependentsWhenOutOfRange
+          .onlyUpdatePeerDependentsWhenOutOfRange,
+      bumpVersionsWithWorkspaceProtocolOnly:
+        config.bumpVersionsWithWorkspaceProtocolOnly
     });
   });
 
diff --git a/packages/apply-release-plan/src/version-package.ts b/packages/apply-release-plan/src/version-package.ts
index a24e1a9..c145333 100644
--- a/packages/apply-release-plan/src/version-package.ts
+++ b/packages/apply-release-plan/src/version-package.ts
@@ -23,10 +23,12 @@ export default function versionPackage(
   versionsToUpdate: Array<{ name: string; version: string; type: VersionType }>,
   {
     updateInternalDependencies,
-    onlyUpdatePeerDependentsWhenOutOfRange
+    onlyUpdatePeerDependentsWhenOutOfRange,
+    bumpVersionsWithWorkspaceProtocolOnly
   }: {
     updateInternalDependencies: "patch" | "minor";
     onlyUpdatePeerDependentsWhenOutOfRange: boolean;
+    bumpVersionsWithWorkspaceProtocolOnly?: boolean;
   }
 ) {
   let { newVersion, packageJson } = release;
@@ -58,6 +60,8 @@ export default function versionPackage(
         const usesWorkspaceRange = depCurrentVersion.startsWith("workspace:");
         if (usesWorkspaceRange) {
           depCurrentVersion = depCurrentVersion.substr(10);
+        } else if (bumpVersionsWithWorkspaceProtocolOnly === true) {
+          continue;
         }
         if (
           // an empty string is the normalised version of x/X/*
diff --git a/packages/assemble-release-plan/src/index.ts b/packages/assemble-release-plan/src/index.ts
index 5cac31d..cfc8f13 100644
--- a/packages/assemble-release-plan/src/index.ts
+++ b/packages/assemble-release-plan/src/index.ts
@@ -100,7 +100,10 @@ function assembleReleasePlan(
     config.ignore
   );
 
-  let dependencyGraph = getDependentsGraph(packages);
+  let dependencyGraph = getDependentsGraph(packages, {
+    bumpVersionsWithWorkspaceProtocolOnly:
+      config.bumpVersionsWithWorkspaceProtocolOnly
+  });
 
   let releasesValidated = false;
   while (releasesValidated === false) {
diff --git a/packages/cli/src/run.ts b/packages/cli/src/run.ts
index db44886..9c04b21 100644
--- a/packages/cli/src/run.ts
+++ b/packages/cli/src/run.ts
@@ -139,7 +139,10 @@ export async function run(
         }
 
         // Validate that all dependents of ignored packages are listed in the ignore list
-        const dependentsGraph = getDependentsGraph(packages);
+        const dependentsGraph = getDependentsGraph(packages, {
+          bumpVersionsWithWorkspaceProtocolOnly:
+            config.bumpVersionsWithWorkspaceProtocolOnly
+        });
         for (const ignoredPackage of config.ignore) {
           const dependents = dependentsGraph.get(ignoredPackage) || [];
           for (const dependent of dependents) {
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
index a5949e3..630c0ce 100644
--- a/packages/config/src/index.ts
+++ b/packages/config/src/index.ts
@@ -305,6 +305,9 @@ export let parse = (json: WrittenConfig, packages: Packages): Config => {
         ? defaultWrittenConfig.ignore
         : normalizePackageNames(json.ignore, pkgNames)[0],
 
+    bumpVersionsWithWorkspaceProtocolOnly:
+      json.bumpVersionsWithWorkspaceProtocolOnly === true,
+
     ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
       onlyUpdatePeerDependentsWhenOutOfRange:
         json.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH === undefined ||
diff --git a/packages/get-dependents-graph/src/get-dependency-graph.ts b/packages/get-dependents-graph/src/get-dependency-graph.ts
index dd20ea3..9ac4b44 100644
--- a/packages/get-dependents-graph/src/get-dependency-graph.ts
+++ b/packages/get-dependents-graph/src/get-dependency-graph.ts
@@ -35,7 +35,10 @@ const getAllDependencies = (config: PackageJSON) => {
 };
 
 export default function getDependencyGraph(
-  packages: Packages
+  packages: Packages,
+  opts?: {
+    bumpVersionsWithWorkspaceProtocolOnly?: boolean;
+  }
 ): {
   graph: Map<string, { pkg: Package; dependencies: Array<string> }>;
   valid: boolean;
@@ -70,6 +73,8 @@ export default function getDependencyGraph(
 
       if (depVersion.startsWith("workspace:")) {
         depVersion = depVersion.substr(10);
+      } else if (opts?.bumpVersionsWithWorkspaceProtocolOnly === true) {
+        continue;
       }
 
       // internal dependencies only need to semver satisfy, not '==='
diff --git a/packages/get-dependents-graph/src/index.ts b/packages/get-dependents-graph/src/index.ts
index 541a11e..1d08442 100644
--- a/packages/get-dependents-graph/src/index.ts
+++ b/packages/get-dependents-graph/src/index.ts
@@ -1,10 +1,16 @@
 import { Packages, Package } from "@manypkg/get-packages";
 import getDependencyGraph from "./get-dependency-graph";
 
-export function getDependentsGraph(packages: Packages) {
+export function getDependentsGraph(
+  packages: Packages,
+  opts?: { bumpVersionsWithWorkspaceProtocolOnly?: boolean }
+) {
   const graph: Map<string, { pkg: Package; dependents: string[] }> = new Map();
 
-  const { graph: dependencyGraph } = getDependencyGraph(packages);
+  const { graph: dependencyGraph } = getDependencyGraph(packages, {
+    bumpVersionsWithWorkspaceProtocolOnly:
+      opts?.bumpVersionsWithWorkspaceProtocolOnly === true
+  });
 
   const dependentsLookup: {
     [key: string]: { pkg: Package; dependents: Array<string> };
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 8470b13..6f929bf 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -65,6 +65,7 @@ export type Config = {
   /** The minimum bump type to trigger automatic update of internal dependencies that are part of the same release */
   updateInternalDependencies: "patch" | "minor";
   ignore: ReadonlyArray<string>;
+  bumpVersionsWithWorkspaceProtocolOnly?: boolean;
   ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: Required<
     ExperimentalOptions
   >;
@@ -79,6 +80,7 @@ export type WrittenConfig = {
   /** The minimum bump type to trigger automatic update of internal dependencies that are part of the same release */
   updateInternalDependencies?: "patch" | "minor";
   ignore?: ReadonlyArray<string>;
+  bumpVersionsWithWorkspaceProtocolOnly?: boolean;
   ___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 b31ccd8..f382dc2 100644
--- a/packages/apply-release-plan/src/index.test.ts
+++ b/packages/apply-release-plan/src/index.test.ts
@@ -23,7 +23,8 @@ class FakeReleasePlan {
 
   constructor(
     changesets: NewChangeset[] = [],
-    releases: ComprehensiveRelease[] = []
+    releases: ComprehensiveRelease[] = [],
+    config: Partial<Config> = {}
   ) {
     const baseChangeset: NewChangeset = {
       id: "quick-lions-devour",
@@ -48,7 +49,8 @@ class FakeReleasePlan {
       ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
         onlyUpdatePeerDependentsWhenOutOfRange: false,
         useCalculatedVersionForSnapshots: false
-      }
+      },
+      ...config
     };
 
     this.changesets = [baseChangeset, ...changesets];
@@ -264,6 +266,73 @@ describe("apply release plan", () => {
         }
       });
     });
+    it("should update workspace ranges only with bumpVersionsWithWorkspaceProtocolOnly", async () => {
+      const releasePlan = new FakeReleasePlan(
+        [
+          {
+            id: "some-id",
+            releases: [
+              { name: "pkg-b", type: "minor" },
+              { name: "pkg-c", type: "minor" }
+            ],
+            summary: "a very useful summary"
+          }
+        ],
+        [
+          {
+            changesets: ["some-id"],
+            name: "pkg-b",
+            newVersion: "1.1.0",
+            oldVersion: "1.0.0",
+            type: "minor"
+          },
+          {
+            changesets: ["some-id"],
+            name: "pkg-c",
+            newVersion: "1.1.0",
+            oldVersion: "1.0.0",
+            type: "minor"
+          }
+        ],
+        {
+          bumpVersionsWithWorkspaceProtocolOnly: true
+        }
+      );
+      let { changedFiles } = await testSetup(
+        "workspace-and-other-range-dep",
+        releasePlan.getReleasePlan(),
+        releasePlan.config
+      );
+      let pkgAPath = changedFiles.find(a =>
+        a.endsWith(`pkg-a${path.sep}package.json`)
+      );
+
+      if (!pkgAPath) throw new Error(`could not find an updated package json`);
+      let pkgAJSON = await fs.readJSON(pkgAPath);
+
+      expect(pkgAJSON).toEqual({
+        name: "pkg-a",
+        version: "1.1.0",
+        dependencies: {
+          "pkg-b": "workspace:1.1.0"
+        }
+      });
+
+      let pkgCPath = changedFiles.find(a =>
+        a.endsWith(`pkg-c${path.sep}package.json`)
+      );
+
+      if (!pkgCPath) throw new Error(`could not find an updated package json`);
+      let pkgCJSON = await fs.readJSON(pkgCPath);
+
+      expect(pkgCJSON).toEqual({
+        name: "pkg-c",
+        version: "1.1.0",
+        dependencies: {
+          "pkg-b": "1.0.0"
+        }
+      });
+    });
     it("should update a version for two packages with different new versions", async () => {
       const releasePlan = new FakeReleasePlan(
         [],
diff --git a/packages/assemble-release-plan/src/index.test.ts b/packages/assemble-release-plan/src/index.test.ts
index 3372d12..3a6e616 100644
--- a/packages/assemble-release-plan/src/index.test.ts
+++ b/packages/assemble-release-plan/src/index.test.ts
@@ -95,6 +95,31 @@ describe("assemble-release-plan", () => {
     expect(releases[1].newVersion).toEqual("1.0.1");
     expect(releases[1].changesets).toEqual([]);
   });
+  it("should assemble the release plan only with workspace protocol dependents when using bumpVersionsWithWorkspaceProtocolOnly", () => {
+    setup.updateDependency("pkg-b", "pkg-a", "^1.0.0");
+    setup.updateDependency("pkg-c", "pkg-a", "workspace:^1.0.0");
+    setup.addChangeset({
+      id: "big-cats-delight",
+      releases: [{ name: "pkg-a", type: "major" }]
+    });
+
+    let { releases } = assembleReleasePlan(
+      setup.changesets,
+      setup.packages,
+      {
+        ...defaultConfig,
+        bumpVersionsWithWorkspaceProtocolOnly: true
+      },
+      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-c");
+    expect(releases[1].newVersion).toEqual("1.0.1");
+    expect(releases[1].changesets).toEqual([]);
+  });
   it("should assemble release plan without dependent through dev dependency", () => {
     setup.updateDevDependency("pkg-b", "pkg-a", "^1.0.0");
     setup.addChangeset({
diff --git a/packages/config/src/index.test.ts b/packages/config/src/index.test.ts
index 40d8f21..9b280c3 100644
--- a/packages/config/src/index.test.ts
+++ b/packages/config/src/index.test.ts
@@ -43,6 +43,7 @@ test("read reads the config", async () => {
     baseBranch: "master",
     updateInternalDependencies: "patch",
     ignore: [],
+    bumpVersionsWithWorkspaceProtocolOnly: false,
     ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
       onlyUpdatePeerDependentsWhenOutOfRange: false,
       useCalculatedVersionForSnapshots: false
@@ -61,7 +62,8 @@ let defaults = {
   ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: {
     onlyUpdatePeerDependentsWhenOutOfRange: false,
     useCalculatedVersionForSnapshots: false
-  }
+  },
+  bumpVersionsWithWorkspaceProtocolOnly: false
 } as const;
 
 let correctCases: Record<string, CorrectCase> = {