feat: make semantic-release CI agnostic
- Remove `@semantic-release/condition-travis` from the default plugins - Verify the current branch in the core - Verify the build is not triggered by a PR in the core - Run in dry-run mode if not triggered on CI - Dry-run mode runs the `verifyConditions` plugins, allowing to detect configuration error locally - Return without error when no version has to be released due to no changes - Return without error if the build is triggered from a PR - Return without error if the current branch is not the configured branch - CLI return with exit code 1 if there is a `semanticReleaseError`, allowing to fail builds in case of config error, missing token etc... BREAKING CHANGE: `semantic-release` doesn't make sure it runs only on one Travis job anymore. The CI configuration has to be done such that `semantic-release` - runs only once per build - runs only after all tests are successful on every jobs of the build - runs on Node >=8 This can easily be done with [travis-deploy-once](https://github.com/semantic-release/travis-deploy-once). Migration Guide Modify your `.travis.yml` to use `travis-deploy-once`. Replace: ```yaml after_success: - npm run semantic-release ``` by: Replace ```yaml after_success: - npm install -g travis-deploy-once@4 - travis-deploy-once "npm run semantic-release" ```
This commit is contained in:
+177
-17
@@ -19,7 +19,6 @@ test.beforeEach(t => {
|
||||
t.context.log = stub();
|
||||
t.context.error = stub();
|
||||
t.context.logger = {log: t.context.log, error: t.context.error};
|
||||
t.context.semanticRelease = proxyquire('../index', {'./lib/logger': t.context.logger});
|
||||
});
|
||||
|
||||
test.afterEach.always(() => {
|
||||
@@ -61,7 +60,11 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
publish,
|
||||
};
|
||||
|
||||
await t.context.semanticRelease(options);
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options));
|
||||
|
||||
t.is(verifyConditions1.callCount, 1);
|
||||
t.deepEqual(verifyConditions1.args[0][0], config);
|
||||
@@ -140,7 +143,11 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
|
||||
publish: [publish1, publish2],
|
||||
};
|
||||
|
||||
await t.context.semanticRelease(options);
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options));
|
||||
|
||||
t.is(generateNotes.callCount, 2);
|
||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
||||
@@ -154,7 +161,7 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
|
||||
t.deepEqual(publish2.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
});
|
||||
|
||||
test.serial('Dry-run skips verifyConditions and publish', async t => {
|
||||
test.serial('Dry-run skips publish', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
@@ -187,9 +194,62 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
|
||||
publish,
|
||||
};
|
||||
|
||||
await t.context.semanticRelease(options);
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options));
|
||||
|
||||
t.is(verifyConditions.callCount, 0);
|
||||
t.not(t.context.log.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(getLastRelease.callCount, 1);
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
t.is(generateNotes.callCount, 1);
|
||||
t.is(publish.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Force a dry-run if not on a CI', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
let commits = await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
commits = (await gitCommits(['Second'])).concat(commits);
|
||||
|
||||
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
|
||||
const verifyConditions = stub().resolves();
|
||||
const getLastRelease = stub().resolves(lastRelease);
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
|
||||
const options = {
|
||||
dryRun: false,
|
||||
branch: 'master',
|
||||
repositoryUrl: 'git@hostname.com:owner/module.git',
|
||||
verifyConditions,
|
||||
getLastRelease,
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: false, branch: 'master'}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options));
|
||||
|
||||
t.is(t.context.log.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(getLastRelease.callCount, 1);
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
@@ -227,7 +287,11 @@ test.serial('Accept "undefined" values for the "getLastRelease" and "generateNot
|
||||
publish,
|
||||
};
|
||||
|
||||
await t.context.semanticRelease(options);
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options));
|
||||
|
||||
t.is(getLastRelease.callCount, 1);
|
||||
|
||||
@@ -245,29 +309,121 @@ test.serial('Accept "undefined" values for the "getLastRelease" and "generateNot
|
||||
t.falsy(publish.args[0][1].nextRelease.notes);
|
||||
});
|
||||
|
||||
test.serial('Throw SemanticReleaseError if not running from a git repository', async t => {
|
||||
test.serial('Returns falsy value if not running from a git repository', async t => {
|
||||
// Set the current working directory to a temp directory
|
||||
process.chdir(tempy.directory());
|
||||
|
||||
const error = await t.throws(t.context.semanticRelease());
|
||||
|
||||
// Verify error code and type
|
||||
t.is(error.code, 'ENOGITREPO');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.falsy(await semanticRelease());
|
||||
t.is(t.context.error.args[0][0], 'Semantic-release must run from a git repository.');
|
||||
});
|
||||
|
||||
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found', async t => {
|
||||
test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
|
||||
const error = await t.throws(t.context.semanticRelease());
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: true}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease({repositoryUrl: 'git@hostname.com:owner/module.git'}));
|
||||
t.is(
|
||||
t.context.log.args[0][0],
|
||||
'This run was triggered by a pull request and therefore a new version won’t be published.'
|
||||
);
|
||||
});
|
||||
|
||||
test.serial('Returns falsy value if not running from the configured branch', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
|
||||
const verifyConditions = stub().resolves();
|
||||
const getLastRelease = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
const publish = stub().resolves();
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl: 'git@hostname.com:owner/module.git',
|
||||
verifyConditions: [verifyConditions],
|
||||
getLastRelease,
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'other-branch', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease(options));
|
||||
t.is(
|
||||
t.context.log.args[0][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 won’t be published.'
|
||||
);
|
||||
});
|
||||
|
||||
test.serial('Returns falsy value if there is no relevant changes', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First']);
|
||||
|
||||
const verifyConditions = stub().resolves();
|
||||
const getLastRelease = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
const publish = stub().resolves();
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl: 'git@hostname.com:owner/module.git',
|
||||
verifyConditions: [verifyConditions],
|
||||
getLastRelease,
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease(options));
|
||||
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[6][0], 'There are no relevant changes, so no new version is released.');
|
||||
});
|
||||
|
||||
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
|
||||
await gitRepo();
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const error = await t.throws(semanticRelease());
|
||||
|
||||
// Verify error code and type
|
||||
t.is(error.code, 'ENOREPOURL');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
});
|
||||
|
||||
test.serial('Throw an Error if returns an unexpected value', async t => {
|
||||
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
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
@@ -287,7 +443,11 @@ test.serial('Throw an Error if returns an unexpected value', async t => {
|
||||
getLastRelease,
|
||||
};
|
||||
|
||||
const error = await t.throws(t.context.semanticRelease(options), Error);
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const error = await t.throws(semanticRelease(options), Error);
|
||||
|
||||
// Verify error message
|
||||
t.regex(error.message, new RegExp(DEFINITIONS.getLastRelease.output.message));
|
||||
|
||||
Reference in New Issue
Block a user