feat: add tagFormat option to customize Git tag name

This commit is contained in:
Pierre Vanduynslager
2018-01-29 00:55:32 -05:00
parent faabffb208
commit 39536fa34e
12 changed files with 287 additions and 16 deletions
+16
View File
@@ -45,6 +45,7 @@ test.serial('Default values, reading repositoryUrl from package.json', async t =
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@package.com:owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
@@ -58,6 +59,7 @@ test.serial('Default values, reading repositoryUrl from repo if not set in packa
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@repo.com:owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
@@ -72,6 +74,7 @@ test.serial('Default values, reading repositoryUrl (http url) from package.json
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, pkg.repository);
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Read options from package.json', async t => {
@@ -80,6 +83,7 @@ test.serial('Read options from package.json', async t => {
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -100,6 +104,7 @@ test.serial('Read options from .releaserc.yml', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -120,6 +125,7 @@ test.serial('Read options from .releaserc.json', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -140,6 +146,7 @@ test.serial('Read options from .releaserc.js', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -160,6 +167,7 @@ test.serial('Read options from release.config.js', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -184,6 +192,7 @@ test.serial('Prioritise CLI/API parameters over file configuration and git repo'
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_cli'},
branch: 'branch_cli',
repositoryUrl: 'http://cli-url.com/owner/package',
tagFormat: `cli\${version}`,
};
const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
@@ -209,6 +218,7 @@ test.serial('Read configuration from file path in "extends"', async t => {
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -236,6 +246,7 @@ test.serial('Read configuration from module path in "extends"', async t => {
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -270,6 +281,7 @@ test.serial('Read configuration from an array of paths in "extends"', async t =>
generateNotes: 'generateNotes2',
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
branch: 'test_branch',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -307,6 +319,7 @@ test.serial('Prioritize configuration from config file over "extends"', async t
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -352,6 +365,7 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
analyzeCommits: 'analyzeCommits2',
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
branch: 'test_branch2',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -379,6 +393,7 @@ test.serial('Allow to unset properties defined in shareable config with "null"',
const shareable = {
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -412,6 +427,7 @@ test.serial('Allow to unset properties defined in shareable config with "undefin
const shareable = {
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
+46 -4
View File
@@ -30,10 +30,10 @@ test.serial('Get the highest valid tag', async t => {
await gitCommits(['Fourth']);
await gitTagVersion('v3.0');
const result = await getLastRelease(t.context.logger);
const result = await getLastRelease(`v\${version}`, t.context.logger);
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
t.deepEqual(t.context.log.args[0], ['Found git tag version %s', 'v2.0.0']);
t.deepEqual(t.context.log.args[0], ['Found git tag %s associated with version %s', 'v2.0.0', '2.0.0']);
});
test.serial('Get the highest tag in the history of the current branch', async t => {
@@ -55,7 +55,7 @@ test.serial('Get the highest tag in the history of the current branch', async t
// Create the tag corresponding to version 2.0.0
await gitTagVersion('v2.0.0');
const result = await getLastRelease(t.context.logger);
const result = await getLastRelease(`v\${version}`, t.context.logger);
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
});
@@ -71,8 +71,50 @@ test.serial('Return empty object if no valid tag is found', async t => {
await gitCommits(['Third']);
await gitTagVersion('v3.0');
const result = await getLastRelease(t.context.logger);
const result = await getLastRelease(`v\${version}`, t.context.logger);
t.deepEqual(result, {});
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test.serial('Get the highest valid tag corresponding to the "tagFormat"', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create some commits and tags
const [{hash: gitHead}] = await gitCommits(['First']);
await gitTagVersion('1.0.0');
t.deepEqual(await getLastRelease(`\${version}`, t.context.logger), {
gitHead,
gitTag: '1.0.0',
version: '1.0.0',
});
await gitTagVersion('foo-1.0.0-bar');
t.deepEqual(await getLastRelease(`foo-\${version}-bar`, t.context.logger), {
gitHead,
gitTag: 'foo-1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('foo-v1.0.0-bar');
t.deepEqual(await getLastRelease(`foo-v\${version}-bar`, t.context.logger), {
gitHead,
gitTag: 'foo-v1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('(.+)/1.0.0/(a-z)');
t.deepEqual(await getLastRelease(`(.+)/\${version}/(a-z)`, t.context.logger), {
gitHead,
gitTag: '(.+)/1.0.0/(a-z)',
version: '1.0.0',
});
await gitTagVersion('2.0.0-1.0.0-bar.1');
t.deepEqual(await getLastRelease(`2.0.0-\${version}-bar.1`, t.context.logger), {
gitHead,
gitTag: '2.0.0-1.0.0-bar.1',
version: '1.0.0',
});
});
+15
View File
@@ -11,6 +11,7 @@ import {
gitTags,
isGitRepo,
deleteTag,
verifyTagName,
} from '../lib/git';
import {
gitRepo,
@@ -175,6 +176,20 @@ test.serial('Return "false" if not in a Git repository', async t => {
t.false(await isGitRepo());
});
test.serial('Return "true" for valid tag names', async t => {
t.true(await verifyTagName('1.0.0'));
t.true(await verifyTagName('v1.0.0'));
t.true(await verifyTagName('tag_name'));
t.true(await verifyTagName('tag/name'));
});
test.serial('Return "false" for invalid tag names', async t => {
t.false(await verifyTagName('?1.0.0'));
t.false(await verifyTagName('*1.0.0'));
t.false(await verifyTagName('[1.0.0]'));
t.false(await verifyTagName('1.0.0..'));
});
test.serial('Throws error if obtaining the tags fails', async t => {
const dir = tempy.directory();
process.chdir(dir);
+36 -1
View File
@@ -68,7 +68,7 @@ test.serial('Plugins are called with expected values', async t => {
const generateNotes = stub().resolves(notes);
const publish = stub().resolves();
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
const options = {
...config,
verifyConditions: [verifyConditions1, verifyConditions2],
@@ -130,6 +130,41 @@ test.serial('Plugins are called with expected values', async t => {
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
});
test.serial('Use custom tag format', async t => {
const repositoryUrl = await gitRepo(true);
await gitCommits(['First']);
await gitTagVersion('test-1.0.0');
await gitCommits(['Second']);
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
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 config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `test-\${version}`};
const options = {
...config,
verifyConditions,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options));
// Verify the tag has been created on the local and remote repo and reference the gitHead
t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
});
test.serial('Use new gitHead, and recreate release notes if a publish plugin create a commit', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
+100
View File
@@ -0,0 +1,100 @@
import test from 'ava';
import {stub} from 'sinon';
import tempy from 'tempy';
import verify from '../lib/verify';
import {gitRepo} from './helpers/git-utils';
// Save the current process.env
const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(t => {
// Delete environment variables that could have been set on the machine running the tests
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
// Stub the logger functions
t.context.log = stub();
t.context.error = stub();
t.context.logger = {log: t.context.log, error: t.context.error};
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Return "false" if does not run on a git repository', async t => {
const dir = tempy.directory();
process.chdir(dir);
t.false(await verify({}, 'master', t.context.logger));
});
test.serial('Throw a AggregateError', async t => {
await gitRepo();
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].message, 'The repositoryUrl option is required');
t.is(errors[0].code, 'ENOREPOURL');
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].message, 'The tagFormat template must compile to a valid Git tag format');
t.is(errors[1].code, 'EINVALIDTAGFORMAT');
t.is(errors[2].name, 'SemanticReleaseError');
t.is(errors[2].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
t.is(errors[2].code, 'ETAGNOVERSION');
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `?\${version}`};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].message, 'The tagFormat template must compile to a valid Git tag format');
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains the "version" variable', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: 'test'};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
t.is(errors[0].code, 'ETAGNOVERSION');
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version" variables', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `\${version}v\${version}`};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
t.is(errors[0].code, 'ETAGNOVERSION');
});
test.serial('Return "false" if the current branch is not the once configured', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
t.false(await verify(options, 'other', t.context.logger));
});
test.serial('Return "true" if all verification pass', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
t.true(await verify(options, 'master', t.context.logger));
});