import test from "ava"; import { union } from "lodash-es"; import semver from "semver"; import * as td from "testdouble"; const getBranch = (branches, branch) => branches.find(({ name }) => name === branch); const release = (branches, name, version) => getBranch(branches, name).tags.push({ version }); const merge = (branches, source, target, tag) => { getBranch(branches, target).tags = union( getBranch(branches, source).tags.filter(({ version }) => !tag || semver.cmp(version, "<=", tag)), getBranch(branches, target).tags ); }; const remoteBranches = []; const repositoryUrl = "repositoryUrl"; let expand, getTags, getBranches; test.beforeEach(async (t) => { getTags = (await td.replaceEsm("../../lib/branches/get-tags.js")).default; expand = (await td.replaceEsm("../../lib/branches/expand.js")).default; getBranches = (await import("../../lib/branches/index.js")).default; }); test.afterEach.always((t) => { td.reset(); }); test.serial("Enforce ranges with branching release workflow", async (t) => { const branches = [ { name: "1.x", tags: [] }, { name: "1.0.x", tags: [] }, { name: "master", tags: [] }, { name: "next", tags: [] }, { name: "next-major", tags: [] }, { name: "beta", prerelease: true, tags: [] }, { name: "alpha", prerelease: true, tags: [] }, ]; const context = { options: { branches } }; td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches); td.when(getTags(context, remoteBranches)).thenResolve(branches); let result = (await getBranches(repositoryUrl, "master", context)).map(({ name, range }) => ({ name, range })); t.is(getBranch(result, "1.0.x").range, ">=1.0.0 <1.0.0", "Cannot release on 1.0.x before a releasing on master"); t.is(getBranch(result, "1.x").range, ">=1.1.0 <1.0.0", "Cannot release on 1.x before a releasing on master"); t.is(getBranch(result, "master").range, ">=1.0.0"); t.is(getBranch(result, "next").range, ">=1.0.0"); t.is(getBranch(result, "next-major").range, ">=1.0.0"); release(branches, "master", "1.0.0"); result = (await getBranches("repositoryUrl", "master", context)).map(({ name, range }) => ({ name, range })); t.is(getBranch(result, "1.0.x").range, ">=1.0.0 <1.0.0", "Cannot release on 1.0.x before a releasing on master"); t.is(getBranch(result, "1.x").range, ">=1.1.0 <1.0.0", "Cannot release on 1.x before a releasing on master"); t.is(getBranch(result, "master").range, ">=1.0.0"); t.is(getBranch(result, "next").range, ">=1.0.0"); t.is(getBranch(result, "next-major").range, ">=1.0.0"); release(branches, "master", "1.0.1"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.0.1", "Can release only > than 1.0.1 on master"); t.is(getBranch(result, "next").range, ">=1.0.1", "Can release only > than 1.0.1 on next"); t.is(getBranch(result, "next-major").range, ">=1.0.1", "Can release only > than 1.0.1 on next-major"); merge(branches, "master", "next"); merge(branches, "master", "next-major"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.0.1", "Can release only > than 1.0.1 on master"); t.is(getBranch(result, "next").range, ">=1.0.1", "Can release only > than 1.0.1 on next"); t.is(getBranch(result, "next-major").range, ">=1.0.1", "Can release only > than 1.0.1 on next-major"); release(branches, "next", "1.1.0"); release(branches, "next", "1.1.1"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.0.1 <1.1.0", "Can release only patch, > than 1.0.1 on master"); t.is(getBranch(result, "next").range, ">=1.1.1", "Can release only > than 1.1.1 on next"); t.is(getBranch(result, "next-major").range, ">=1.1.1", "Can release > than 1.1.1 on next-major"); release(branches, "next-major", "2.0.0"); release(branches, "next-major", "2.0.1"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.0.1 <1.1.0", "Can release only patch, > than 1.0.1 on master"); t.is(getBranch(result, "next").range, ">=1.1.1 <2.0.0", "Can release only patch or minor, > than 1.1.0 on next"); t.is(getBranch(result, "next-major").range, ">=2.0.1", "Can release any version, > than 2.0.1 on next-major"); merge(branches, "next-major", "beta"); release(branches, "beta", "3.0.0-beta.1"); merge(branches, "beta", "alpha"); release(branches, "alpha", "4.0.0-alpha.1"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "next-major").range, ">=2.0.1", "Can release any version, > than 2.0.1 on next-major"); merge(branches, "master", "1.0.x"); merge(branches, "master", "1.x"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.0.1 <1.1.0", "Can release only patch, > than 1.0.1 on master"); t.is( getBranch(result, "1.0.x").range, ">=1.0.1 <1.0.1", "Cannot release on 1.0.x before >= 1.1.0 is released on master" ); t.is(getBranch(result, "1.x").range, ">=1.1.0 <1.0.1", "Cannot release on 1.x before >= 1.2.0 is released on master"); release(branches, "master", "1.0.2"); release(branches, "master", "1.0.3"); release(branches, "master", "1.0.4"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.0.4 <1.1.0", "Can release only patch, > than 1.0.4 on master"); t.is( getBranch(result, "1.0.x").range, ">=1.0.1 <1.0.2", "Cannot release on 1.0.x before >= 1.1.0 is released on master" ); t.is(getBranch(result, "1.x").range, ">=1.1.0 <1.0.2", "Cannot release on 1.x before >= 1.2.0 is released on master"); merge(branches, "next", "master"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.1.1", "Can release only > than 1.1.1 on master"); t.is(getBranch(result, "next").range, ">=1.1.1 <2.0.0", "Can release only patch or minor, > than 1.1.1 on next"); t.is(getBranch(result, "next-major").range, ">=2.0.1", "Can release any version, > than 2.0.1 on next-major"); t.is( getBranch(result, "1.0.x").range, ">=1.0.1 <1.0.2", "Cannot release on 1.0.x before 1.0.x version from master are merged" ); t.is(getBranch(result, "1.x").range, ">=1.1.0 <1.0.2", "Cannot release on 1.x before >= 2.0.0 is released on master"); merge(branches, "master", "1.0.x", "1.0.4"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.1.1", "Can release only > than 1.1.1 on master"); t.is(getBranch(result, "1.0.x").range, ">=1.0.4 <1.1.0", "Can release on 1.0.x only within range"); t.is(getBranch(result, "1.x").range, ">=1.1.0 <1.1.0", "Cannot release on 1.x before >= 2.0.0 is released on master"); merge(branches, "master", "1.x"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=1.1.1", "Can release only > than 1.1.1 on master"); t.is(getBranch(result, "1.0.x").range, ">=1.0.4 <1.1.0", "Can release on 1.0.x only within range"); t.is(getBranch(result, "1.x").range, ">=1.1.1 <1.1.1", "Cannot release on 1.x before >= 2.0.0 is released on master"); merge(branches, "next-major", "next"); merge(branches, "next", "master"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=2.0.1", "Can release only > than 2.0.1 on master"); t.is(getBranch(result, "next").range, ">=2.0.1", "Can release only > than 2.0.1 on next"); t.is(getBranch(result, "next-major").range, ">=2.0.1", "Can release only > than 2.0.1 on next-major"); t.is(getBranch(result, "1.x").range, ">=1.1.1 <2.0.0", "Can release on 1.x only within range"); merge(branches, "beta", "master"); release(branches, "master", "3.0.0"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "master").range, ">=3.0.0", "Can release only > than 3.0.0 on master"); t.is(getBranch(result, "next").range, ">=3.0.0", "Can release only > than 3.0.0 on next"); t.is(getBranch(result, "next-major").range, ">=3.0.0", "Can release only > than 3.0.0 on next-major"); branches.push({ name: "1.1.x", tags: [] }); merge(branches, "1.x", "1.1.x"); result = (await getBranches("repositoryUrl", "master", { options: { branches } })).map(({ name, range }) => ({ name, range, })); t.is(getBranch(result, "1.0.x").range, ">=1.0.4 <1.1.0", "Can release on 1.0.x only within range"); t.is(getBranch(result, "1.1.x").range, ">=1.1.1 <1.2.0", "Can release on 1.1.x only within range"); t.is(getBranch(result, "1.x").range, ">=1.2.0 <2.0.0", "Can release on 1.x only within range"); }); test.serial("Throw SemanticReleaseError for invalid configurations", async (t) => { const branches = [ { name: "123", range: "123", tags: [] }, { name: "1.x", tags: [] }, { name: "maintenance-1", range: "1.x", tags: [] }, { name: "1.x.x", tags: [] }, { name: "beta", prerelease: "", tags: [] }, { name: "alpha", prerelease: "alpha", tags: [] }, { name: "preview", prerelease: "alpha", tags: [] }, ]; const context = { options: { branches } }; td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches); td.when(getTags(context, remoteBranches)).thenResolve(branches); const error = await t.throwsAsync(getBranches(repositoryUrl, "master", context)); const errors = [...error.errors]; t.is(errors[0].name, "SemanticReleaseError"); t.is(errors[0].code, "EMAINTENANCEBRANCH"); t.truthy(errors[0].message); t.truthy(errors[0].details); t.is(errors[1].name, "SemanticReleaseError"); t.is(errors[1].code, "EMAINTENANCEBRANCHES"); t.truthy(errors[1].message); t.truthy(errors[1].details); t.is(errors[2].name, "SemanticReleaseError"); t.is(errors[2].code, "EPRERELEASEBRANCH"); t.truthy(errors[2].message); t.truthy(errors[2].details); t.is(errors[3].name, "SemanticReleaseError"); t.is(errors[3].code, "EPRERELEASEBRANCHES"); t.truthy(errors[3].message); t.truthy(errors[3].details); t.is(errors[4].name, "SemanticReleaseError"); t.is(errors[4].code, "ERELEASEBRANCHES"); t.truthy(errors[4].message); t.truthy(errors[4].details); }); test.serial("Throw a SemanticReleaseError if there is duplicate branches", async (t) => { const branches = [ { name: "master", tags: [] }, { name: "master", tags: [] }, ]; const context = { options: { branches } }; td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches); td.when(getTags(context, remoteBranches)).thenResolve(branches); const errors = [...(await t.throwsAsync(getBranches(repositoryUrl, "master", context))).errors]; t.is(errors[0].name, "SemanticReleaseError"); t.is(errors[0].code, "EDUPLICATEBRANCHES"); t.truthy(errors[0].message); t.truthy(errors[0].details); }); test.serial("Throw a SemanticReleaseError for each invalid branch name", async (t) => { const branches = [ { name: "~master", tags: [] }, { name: "^master", tags: [] }, ]; const context = { options: { branches } }; const remoteBranches = []; td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches); td.when(getTags(context, remoteBranches)).thenResolve(branches); const errors = [...(await t.throwsAsync(getBranches(repositoryUrl, "master", context))).errors]; t.is(errors[0].name, "SemanticReleaseError"); t.is(errors[0].code, "EINVALIDBRANCHNAME"); t.truthy(errors[0].message); t.truthy(errors[0].details); t.is(errors[1].name, "SemanticReleaseError"); t.is(errors[1].code, "EINVALIDBRANCHNAME"); t.truthy(errors[1].message); t.truthy(errors[1].details); });