feat: get last release with git tags

- Remove the `getLastRelease` plugin type
- Retrieve the last release based on Git tags
- Create the next release Git tag before calling the `publish` plugins

BREAKING CHANGE: Remove the `getLastRelease` plugin type

The `getLastRelease` plugins will not be called anymore.

BREAKING CHANGE: Git repository authentication is now mandatory

The Git authentication is now mandatory and must be set via `GH_TOKEN`, `GITHUB_TOKEN`,  `GL_TOKEN`, `GITLAB_TOKEN` or `GIT_CREDENTIALS` as described in [CI configuration](https://github.com/semantic-release/semantic-release/blob/caribou/docs/usage/ci-configuration.md#authentication).
This commit is contained in:
Pierre Vanduynslager
2018-01-27 16:50:29 -05:00
parent fb0caa005b
commit d0b304e240
29 changed files with 1069 additions and 1179 deletions
+80 -44
View File
@@ -1,6 +1,5 @@
const execa = require('execa');
const debug = require('debug')('semantic-release:get-version-head');
const {debugShell} = require('./debug');
/**
* Get the commit sha for a given tag.
@@ -10,45 +9,29 @@ const {debugShell} = require('./debug');
* @return {string} The commit sha of the tag in parameter or `null`.
*/
async function gitTagHead(tagName) {
try {
const shell = await execa('git', ['rev-list', '-1', tagName]);
debugShell('Get git tag head', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
return null;
}
return execa.stdout('git', ['rev-list', '-1', tagName], {reject: false});
}
/**
* Get the tag associated with a commit sha.
*
* @param {string} gitHead The commit sha for which to retrieve the associated tag.
*
* @return {string} The tag associatedwith the sha in parameter or `undefined`.
* @return {Array<String>} List of git tags.
* @throws {Error} If the `git` command fails.
*/
async function gitCommitTag(gitHead) {
try {
const shell = await execa('git', ['describe', '--tags', '--exact-match', gitHead]);
debugShell('Get git commit tag', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
return undefined;
}
async function gitTags() {
return (await execa.stdout('git', ['tag']))
.split('\n')
.map(tag => tag.trim())
.filter(tag => Boolean(tag));
}
/**
* Verify if the commit `sha` is in the direct history of the current branch.
* Verify if the `ref` is in the direct history of the current branch.
*
* @param {string} sha The sha of the commit to look for.
* @param {string} ref The reference to look for.
*
* @return {boolean} `true` if the commit `sha` is in the history of the current branch, `false` otherwise.
* @return {boolean} `true` if the reference is in the history of the current branch, `false` otherwise.
*/
async function isCommitInHistory(sha) {
const shell = await execa('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {reject: false});
debugShell('Check if commit is in history', shell, debug);
return shell.code === 0;
async function isRefInHistory(ref) {
return (await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], {reject: false})).code === 0;
}
/**
@@ -62,30 +45,83 @@ async function unshallow() {
* @return {string} the sha of the HEAD commit.
*/
async function gitHead() {
try {
const shell = await execa('git', ['rev-parse', 'HEAD']);
debugShell('Get git head', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
throw new Error(err.stderr);
}
return execa.stdout('git', ['rev-parse', 'HEAD']);
}
/**
* @return {string|undefined} The value of the remote git URL.
* @return {string} The value of the remote git URL.
*/
async function repoUrl() {
return (await execa.stdout('git', ['remote', 'get-url', 'origin'], {reject: false})) || undefined;
return execa.stdout('git', ['remote', 'get-url', 'origin'], {reject: false});
}
/**
* @return {Boolean} `true` if the current working directory is in a git repository, `false` otherwise.
*/
async function isGitRepo() {
const shell = await execa('git', ['rev-parse', '--git-dir'], {reject: false});
debugShell('Check if the current working directory is a git repository', shell, debug);
return shell.code === 0;
return (await execa('git', ['rev-parse', '--git-dir'], {reject: false})).code === 0;
}
module.exports = {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead, repoUrl, isGitRepo};
/**
* Verify the write access authorization to remote repository with push dry-run.
*
* @param {String} origin The remote repository URL.
* @param {String} branch The repositoru branch for which to verify write access.
*
* @return {Boolean} `true` is authorized to push, `false` otherwise.
*/
async function verifyAuth(origin, branch) {
return (await execa('git', ['push', '--dry-run', origin, `HEAD:${branch}`], {reject: false})).code === 0;
}
/**
* Tag the commit head on the local repository.
*
* @param {String} tagName The name of the tag.
* @throws {Error} if the tag creation failed.
*/
async function tag(tagName) {
await execa('git', ['tag', tagName]);
}
/**
* Push to the remote repository.
*
* @param {String} origin The remote repository URL.
* @param {String} branch The branch to push.
* @throws {Error} if the push failed.
*/
async function push(origin, branch) {
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]);
}
/**
* Delete a tag locally and remotely.
*
* @param {String} origin The remote repository URL.
* @param {String} tagName The tag name to delete.
* @throws {SemanticReleaseError} if the remote tag exists and references a commit that is not the local head commit.
*/
async function deleteTag(origin, tagName) {
// Delete the local tag
let shell = await execa('git', ['tag', '-d', tagName], {reject: false});
debug('delete local tag', shell);
// Delete the tag remotely
shell = await execa('git', ['push', '-d', origin, tagName], {reject: false});
debug('delete remote tag', shell);
}
module.exports = {
gitTagHead,
gitTags,
isRefInHistory,
unshallow,
gitHead,
repoUrl,
isGitRepo,
verifyAuth,
tag,
push,
deleteTag,
};