Skip to content

Commit

Permalink
feat: check that sequentialPrepare is not enabled on cyclic projects
Browse files Browse the repository at this point in the history
  • Loading branch information
KillianHmyd authored and antongolub committed Apr 13, 2021
1 parent 299748a commit 68c1198
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 4 deletions.
2 changes: 1 addition & 1 deletion lib/createInlinePluginCreator.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
};

const prepare = async (pluginOptions, context) => {
if (pkg.options.sequentialPrepare) {
if (flags.sequentialPrepare) {
debug(debugPrefix, "waiting local dependencies preparation");
await waitLocalDeps("_prepared", pkg);
}
Expand Down
12 changes: 12 additions & 0 deletions lib/isCyclicProject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Detect if there is a cyclic dependency tree between the packages
* @param {Array<Package>} pkgs Array of package object
* @returns {boolean} True if there are a cyclic dependency
*/
const isCyclicProject = (pkgs) => {
return pkgs.some((pkg) =>
pkg.localDeps.some((dep) => dep.localDeps.some((nestedDep) => nestedDep.name === pkg.name))
);
};

module.exports = isCyclicProject;
8 changes: 7 additions & 1 deletion lib/multiSemanticRelease.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { dirname } = require("path");
const semanticRelease = require("semantic-release");
const { uniq } = require("lodash");
const { check } = require("./blork");
const { check, ValueError } = require("./blork");
const getLogger = require("./getLogger");
const getSynchronizer = require("./getSynchronizer");
const getConfig = require("./getConfig");
Expand All @@ -10,6 +10,7 @@ const getManifest = require("./getManifest");
const cleanPath = require("./cleanPath");
const RescopedStream = require("./RescopedStream");
const createInlinePluginCreator = require("./createInlinePluginCreator");
const isCyclicProject = require("./isCyclicProject");

/**
* The multirelease context.
Expand Down Expand Up @@ -77,6 +78,11 @@ async function multiSemanticRelease(
logger.success(`Loaded package ${pkg.name}`);
});

if (flags.sequentialPrepare && isCyclicProject(packages)) {
logger.error("There is a cyclic dependency in packages while the sequentialPrepare is enabled");
throw new ValueError("can't have cyclic with sequentialPrepare option");
}

logger.complete(`Queued ${packages.length} packages! Starting release...`);

// Shared signal bus.
Expand Down
42 changes: 42 additions & 0 deletions test/lib/isCyclicProject.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const isCyclicProject = require("../../lib/isCyclicProject");

// Tests.
describe("isCyclicProject()", () => {
const pkgA = {
name: "pkgA",
localDeps: [],
};

const pkgB = {
name: "pkgB",
localDeps: [],
};

const pkgC = {
name: "pkgC",
localDeps: [pkgB],
};

const pkgE = {
name: "pkgD",
localDeps: [],
};

const pkgD = {
name: "pkgD",
localDeps: [pkgE],
};

pkgE.localDeps = [pkgD];

test("With independent packages", () => {
expect(isCyclicProject([pkgA, pkgB])).toBeFalsy();
});
test("With simple chain", () => {
expect(isCyclicProject([pkgB, pkgC])).toBeFalsy();
});

test("With cyclic dependency", () => {
expect(isCyclicProject([pkgD, pkgD])).toBeTruthy();
});
});
145 changes: 143 additions & 2 deletions test/lib/multiSemanticRelease.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { ValueError } = require("blork");
const { writeFileSync } = require("fs");
const { resolve } = require("path");
const path = require("path");
const { Signale } = require("signale");
const { WritableStreamBuffer } = require("stream-buffers");
Expand All @@ -17,6 +19,7 @@ const {

// Clear mocks before tests.
beforeEach(() => {
jest.setTimeout(50000);
jest.clearAllMocks(); // Clear all mocks.
require.cache = {}; // Clear the require cache so modules are loaded fresh.
});
Expand Down Expand Up @@ -353,6 +356,7 @@ describe("multiSemanticRelease()", () => {
},
});
});

test("Two separate releases (release to prerelease)", async () => {
const packages = ["packages/c/", "packages/d/"];

Expand Down Expand Up @@ -798,6 +802,111 @@ describe("multiSemanticRelease()", () => {
},
});
});

test("Changes in child packages with sequentialPrepare", async () => {
const mockPrepare = jest.fn();
// Create Git repo.
const cwd = gitInit();
// Initial commit.
copyDirectory(`test/fixtures/yarnWorkspaces2Packages/`, cwd);
const sha1 = gitCommitAll(cwd, "feat: Initial release");
gitTag(cwd, "[email protected]");
gitTag(cwd, "[email protected]");
// Second commit.
writeFileSync(`${cwd}/packages/d/aaa.txt`, "AAA");
const sha2 = gitCommitAll(cwd, "feat(aaa): Add missing text file");
const url = gitInitOrigin(cwd);
gitPush(cwd);

// Capture output.
const stdout = new WritableStreamBuffer();
const stderr = new WritableStreamBuffer();

// Call multiSemanticRelease()
// Doesn't include plugins that actually publish.
const multiSemanticRelease = require("../../");
const result = await multiSemanticRelease(
[`packages/c/package.json`, `packages/d/package.json`],
{
plugins: [
{
// Ensure that msr-test-c is always ready before msr-test-d
verify: (_, { lastRelease: { name } }) =>
new Promise((resolvePromise) => {
if (name.split("@")[0] === "msr-test-c") {
resolvePromise();
}

setTimeout(resolvePromise, 5000);
}),
},
{
prepare: (_, { lastRelease: { name } }) => {
mockPrepare(name.split("@")[0]);
},
},
],
},
{ cwd, stdout, stderr },
{ deps: {}, dryRun: false, sequentialPrepare: true }
);

expect(mockPrepare).toHaveBeenNthCalledWith(1, "msr-test-d");
expect(mockPrepare).toHaveBeenNthCalledWith(2, "msr-test-c");

// Get stdout and stderr output.
const err = stderr.getContentsAsString("utf8");
expect(err).toBe(false);
const out = stdout.getContentsAsString("utf8");
expect(out).toMatch("Started multirelease! Loading 2 packages...");
expect(out).toMatch("Loaded package msr-test-c");
expect(out).toMatch("Loaded package msr-test-d");
expect(out).toMatch("Queued 2 packages! Starting release...");
expect(out).toMatch("Created tag [email protected]");
expect(out).toMatch("Created tag [email protected]");
expect(out).toMatch("Released 2 of 2 packages, semantically!");

// C.
expect(result[0].name).toBe("msr-test-c");
expect(result[0].result.lastRelease).toMatchObject({
gitHead: sha1,
gitTag: "[email protected]",
version: "1.0.0",
});
expect(result[0].result.nextRelease).toMatchObject({
gitHead: sha2,
gitTag: "[email protected]",
type: "patch",
version: "1.0.1",
});

// D.
expect(result[1].name).toBe("msr-test-d");
expect(result[1].result.lastRelease).toEqual({
channels: [null],
gitHead: sha1,
gitTag: "[email protected]",
name: "[email protected]",
version: "1.0.0",
});
expect(result[1].result.nextRelease).toMatchObject({
gitHead: sha2,
gitTag: "[email protected]",
type: "minor",
version: "1.1.0",
});

// ONLY three times.
expect(result[2]).toBe(undefined);

// Check manifests.
expect(require(`${cwd}/packages/c/package.json`)).toMatchObject({
dependencies: {
"msr-test-d": "1.1.0",
},
});
});

test("Changes in some packages (sequential-init)", async () => {
// Create Git repo.
const cwd = gitInit();
Expand Down Expand Up @@ -829,8 +938,7 @@ describe("multiSemanticRelease()", () => {
`packages/a/package.json`,
],
{},
{ cwd, stdout, stderr },
{ sequentialInit: true, deps: {} }
{ cwd, stdout, stderr }
);

// Check manifests.
Expand Down Expand Up @@ -1081,4 +1189,37 @@ describe("multiSemanticRelease()", () => {
message: expect.stringMatching("Package peerDependencies must be object"),
});
});

test("ValueError if sequentialPrepare is enabled on a cyclic project", async () => {
// Create Git repo with copy of Yarn workspaces fixture.
const cwd = gitInit();
copyDirectory(`test/fixtures/yarnWorkspaces/`, cwd);
const sha = gitCommitAll(cwd, "feat: Initial release");
const url = gitInitOrigin(cwd);
gitPush(cwd);

// Capture output.
const stdout = new WritableStreamBuffer();
const stderr = new WritableStreamBuffer();

// Call multiSemanticRelease()
// Doesn't include plugins that actually publish.
const multiSemanticRelease = require("../../");
const result = multiSemanticRelease(
[
`packages/a/package.json`,
`packages/b/package.json`,
`packages/c/package.json`,
`packages/d/package.json`,
],
{},
{ cwd, stdout, stderr },
{ sequentialPrepare: true, deps: {} }
);

await expect(result).rejects.toBeInstanceOf(ValueError);
await expect(result).rejects.toMatchObject({
message: expect.stringMatching("can't have cyclic with sequentialPrepare option"),
});
});
});

0 comments on commit 68c1198

Please sign in to comment.