semantic-release/test/index.test.js
Matt Travi 4bf763f264
test(semantic-release-error): switched instanceof check to the native version
since the check provided by the ava assertion seems to fail beyond the context of the test
2023-06-09 16:14:46 -05:00

1998 lines
73 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import test from "ava";
import { escapeRegExp, isString, omit, sortBy } from "lodash-es";
import * as td from "testdouble";
import { spy, stub } from "sinon";
import { WritableStreamBuffer } from "stream-buffers";
import AggregateError from "aggregate-error";
import SemanticReleaseError from "@semantic-release/error";
import { COMMIT_EMAIL, COMMIT_NAME, SECRET_REPLACEMENT } from "../lib/definitions/constants.js";
import {
gitAddNote,
gitCheckout,
gitCommits,
gitGetNote,
gitHead as getGitHead,
gitPush,
gitRemoteTagHead,
gitRepo,
gitShallowClone,
gitTagHead,
gitTagVersion,
merge,
mergeFf,
rebase,
} from "./helpers/git-utils.js";
import pluginNoop from "./fixtures/plugin-noop.cjs";
test.beforeEach((t) => {
// Stub the logger functions
t.context.log = spy();
t.context.error = spy();
t.context.success = spy();
t.context.warn = spy();
t.context.logger = {
log: t.context.log,
error: t.context.error,
success: t.context.success,
warn: t.context.warn,
scope: () => t.context.logger,
};
});
test.afterEach.always((t) => {
td.reset();
});
test.serial("Plugins are called with expected values", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
let commits = await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.0.0", { cwd });
commits = (await gitCommits(["Second"], { cwd })).concat(commits);
await gitCheckout("next", true, { cwd });
await gitPush(repositoryUrl, "next", { cwd });
await gitCheckout("master", false, { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const lastRelease = {
version: "1.0.0",
gitHead: commits[commits.length - 1].hash,
gitTag: "v1.0.0",
name: "v1.0.0",
channels: ["next"],
};
const nextRelease = {
name: "v1.1.0",
type: "minor",
version: "1.1.0",
gitHead: await getGitHead({ cwd }),
gitTag: "v1.1.0",
channel: null,
};
const notes1 = "Release notes 1";
const notes2 = "Release notes 2";
const notes3 = "Release notes 3";
const verifyConditions1 = stub().resolves();
const verifyConditions2 = stub().resolves();
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes1 = stub().resolves(notes1);
const generateNotes2 = stub().resolves(notes2);
const generateNotes3 = stub().resolves(notes3);
const release1 = { name: "Release 1", url: "https://release1.com" };
const release2 = { name: "Release 2", url: "https://release2.com" };
const addChannel = stub().resolves(release1);
const prepare = stub().resolves();
const publish = stub().resolves(release2);
const success = stub().resolves();
const env = {};
const config = {
branches: [{ name: "master" }, { name: "next" }],
repositoryUrl,
originalRepositoryURL: repositoryUrl,
globalOpt: "global",
tagFormat: `v\${version}`,
};
const branches = [
{
channel: undefined,
name: "master",
range: ">=1.0.0",
accept: ["patch", "minor", "major"],
tags: [{ channels: ["next"], gitTag: "v1.0.0", version: "1.0.0" }],
type: "release",
main: true,
},
{
channel: "next",
name: "next",
range: ">=1.0.0",
accept: ["patch", "minor", "major"],
tags: [{ channels: ["next"], gitTag: "v1.0.0", version: "1.0.0" }],
type: "release",
main: false,
},
];
const branch = branches[0];
const options = {
...config,
plugins: false,
verifyConditions: [verifyConditions1, verifyConditions2],
analyzeCommits,
verifyRelease,
addChannel,
generateNotes: [generateNotes1, generateNotes2, generateNotes3],
prepare,
publish: [publish, pluginNoop],
success,
};
const envCiResults = { branch: "master", isCi: true, isPr: false };
const releases = [
{
...omit(lastRelease, "channels"),
...release1,
type: "major",
version: "1.0.0",
channel: null,
gitTag: "v1.0.0",
notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
pluginName: "[Function: functionStub]",
},
{
...nextRelease,
...release2,
notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
pluginName: "[Function: functionStub]",
},
{ ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: "[Function: noop]" },
];
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ env, cwd })).thenReturn(envCiResults);
const semanticRelease = (await import("../index.js")).default;
const result = await semanticRelease(options, {
cwd,
env,
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
});
t.is(verifyConditions1.callCount, 1);
t.deepEqual(verifyConditions1.args[0][0], config);
t.deepEqual(verifyConditions1.args[0][1].cwd, cwd);
t.deepEqual(verifyConditions1.args[0][1].options, options);
t.deepEqual(verifyConditions1.args[0][1].branch, branch);
t.deepEqual(verifyConditions1.args[0][1].branches, branches);
t.deepEqual(verifyConditions1.args[0][1].logger, t.context.logger);
t.deepEqual(verifyConditions1.args[0][1].envCi, envCiResults);
t.is(verifyConditions2.callCount, 1);
t.deepEqual(verifyConditions2.args[0][0], config);
t.deepEqual(verifyConditions2.args[0][1].cwd, cwd);
t.deepEqual(verifyConditions2.args[0][1].options, options);
t.deepEqual(verifyConditions2.args[0][1].branch, branch);
t.deepEqual(verifyConditions2.args[0][1].branches, branches);
t.deepEqual(verifyConditions2.args[0][1].logger, t.context.logger);
t.deepEqual(verifyConditions2.args[0][1].envCi, envCiResults);
t.is(generateNotes1.callCount, 2);
t.is(generateNotes2.callCount, 2);
t.is(generateNotes3.callCount, 2);
t.deepEqual(generateNotes1.args[0][0], config);
t.deepEqual(generateNotes1.args[0][1].options, options);
t.deepEqual(generateNotes1.args[0][1].branch, branch);
t.deepEqual(generateNotes1.args[0][1].branches, branches);
t.deepEqual(generateNotes1.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes1.args[0][1].lastRelease, {});
t.deepEqual(generateNotes1.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(generateNotes1.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(generateNotes1.args[0][1].nextRelease, {
...omit(lastRelease, "channels"),
type: "major",
version: "1.0.0",
channel: null,
gitTag: "v1.0.0",
name: "v1.0.0",
});
t.deepEqual(generateNotes2.args[0][1].envCi, envCiResults);
t.deepEqual(generateNotes2.args[0][0], config);
t.deepEqual(generateNotes2.args[0][1].options, options);
t.deepEqual(generateNotes2.args[0][1].branch, branch);
t.deepEqual(generateNotes2.args[0][1].branches, branches);
t.deepEqual(generateNotes2.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes2.args[0][1].lastRelease, {});
t.deepEqual(generateNotes2.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(generateNotes2.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(generateNotes2.args[0][1].nextRelease, {
...omit(lastRelease, "channels"),
type: "major",
version: "1.0.0",
channel: null,
gitTag: "v1.0.0",
name: "v1.0.0",
notes: notes1,
});
t.deepEqual(generateNotes2.args[0][1].envCi, envCiResults);
t.deepEqual(generateNotes3.args[0][0], config);
t.deepEqual(generateNotes3.args[0][1].options, options);
t.deepEqual(generateNotes3.args[0][1].branch, branch);
t.deepEqual(generateNotes3.args[0][1].branches, branches);
t.deepEqual(generateNotes3.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes3.args[0][1].lastRelease, {});
t.deepEqual(generateNotes3.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(generateNotes3.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(generateNotes3.args[0][1].nextRelease, {
...omit(lastRelease, "channels"),
type: "major",
version: "1.0.0",
channel: null,
gitTag: "v1.0.0",
name: "v1.0.0",
notes: `${notes1}\n\n${notes2}`,
});
t.deepEqual(generateNotes3.args[0][1].envCi, envCiResults);
branch.tags.push({
version: "1.0.0",
channel: null,
gitTag: "v1.0.0",
gitHead: commits[commits.length - 1].hash,
});
t.is(addChannel.callCount, 1);
t.deepEqual(addChannel.args[0][0], config);
t.deepEqual(addChannel.args[0][1].options, options);
t.deepEqual(addChannel.args[0][1].branch, branch);
t.deepEqual(addChannel.args[0][1].branches, branches);
t.deepEqual(addChannel.args[0][1].logger, t.context.logger);
t.deepEqual(addChannel.args[0][1].lastRelease, {});
t.deepEqual(addChannel.args[0][1].currentRelease, { ...lastRelease, type: "major" });
t.deepEqual(addChannel.args[0][1].nextRelease, {
...omit(lastRelease, "channels"),
type: "major",
version: "1.0.0",
channel: null,
gitTag: "v1.0.0",
name: "v1.0.0",
notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
});
t.deepEqual(addChannel.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(addChannel.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(addChannel.args[0][1].envCi, envCiResults);
t.is(analyzeCommits.callCount, 1);
t.deepEqual(analyzeCommits.args[0][0], config);
t.deepEqual(analyzeCommits.args[0][1].options, options);
t.deepEqual(analyzeCommits.args[0][1].branch, branch);
t.deepEqual(analyzeCommits.args[0][1].branches, branches);
t.deepEqual(analyzeCommits.args[0][1].logger, t.context.logger);
t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
t.deepEqual(analyzeCommits.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(analyzeCommits.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(analyzeCommits.args[0][1].envCi, envCiResults);
t.is(verifyRelease.callCount, 1);
t.deepEqual(verifyRelease.args[0][0], config);
t.deepEqual(verifyRelease.args[0][1].options, options);
t.deepEqual(verifyRelease.args[0][1].branch, branch);
t.deepEqual(verifyRelease.args[0][1].branches, branches);
t.deepEqual(verifyRelease.args[0][1].logger, t.context.logger);
t.deepEqual(verifyRelease.args[0][1].lastRelease, lastRelease);
t.deepEqual(verifyRelease.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(verifyRelease.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease);
t.deepEqual(verifyRelease.args[0][1].envCi, envCiResults);
t.deepEqual(generateNotes1.args[1][0], config);
t.deepEqual(generateNotes1.args[1][1].options, options);
t.deepEqual(generateNotes1.args[1][1].branch, branch);
t.deepEqual(generateNotes1.args[1][1].branches, branches);
t.deepEqual(generateNotes1.args[1][1].logger, t.context.logger);
t.deepEqual(generateNotes1.args[1][1].lastRelease, lastRelease);
t.deepEqual(generateNotes1.args[1][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes1.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes1.args[1][1].nextRelease, nextRelease);
t.deepEqual(generateNotes1.args[1][1].envCi, envCiResults);
t.deepEqual(generateNotes2.args[1][0], config);
t.deepEqual(generateNotes2.args[1][1].options, options);
t.deepEqual(generateNotes2.args[1][1].branch, branch);
t.deepEqual(generateNotes2.args[1][1].branches, branches);
t.deepEqual(generateNotes2.args[1][1].logger, t.context.logger);
t.deepEqual(generateNotes2.args[1][1].lastRelease, lastRelease);
t.deepEqual(generateNotes2.args[1][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes2.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes2.args[1][1].nextRelease, { ...nextRelease, notes: notes1 });
t.deepEqual(generateNotes2.args[1][1].envCi, envCiResults);
t.deepEqual(generateNotes3.args[1][0], config);
t.deepEqual(generateNotes3.args[1][1].options, options);
t.deepEqual(generateNotes3.args[1][1].branch, branch);
t.deepEqual(generateNotes3.args[1][1].branches, branches);
t.deepEqual(generateNotes3.args[1][1].logger, t.context.logger);
t.deepEqual(generateNotes3.args[1][1].lastRelease, lastRelease);
t.deepEqual(generateNotes3.args[1][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes3.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes3.args[1][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}` });
t.deepEqual(generateNotes3.args[1][1].envCi, envCiResults);
t.is(prepare.callCount, 1);
t.deepEqual(prepare.args[0][0], config);
t.deepEqual(prepare.args[0][1].options, options);
t.deepEqual(prepare.args[0][1].branch, branch);
t.deepEqual(prepare.args[0][1].branches, branches);
t.deepEqual(prepare.args[0][1].logger, t.context.logger);
t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(prepare.args[0][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` });
t.deepEqual(prepare.args[0][1].envCi, envCiResults);
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][0], config);
t.deepEqual(publish.args[0][1].options, options);
t.deepEqual(publish.args[0][1].branch, branch);
t.deepEqual(publish.args[0][1].branches, branches);
t.deepEqual(publish.args[0][1].logger, t.context.logger);
t.deepEqual(publish.args[0][1].lastRelease, lastRelease);
t.deepEqual(publish.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(publish.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(publish.args[0][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` });
t.deepEqual(publish.args[0][1].envCi, envCiResults);
t.is(success.callCount, 2);
t.deepEqual(success.args[0][0], config);
t.deepEqual(success.args[0][1].options, options);
t.deepEqual(success.args[0][1].branch, branch);
t.deepEqual(success.args[0][1].branches, branches);
t.deepEqual(success.args[0][1].logger, t.context.logger);
t.deepEqual(success.args[0][1].lastRelease, {});
t.deepEqual(success.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(success.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(success.args[0][1].nextRelease, {
...omit(lastRelease, "channels"),
type: "major",
version: "1.0.0",
channel: null,
gitTag: "v1.0.0",
name: "v1.0.0",
notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
});
t.deepEqual(success.args[0][1].releases, [releases[0]]);
t.deepEqual(success.args[0][1].envCi, envCiResults);
t.deepEqual(success.args[1][0], config);
t.deepEqual(success.args[1][1].options, options);
t.deepEqual(success.args[1][1].branch, branch);
t.deepEqual(success.args[1][1].branches, branches);
t.deepEqual(success.args[1][1].logger, t.context.logger);
t.deepEqual(success.args[1][1].lastRelease, lastRelease);
t.deepEqual(success.args[1][1].commits[0].hash, commits[0].hash);
t.deepEqual(success.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(success.args[1][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` });
t.deepEqual(success.args[1][1].releases, [releases[1], releases[2]]);
t.deepEqual(success.args[1][1].envCi, envCiResults);
t.deepEqual(result, {
lastRelease,
commits: [{ ...commits[0], gitTags: "(HEAD -> master, next)" }],
nextRelease: { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` },
releases,
});
// Verify the tag has been created on the local and remote repo and reference the gitHead
t.is(await gitTagHead(nextRelease.gitTag, { cwd }), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), nextRelease.gitHead);
// Verify the author/commiter name and email have been set
t.is(env.GIT_AUTHOR_NAME, COMMIT_NAME);
t.is(env.GIT_AUTHOR_EMAIL, COMMIT_EMAIL);
t.is(env.GIT_COMMITTER_NAME, COMMIT_NAME);
t.is(env.GIT_COMMITTER_EMAIL, COMMIT_EMAIL);
});
test.serial("Use custom tag format", async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["First"], { cwd });
await gitTagVersion("test-1.0.0", undefined, { cwd });
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const nextRelease = {
name: "test-2.0.0",
type: "major",
version: "2.0.0",
gitHead: await getGitHead({ cwd }),
gitTag: "test-2.0.0",
};
const notes = "Release notes";
const config = { branches: "master", repositoryUrl, globalOpt: "global", tagFormat: `test-\${version}` };
const options = {
...config,
verifyConditions: stub().resolves(),
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(notes),
addChannel: stub().resolves(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
// Verify the tag has been created on the local and remote repo and reference the gitHead
t.is(await gitTagHead(nextRelease.gitTag, { cwd }), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), nextRelease.gitHead);
});
test.serial("Use new gitHead, and recreate release notes if a prepare plugin create a commit", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
let commits = await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
commits = (await gitCommits(["Second"], { cwd })).concat(commits);
await gitPush(repositoryUrl, "master", { cwd });
const nextRelease = {
name: "v2.0.0",
type: "major",
version: "2.0.0",
gitHead: await getGitHead({ cwd }),
gitTag: "v2.0.0",
channel: null,
};
const notes = "Release notes";
const generateNotes = stub().resolves(notes);
const prepare1 = stub().callsFake(async () => {
commits = (await gitCommits(["Third"], { cwd })).concat(commits);
});
const prepare2 = stub().resolves();
const publish = stub().resolves();
const options = {
branches: ["master"],
repositoryUrl,
verifyConditions: stub().resolves(),
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes,
addChannel: stub().resolves(),
prepare: [prepare1, prepare2],
publish,
success: stub().resolves(),
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.is(generateNotes.callCount, 2);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(prepare1.callCount, 1);
t.deepEqual(prepare1.args[0][1].nextRelease, { ...nextRelease, notes });
nextRelease.gitHead = await getGitHead({ cwd });
t.deepEqual(generateNotes.args[1][1].nextRelease, { ...nextRelease, notes });
t.is(prepare2.callCount, 1);
t.deepEqual(prepare2.args[0][1].nextRelease, { ...nextRelease, notes });
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][1].nextRelease, { ...nextRelease, notes });
// Verify the tag has been created on the local and remote repo and reference the last gitHead
t.is(await gitTagHead(nextRelease.gitTag, { cwd }), commits[0].hash);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), commits[0].hash);
});
test.serial("Make a new release when a commit is forward-ported to an upper branch", async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial release"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "1.0.x"] }), "v1.0.0", { cwd });
await gitCheckout("1.0.x", true, { cwd });
await gitCommits(["fix: fix on maintenance version 1.0.x"], { cwd });
await gitTagVersion("v1.0.1", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["1.0.x"] }), "v1.0.1", { cwd });
await gitPush("origin", "1.0.x", { cwd });
await gitCheckout("master", false, { cwd });
await gitCommits(["feat: new feature on master"], { cwd });
await gitTagVersion("v1.1.0", undefined, { cwd });
await merge("1.0.x", { cwd });
await gitPush("origin", "master", { cwd });
const verifyConditions = stub().resolves();
const verifyRelease = stub().resolves();
const addChannel = stub().resolves();
const prepare = stub().resolves();
const publish = stub().resolves();
const success = stub().resolves();
const config = { branches: [{ name: "1.0.x" }, { name: "master" }], repositoryUrl, tagFormat: `v\${version}` };
const options = {
...config,
verifyConditions,
verifyRelease,
addChannel,
prepare,
publish,
success,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(await semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } }));
t.is(addChannel.callCount, 0);
t.is(publish.callCount, 1);
// The release 1.1.1, triggered by the forward-port of "fix: fix on maintenance version 1.0.x" has been published from master
t.is(publish.args[0][1].nextRelease.version, "1.1.1");
t.is(success.callCount, 1);
});
test.serial("Publish a pre-release version", async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial commit"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitPush(repositoryUrl, "master", { cwd });
await gitCheckout("beta", true, { cwd });
await gitCommits(["feat: a feature"], { cwd });
await gitPush(repositoryUrl, "beta", { cwd });
const config = { branches: ["master", { name: "beta", prerelease: true }], repositoryUrl };
const options = {
...config,
verifyConditions: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(""),
addChannel: false,
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "beta", isPr: false });
const semanticRelease = (await import("../index.js")).default;
let { releases } = await semanticRelease(options, {
cwd,
env: {},
stdout: { write: () => {} },
stderr: { write: () => {} },
});
t.is(releases.length, 1);
t.is(releases[0].version, "1.1.0-beta.1");
t.is(releases[0].gitTag, "v1.1.0-beta.1");
t.is(await gitGetNote("v1.1.0-beta.1", { cwd }), '{"channels":["beta"]}');
await gitCommits(["fix: a fix"], { cwd });
({ releases } = await semanticRelease(options, {
cwd,
env,
stdout: { write: () => {} },
stderr: { write: () => {} },
}));
t.is(releases.length, 1);
t.is(releases[0].version, "1.1.0-beta.2");
t.is(releases[0].gitTag, "v1.1.0-beta.2");
t.is(await gitGetNote("v1.1.0-beta.2", { cwd }), '{"channels":["beta"]}');
});
test.serial("Publish releases from different branch on the same channel", async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial commit"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitPush(repositoryUrl, "master", { cwd });
await gitCheckout("next-major", true, { cwd });
await gitPush(repositoryUrl, "next-major", { cwd });
await gitCheckout("next", true, { cwd });
await gitCommits(["feat: a feature"], { cwd });
await gitPush(repositoryUrl, "next", { cwd });
const config = {
branches: ["master", { name: "next", channel: false }, { name: "next-major", channel: false }],
repositoryUrl,
};
const addChannel = stub().resolves({});
const options = {
...config,
verifyConditions: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(""),
addChannel,
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "next", isPr: false });
let semanticRelease = (await import("../index.js")).default;
let { releases } = await semanticRelease(options, {
cwd,
env,
stdout: { write: () => {} },
stderr: { write: () => {} },
});
t.is(releases.length, 1);
t.is(releases[0].version, "1.1.0");
t.is(releases[0].gitTag, "v1.1.0");
await gitCommits(["fix: a fix"], { cwd });
({ releases } = await semanticRelease(options, {
cwd,
env,
stdout: { write: () => {} },
stderr: { write: () => {} },
}));
t.is(releases.length, 1);
t.is(releases[0].version, "1.1.1");
t.is(releases[0].gitTag, "v1.1.1");
await gitCheckout("master", false, { cwd });
await merge("next", { cwd });
await gitPush("origin", "master", { cwd });
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
semanticRelease = (await import("../index.js")).default;
t.falsy(await semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } }));
t.is(addChannel.callCount, 0);
});
test.serial("Publish pre-releases the same channel as regular releases", async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial commit"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitPush(repositoryUrl, "master", { cwd });
await gitCheckout("beta", true, { cwd });
await gitCommits(["feat: a feature"], { cwd });
await gitPush(repositoryUrl, "beta", { cwd });
const config = {
branches: ["master", { name: "beta", channel: false, prerelease: true }],
repositoryUrl,
};
const options = {
...config,
verifyConditions: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(""),
addChannel: false,
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "beta", isPr: false });
const semanticRelease = (await import("../index.js")).default;
let { releases } = await semanticRelease(options, {
cwd,
env,
stdout: { write: () => {} },
stderr: { write: () => {} },
});
t.is(releases.length, 1);
t.is(releases[0].version, "1.1.0-beta.1");
t.is(releases[0].gitTag, "v1.1.0-beta.1");
await gitCommits(["fix: a fix"], { cwd });
({ releases } = await semanticRelease(options, {
cwd,
env,
stdout: { write: () => {} },
stderr: { write: () => {} },
}));
t.is(releases.length, 1);
t.is(releases[0].version, "1.1.0-beta.2");
t.is(releases[0].gitTag, "v1.1.0-beta.2");
});
test.serial("Do not add pre-releases to a different channel", async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial release"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "beta"] }), "v1.0.0", { cwd });
await gitCheckout("beta", true, { cwd });
await gitCommits(["feat: breaking change/n/nBREAKING CHANGE: break something"], { cwd });
await gitTagVersion("v2.0.0-beta.1", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["beta"] }), "v2.0.0-beta.1", { cwd });
await gitCommits(["fix: a fix"], { cwd });
await gitTagVersion("v2.0.0-beta.2", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["beta"] }), "v2.0.0-beta.2", { cwd });
await gitPush("origin", "beta", { cwd });
await gitCheckout("master", false, { cwd });
await merge("beta", { cwd });
await gitPush("origin", "master", { cwd });
const verifyConditions = stub().resolves();
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves("Release notes");
const release1 = { name: "Release 1", url: "https://release1.com" };
const addChannel = stub().resolves(release1);
const prepare = stub().resolves();
const publish = stub().resolves();
const success = stub().resolves();
const config = {
branches: [{ name: "master" }, { name: "beta", prerelease: "beta" }],
repositoryUrl,
tagFormat: `v\${version}`,
};
const options = {
...config,
verifyConditions,
verifyRelease,
addChannel,
generateNotes,
prepare,
publish,
success,
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "master", isPr: false });
const semanticRelease = (await import("../index.js")).default;
t.truthy(await semanticRelease(options, { cwd, env, stdout: { write: () => {} }, stderr: { write: () => {} } }));
t.is(addChannel.callCount, 0);
});
async function addChannelMacro(t, mergeFunction) {
const { cwd, repositoryUrl } = await gitRepo(true);
const commits = await gitCommits(["feat: initial release"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "next"] }), "v1.0.0", { cwd });
await gitCheckout("next", true, { cwd });
commits.push(...(await gitCommits(["feat: breaking change/n/nBREAKING CHANGE: break something"], { cwd })));
await gitTagVersion("v2.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["next"] }), "v2.0.0", { cwd });
commits.push(...(await gitCommits(["fix: a fix"], { cwd })));
await gitTagVersion("v2.0.1", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["next"] }), "v2.0.1", { cwd });
commits.push(...(await gitCommits(["feat: a feature"], { cwd })));
await gitTagVersion("v2.1.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["next"] }), "v2.1.0", { cwd });
await gitPush("origin", "next", { cwd });
await gitCheckout("master", false, { cwd });
// Merge all commits but last one from next to master
await mergeFunction("next~1", { cwd });
await gitPush("origin", "master", { cwd });
const notes = "Release notes";
const verifyConditions = stub().resolves();
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const release1 = { name: "Release 1", url: "https://release1.com" };
const addChannel1 = stub().resolves(release1);
const addChannel2 = stub().resolves();
const prepare = stub().resolves();
const publish = stub().resolves();
const success = stub().resolves();
const config = {
branches: [
{ name: "master", channel: "latest" },
{ name: "next", channel: "next" },
],
repositoryUrl,
tagFormat: `v\${version}`,
};
const options = {
...config,
verifyConditions,
verifyRelease,
addChannel: [addChannel1, addChannel2],
generateNotes,
prepare,
publish,
success,
};
const nextRelease = {
name: "v2.0.1",
type: "patch",
version: "2.0.1",
channel: "latest",
gitTag: "v2.0.1",
gitHead: commits[2].hash,
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "master", isPr: false });
const semanticRelease = (await import("../index.js")).default;
const result = await semanticRelease(options, { cwd, env, stdout: { write: () => {} }, stderr: { write: () => {} } });
t.deepEqual(result.releases, [
{ ...nextRelease, ...release1, notes, pluginName: "[Function: functionStub]" },
{ ...nextRelease, notes, pluginName: "[Function: functionStub]" },
]);
// Verify the tag has been created on the local and remote repo and reference
t.is(await gitTagHead(nextRelease.gitTag, { cwd }), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), nextRelease.gitHead);
}
addChannelMacro.title = (providedTitle) => `Add version to a channel after a merge (${providedTitle})`;
test.serial("fast-forward", addChannelMacro, mergeFf);
test.serial("non fast-forward", addChannelMacro, merge);
test.serial("rebase", addChannelMacro, rebase);
test.serial('Call all "success" plugins even if one errors out', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const nextRelease = {
name: "v2.0.0",
type: "major",
version: "2.0.0",
gitHead: await getGitHead({ cwd }),
gitTag: "v2.0.0",
channel: null,
};
const notes = "Release notes";
const verifyConditions1 = stub().resolves();
const verifyConditions2 = stub().resolves();
const analyzeCommits = stub().resolves(nextRelease.type);
const generateNotes = stub().resolves(notes);
const release = { name: "Release", url: "https://release.com" };
const publish = stub().resolves(release);
const success1 = stub().rejects();
const success2 = stub().resolves();
const config = {
branches: [{ name: "master" }],
repositoryUrl,
globalOpt: "global",
tagFormat: `v\${version}`,
};
const options = {
...config,
verifyConditions: [verifyConditions1, verifyConditions2],
analyzeCommits,
generateNotes,
addChannel: stub().resolves(),
prepare: stub().resolves(),
publish,
success: [success1, success2],
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "master", isPr: false });
const semanticRelease = (await import("../index.js")).default;
await t.throwsAsync(
semanticRelease(options, { cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
);
t.is(success1.callCount, 1);
t.deepEqual(success1.args[0][0], config);
t.deepEqual(success1.args[0][1].releases, [
{ ...nextRelease, ...release, notes, pluginName: "[Function: functionStub]" },
]);
t.is(success2.callCount, 1);
t.deepEqual(success2.args[0][1].releases, [
{ ...nextRelease, ...release, notes, pluginName: "[Function: functionStub]" },
]);
});
test.serial('Log all "verifyConditions" errors', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const error1 = new Error("error 1");
const error2 = new SemanticReleaseError("error 2", "ERR2");
const error3 = new SemanticReleaseError("error 3", "ERR3");
const fail = stub().resolves();
const config = {
branches: [{ name: "master" }],
repositoryUrl,
originalRepositoryURL: repositoryUrl,
tagFormat: `v\${version}`,
};
const options = {
...config,
plugins: false,
verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
fail,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
const errors = [
...(
await t.throwsAsync(
semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
)
).errors,
];
t.deepEqual(sortBy(errors, ["message"]), sortBy([error1, error2, error3], ["message"]));
t.true(t.context.error.calledWith("An error occurred while running semantic-release: %O", error1));
t.true(t.context.error.calledWith("ERR2 error 2"));
t.true(t.context.error.calledWith("ERR3 error 3"));
t.true(t.context.error.calledAfter(t.context.log));
t.is(fail.callCount, 1);
t.deepEqual(fail.args[0][0], config);
t.deepEqual(fail.args[0][1].options, options);
t.deepEqual(fail.args[0][1].logger, t.context.logger);
t.deepEqual(fail.args[0][1].errors, [error2, error3]);
});
test.serial('Log all "verifyRelease" errors', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const error1 = new SemanticReleaseError("error 1", "ERR1");
const error2 = new SemanticReleaseError("error 2", "ERR2");
const fail = stub().resolves();
const config = { branches: [{ name: "master" }], repositoryUrl, tagFormat: `v\${version}` };
const options = {
...config,
verifyConditions: stub().resolves(),
analyzeCommits: stub().resolves("major"),
verifyRelease: [stub().rejects(error1), stub().rejects(error2)],
fail,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
const errors = [
...(
await t.throwsAsync(
semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
)
).errors,
];
t.deepEqual(sortBy(errors, ["message"]), sortBy([error1, error2], ["message"]));
t.true(t.context.error.calledWith("ERR1 error 1"));
t.true(t.context.error.calledWith("ERR2 error 2"));
t.is(fail.callCount, 1);
t.deepEqual(fail.args[0][0], config);
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
});
test.serial("Dry-run skips addChannel, prepare, publish and success", async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["First"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "next"] }), "v1.0.0", { cwd });
await gitCommits(["Second"], { cwd });
await gitTagVersion("v1.1.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.1.0", { cwd });
await gitPush(repositoryUrl, "master", { cwd });
await gitCheckout("next", true, { cwd });
await gitPush("origin", "next", { cwd });
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves("minor");
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves();
const addChannel = stub().resolves();
const prepare = stub().resolves();
const publish = stub().resolves();
const success = stub().resolves();
const options = {
dryRun: true,
branches: ["master", "next"],
repositoryUrl,
verifyConditions,
analyzeCommits,
verifyRelease,
generateNotes,
addChannel,
prepare,
publish,
success,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.not(t.context.warn.args[0][0], "This run was not triggered in a known CI environment, running in dry-run mode.");
t.is(verifyConditions.callCount, 1);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 1);
t.is(generateNotes.callCount, 2);
t.is(addChannel.callCount, 0);
t.true(
t.context.warn.calledWith(`Skip step "addChannel" of plugin "[Function: ${addChannel.name}]" in dry-run mode`)
);
t.is(prepare.callCount, 0);
t.true(t.context.warn.calledWith(`Skip step "prepare" of plugin "[Function: ${prepare.name}]" in dry-run mode`));
t.is(publish.callCount, 0);
t.true(t.context.warn.calledWith(`Skip step "publish" of plugin "[Function: ${publish.name}]" in dry-run mode`));
t.is(success.callCount, 0);
t.true(t.context.warn.calledWith(`Skip step "success" of plugin "[Function: ${success.name}]" in dry-run mode`));
});
test.serial("Dry-run skips fail", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const error1 = new SemanticReleaseError("error 1", "ERR1");
const error2 = new SemanticReleaseError("error 2", "ERR2");
const fail = stub().resolves();
const options = {
dryRun: true,
branches: ["master"],
repositoryUrl,
verifyConditions: [stub().rejects(error1), stub().rejects(error2)],
fail,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
const errors = [
...(
await t.throwsAsync(
semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
)
).errors,
];
t.deepEqual(sortBy(errors, ["message"]), sortBy([error1, error2], ["message"]));
t.true(t.context.error.calledWith("ERR1 error 1"));
t.true(t.context.error.calledWith("ERR2 error 2"));
t.is(fail.callCount, 0);
t.true(t.context.warn.calledWith(`Skip step "fail" of plugin "[Function: ${fail.name}]" in dry-run mode`));
});
test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const nextRelease = {
name: "v2.0.0",
type: "major",
version: "2.0.0",
gitHead: await getGitHead({ cwd }),
gitTag: "v2.0.0",
channel: undefined,
};
const notes = "Release notes";
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const publish = stub().resolves();
const success = stub().resolves();
const options = {
dryRun: false,
branches: ["master"],
repositoryUrl,
verifyConditions,
analyzeCommits,
verifyRelease,
generateNotes,
addChannel: stub().resolves(),
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: false, branch: "master" }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.true(t.context.warn.calledWith("This run was not triggered in a known CI environment, running in dry-run mode."));
t.is(verifyConditions.callCount, 1);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 1);
t.is(generateNotes.callCount, 1);
t.is(publish.callCount, 0);
t.is(success.callCount, 0);
});
test.serial('Dry-run does not print changelog if "generateNotes" return "undefined"', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const nextRelease = { type: "major", version: "2.0.0", gitHead: await getGitHead({ cwd }), gitTag: "v2.0.0" };
const analyzeCommits = stub().resolves(nextRelease.type);
const generateNotes = stub().resolves();
const options = {
dryRun: true,
branches: ["master"],
repositoryUrl,
verifyConditions: false,
analyzeCommits,
verifyRelease: false,
generateNotes,
prepare: false,
publish: false,
success: false,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ["Release note for version 2.0.0:"]);
});
test.serial('Allow local releases with "noCi" option', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const nextRelease = {
name: "v2.0.0",
type: "major",
version: "2.0.0",
gitHead: await getGitHead({ cwd }),
gitTag: "v2.0.0",
channel: undefined,
};
const notes = "Release notes";
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const publish = stub().resolves();
const success = stub().resolves();
const options = {
noCi: true,
branches: ["master"],
repositoryUrl,
verifyConditions,
analyzeCommits,
verifyRelease,
generateNotes,
addChannel: stub().resolves(),
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: false, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.not(t.context.log.args[0][0], "This run was not triggered in a known CI environment, running in dry-run mode.");
t.not(
t.context.log.args[0][0],
"This run was triggered by a pull request and therefore a new version won't be published."
);
t.is(verifyConditions.callCount, 1);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 1);
t.is(generateNotes.callCount, 1);
t.is(publish.callCount, 1);
t.is(success.callCount, 1);
});
test.serial(
'Accept "undefined" value returned by "generateNotes" and "false" by "publish" and "addChannel"',
async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["First"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "next"] }), "v1.0.0", { cwd });
await gitCommits(["Second"], { cwd });
await gitTagVersion("v1.1.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.1.0", { cwd });
await gitPush(repositoryUrl, "master", { cwd });
await gitCheckout("next", true, { cwd });
await gitPush("origin", "next", { cwd });
await gitCheckout("master", false, { cwd });
const nextRelease = {
name: "v1.2.0",
type: "minor",
version: "1.2.0",
gitHead: await getGitHead({ cwd }),
gitTag: "v1.2.0",
channel: null,
};
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes1 = stub().resolves();
const notes2 = "Release notes 2";
const generateNotes2 = stub().resolves(notes2);
const publish = stub().resolves(false);
const addChannel = stub().resolves(false);
const success = stub().resolves();
const options = {
branches: ["master", "next"],
repositoryUrl,
verifyConditions: stub().resolves(),
analyzeCommits,
verifyRelease,
generateNotes: [generateNotes1, generateNotes2],
addChannel,
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 1);
t.is(generateNotes1.callCount, 2);
t.is(generateNotes2.callCount, 2);
t.is(addChannel.callCount, 1);
t.is(publish.callCount, 1);
t.is(success.callCount, 2);
t.deepEqual(publish.args[0][1].nextRelease, { ...nextRelease, notes: notes2 });
t.deepEqual(success.args[0][1].releases, [{ pluginName: "[Function: functionStub]" }]);
t.deepEqual(success.args[1][1].releases, [{ pluginName: "[Function: functionStub]" }]);
}
);
test.serial("Returns false if triggered by a PR", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", prBranch: "patch-1", isPr: true }));
const semanticRelease = (await import("../index.js")).default;
t.false(
await semanticRelease(
{ cwd, repositoryUrl },
{ cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() }
)
);
t.is(
t.context.log.args[t.context.log.args.length - 1][0],
"This run was triggered by a pull request and therefore a new version won't be published."
);
});
test.serial(
'Throws "EINVALIDNEXTVERSION" if next release is out of range of the current maintenance branch',
async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial commit"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "1.x"] }), "v1.0.0", { cwd });
await gitCheckout("1.x", true, { cwd });
await gitPush("origin", "1.x", { cwd });
await gitCheckout("master", false, { cwd });
await gitCommits(["feat: new feature on master"], { cwd });
await gitTagVersion("v1.1.0", undefined, { cwd });
await gitCheckout("1.x", false, { cwd });
await gitCommits(["feat: feature on maintenance version 1.x"], { cwd });
await gitPush("origin", "master", { cwd });
const verifyConditions = stub().resolves();
const verifyRelease = stub().resolves();
const addChannel = stub().resolves();
const prepare = stub().resolves();
const publish = stub().resolves();
const success = stub().resolves();
const config = {
branches: [{ name: "1.x" }, { name: "master" }],
repositoryUrl,
tagFormat: `v\${version}`,
};
const options = {
...config,
verifyConditions,
verifyRelease,
addChannel,
prepare,
publish,
success,
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "1.x", isPr: false });
const semanticRelease = (await import("../index.js")).default;
const error = await t.throwsAsync(
semanticRelease(options, { cwd, env, stdout: { write: () => {} }, stderr: { write: () => {} } })
);
t.is(error.code, "EINVALIDNEXTVERSION");
t.is(error.name, "SemanticReleaseError");
t.is(error.message, "The release `1.1.0` on branch `1.x` cannot be published as it is out of range.");
t.regex(error.details, /A valid branch could be `master`./);
}
);
test.serial('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current release branch', async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial commit"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitCheckout("next", true, { cwd });
await gitCommits(["feat: new feature on next"], { cwd });
await gitTagVersion("v1.1.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.1.0", { cwd });
await gitPush("origin", "next", { cwd });
await gitCheckout("next-major", true, { cwd });
await gitPush("origin", "next-major", { cwd });
await gitCheckout("master", false, { cwd });
await gitCommits(["feat: new feature on master", "fix: new fix on master"], { cwd });
await gitPush("origin", "master", { cwd });
const verifyConditions = stub().resolves();
const verifyRelease = stub().resolves();
const addChannel = stub().resolves();
const prepare = stub().resolves();
const publish = stub().resolves();
const success = stub().resolves();
const config = {
branches: [{ name: "master" }, { name: "next" }, { name: "next-major" }],
repositoryUrl,
tagFormat: `v\${version}`,
};
const options = {
...config,
verifyConditions,
verifyRelease,
addChannel,
prepare,
publish,
success,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
const error = await t.throwsAsync(
semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } })
);
t.is(error.code, "EINVALIDNEXTVERSION");
t.is(error.name, "SemanticReleaseError");
t.is(error.message, "The release `1.1.0` on branch `master` cannot be published as it is out of range.");
t.regex(error.details, /A valid branch could be `next` or `next-major`./);
});
test.serial('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a maintenance branch', async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["First"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "1.1.x"] }), "v1.0.0", { cwd });
await gitCommits(["Second"], { cwd });
await gitTagVersion("v1.1.0", undefined, { cwd });
await gitAddNote(JSON.stringify({ channels: [null, "1.1.x"] }), "v1.1.0", { cwd });
await gitCheckout("1.1.x", "master", { cwd });
await gitPush("origin", "1.1.x", { cwd });
await gitCheckout("master", false, { cwd });
await gitCommits(["Third"], { cwd });
await gitTagVersion("v1.1.1", undefined, { cwd });
await gitCommits(["Fourth"], { cwd });
await gitTagVersion("v1.2.0", undefined, { cwd });
await gitPush("origin", "master", { cwd });
await gitCheckout("1.1.x", false, { cwd });
await merge("master", { cwd });
await gitPush("origin", "1.1.x", { cwd });
const notes = "Release notes";
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves();
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const addChannel = stub().resolves();
const prepare = stub().resolves();
const publish = stub().resolves();
const success = stub().resolves();
const fail = stub().resolves();
const config = { branches: [{ name: "master" }, { name: "1.1.x" }], repositoryUrl, tagFormat: `v\${version}` };
const options = {
...config,
verifyConditions,
analyzeCommits,
verifyRelease,
addChannel,
generateNotes,
prepare,
publish,
success,
fail,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "1.1.x", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
const errors = [
...(
await t.throwsAsync(
semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } })
)
).errors,
];
t.is(addChannel.callCount, 0);
t.is(publish.callCount, 0);
t.is(success.callCount, 0);
t.is(fail.callCount, 1);
t.deepEqual(fail.args[0][1].errors, errors);
t.is(errors[0].code, "EINVALIDMAINTENANCEMERGE");
t.is(errors[0].name, "SemanticReleaseError");
t.truthy(errors[0].message);
t.truthy(errors[0].details);
});
test.serial("Returns false value if triggered on an outdated clone", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
let { cwd, repositoryUrl } = await gitRepo(true);
const repoDir = cwd;
// Add commits to the master branch
await gitCommits(["First"], { cwd });
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
cwd = await gitShallowClone(repositoryUrl);
await gitCommits(["Third"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.false(
await semanticRelease(
{ repositoryUrl },
{ cwd: repoDir, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() }
)
);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [
"The local branch master is behind the remote one, therefore a new version won't be published.",
]);
});
test.serial("Returns false if not running from the configured branch", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
const options = {
branches: ["master"],
repositoryUrl,
verifyConditions: stub().resolves(),
analyzeCommits: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(),
addChannel: stub().resolves(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "other-branch", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.false(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.is(
t.context.log.args[1][0],
"This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version wont be published."
);
});
test.serial("Returns false if there is no relevant changes", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const analyzeCommits = stub().resolves();
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves();
const publish = stub().resolves();
const options = {
branches: ["master"],
repositoryUrl,
verifyConditions: [stub().resolves()],
analyzeCommits,
verifyRelease,
generateNotes,
addChannel: stub().resolves(),
prepare: stub().resolves(),
publish,
success: stub().resolves(),
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.false(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 0);
t.is(generateNotes.callCount, 0);
t.is(publish.callCount, 0);
t.is(
t.context.log.args[t.context.log.args.length - 1][0],
"There are no relevant changes, so no new version is released."
);
});
test.serial("Exclude commits with [skip release] or [release skip] from analysis", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
const commits = await gitCommits(
[
"Test commit",
"Test commit [skip release]",
"Test commit [release skip]",
"Test commit [Release Skip]",
"Test commit [Skip Release]",
"Test commit [skip release]",
"Test commit\n\n commit body\n[skip release]",
"Test commit\n\n commit body\n[release skip]",
],
{ cwd }
);
await gitPush(repositoryUrl, "master", { cwd });
const analyzeCommits = stub().resolves();
const config = { branches: ["master"], repositoryUrl, globalOpt: "global" };
const options = {
...config,
verifyConditions: [stub().resolves(), stub().resolves()],
analyzeCommits,
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(),
addChannel: stub().resolves(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const env = {};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
const envCi = (await td.replaceEsm("env-ci")).default;
td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "master", isPr: false });
const semanticRelease = (await import("../index.js")).default;
await semanticRelease(options, {
cwd,
env,
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
});
t.is(analyzeCommits.callCount, 1);
t.is(analyzeCommits.args[0][1].commits.length, 2);
t.deepEqual(analyzeCommits.args[0][1].commits[0], commits[commits.length - 1]);
});
test.serial('Log both plugins errors and errors thrown by "fail" plugin', async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
const pluginError = new SemanticReleaseError("Plugin error", "ERR");
const failError1 = new Error("Fail error 1");
const failError2 = new Error("Fail error 2");
const options = {
branches: ["master"],
repositoryUrl,
verifyConditions: stub().rejects(pluginError),
fail: [stub().rejects(failError1), stub().rejects(failError2)],
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
await t.throwsAsync(
semanticRelease(options, { cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
);
t.is(t.context.error.args[t.context.error.args.length - 1][0], "ERR Plugin error");
t.is(t.context.error.args[t.context.error.args.length - 3][1], failError1);
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError2);
});
test.serial('Call "fail" only if a plugin returns a SemanticReleaseError', async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
const pluginError = new Error("Plugin error");
const fail = stub().resolves();
const options = {
branches: ["master"],
repositoryUrl,
verifyConditions: stub().rejects(pluginError),
fail,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
await t.throwsAsync(
semanticRelease(options, { cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
);
t.true(fail.notCalled);
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
});
test.serial(
"Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config",
async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd } = await gitRepo();
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
const errors = [
...(
await t.throwsAsync(
semanticRelease({}, { cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
)
).errors,
];
// Verify error code and type
t.is(errors[0].code, "ENOREPOURL");
t.is(errors[0].name, "SemanticReleaseError");
t.truthy(errors[0].message);
t.truthy(errors[0].details);
}
);
test.serial("Throw an Error if plugin returns an unexpected value", async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const { cwd, repositoryUrl } = await gitRepo(true);
// Add commits to the master branch
await gitCommits(["First"], { cwd });
// Create the tag corresponding to version 1.0.0
await gitTagVersion("v1.0.0", undefined, { cwd });
// Add new commits to the master branch
await gitCommits(["Second"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves("string");
const options = {
branches: ["master"],
repositoryUrl,
verifyConditions: [verifyConditions],
analyzeCommits,
success: stub().resolves(),
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
let error;
try {
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
});
} catch (e) {
error = e;
}
t.is(error.code, "EANALYZECOMMITSOUTPUT");
t.regex(error.details, /string/);
});
test.serial('Hide sensitive information passed to "fail" plugin', async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
const fail = stub().resolves();
const env = { MY_TOKEN: "secret token" };
const options = {
branch: "master",
repositoryUrl,
verifyConditions: stub().throws(
new SemanticReleaseError(
`Message: Exposing token ${env.MY_TOKEN}`,
"ERR",
`Details: Exposing token ${env.MY_TOKEN}`
)
),
success: stub().resolves(),
fail,
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
await t.throwsAsync(
semanticRelease(options, { cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
);
const error = fail.args[0][1].errors[0];
t.is(error.message, `Message: Exposing token ${SECRET_REPLACEMENT}`);
t.is(error.details, `Details: Exposing token ${SECRET_REPLACEMENT}`);
Object.getOwnPropertyNames(error).forEach((prop) => {
if (isString(error[prop])) {
t.notRegex(error[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
}
});
});
test.serial('Hide sensitive information passed to "success" plugin', async (t) => {
const { cwd, repositoryUrl } = await gitRepo(true);
await gitCommits(["feat: initial release"], { cwd });
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitCommits(["feat: new feature"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
const success = stub().resolves();
const env = { MY_TOKEN: "secret token" };
const options = {
branch: "master",
repositoryUrl,
verifyConditions: false,
verifyRelease: false,
prepare: false,
generateNotes: stub().resolves(`Exposing token ${env.MY_TOKEN}`),
publish: stub().resolves({
name: `Name: Exposing token ${env.MY_TOKEN}`,
url: `URL: Exposing token ${env.MY_TOKEN}`,
}),
addChannel: false,
success,
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
await semanticRelease(options, { cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() });
const release = success.args[0][1].releases[0];
t.is(release.name, `Name: Exposing token ${SECRET_REPLACEMENT}`);
t.is(release.url, `URL: Exposing token ${SECRET_REPLACEMENT}`);
Object.getOwnPropertyNames(release).forEach((prop) => {
if (isString(release[prop])) {
t.notRegex(release[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
}
});
});
test.serial("Get all commits including the ones not in the shallow clone", async (t) => {
let { cwd, repositoryUrl } = await gitRepo(true);
await gitTagVersion("v1.0.0", undefined, { cwd });
await gitCommits(["First", "Second", "Third"], { cwd });
await gitPush(repositoryUrl, "master", { cwd });
cwd = await gitShallowClone(repositoryUrl);
const nextRelease = {
name: "v2.0.0",
type: "major",
version: "2.0.0",
gitHead: await getGitHead({ cwd }),
gitTag: "v2.0.0",
channel: undefined,
};
const notes = "Release notes";
const analyzeCommits = stub().resolves(nextRelease.type);
const config = { branches: ["master"], repositoryUrl, globalOpt: "global" };
const options = {
...config,
verifyConditions: stub().resolves(),
analyzeCommits,
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(notes),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
const semanticRelease = (await import("../index.js")).default;
t.truthy(
await semanticRelease(options, {
cwd,
env: {},
stdout: new WritableStreamBuffer(),
stderr: new WritableStreamBuffer(),
})
);
t.is(analyzeCommits.args[0][1].commits.length, 3);
});