Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Usage:
.option('p', {alias: 'plugins', describe: 'Plugins', ...stringList, group: 'Options'})
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'})
.option('allow-outdated-branch', {
describe: 'Allow local branch to be behind remote',
type: 'boolean',
group: 'Options',
})
.option('verify-conditions', {...stringList, group: 'Plugins'})
.option('analyze-commits', {type: 'string', group: 'Plugins'})
.option('verify-release', {...stringList, group: 'Plugins'})
Expand Down
56 changes: 43 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@ const {extractErrors, makeTag} = require('./lib/utils');
const getGitAuthUrl = require('./lib/get-git-auth-url');
const getBranches = require('./lib/branches');
const getLogger = require('./lib/get-logger');
const {verifyAuth, isBranchUpToDate, getGitHead, tag, push, pushNotes, getTagHead, addNote} = require('./lib/git');
const {
verifyAuth,
isAncestor,
getNoMergeTags,
getGitHead,
getGitRemoteHead,
tag,
push,
pushNotes,
getTagHead,
addNote,
} = require('./lib/git');
const getError = require('./lib/get-error');
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');

Expand Down Expand Up @@ -81,25 +92,18 @@ async function run(context, plugins) {
);

try {
try {
await verifyAuth(options.repositoryUrl, context.branch.name, {cwd, env});
} catch (error) {
if (!(await isBranchUpToDate(options.repositoryUrl, context.branch.name, {cwd, env}))) {
logger.log(
`The local branch ${context.branch.name} is behind the remote one, therefore a new version won't be published.`
);
return false;
}

throw error;
}
await verifyAuth(options.repositoryUrl, context.branch.name, {cwd, env});
} catch (error) {
logger.error(`The command "${error.command}" failed with the error message ${error.stderr}.`);
throw getError('EGITNOPERMISSION', context);
}

logger.success(`Allowed to push to the Git repository`);

if (!(await validateBranch(context, {cwd, env}))) {
return false;
}

await plugins.verifyConditions(context);

const errors = [];
Expand Down Expand Up @@ -247,6 +251,32 @@ async function callFail(context, plugins, err) {
}
}

async function validateBranch(context, execaOptions) {
const localHead = await getGitHead(execaOptions);
const remoteHead = await getGitRemoteHead(context.options.repositoryUrl, context.branch.name, execaOptions);

if (!(await isAncestor(localHead, remoteHead, execaOptions))) {
throw getError('ELOCALCOMMIT', context);
}

if (!context.options.allowOutdatedBranch && localHead !== remoteHead) {
context.logger.log(
`The local branch ${context.branch.name} is behind the remote one, therefore a new version won't be published.`
);
return false;
}

const tagsNotLocal = await getNoMergeTags(localHead, execaOptions);
const tagsNotRemote = await getNoMergeTags(remoteHead, execaOptions);
const localMissingTags = tagsNotLocal.filter((value) => !tagsNotRemote.includes(value));

if (localMissingTags.length !== 0) {
throw getError('EREMOTETAG', context);
}

return true;
}

module.exports = async (cliOptions = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
const {unhook} = hookStd(
{silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
Expand Down
10 changes: 10 additions & 0 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,14 @@ The branch \`${name}\` head should be [reset](https://git-scm.com/docs/git-reset

See the [workflow configuration documentation](${linkify('docs/usage/workflow-configuration.md')}) for more details.`,
}),
EREMOTETAG: ({branch: {name}}) => ({
message: `The local branch \`${name}\` is missing remote tags.`,
details: `Only local branch that contains the same tags present on the remote can be released.
The branch \`${name}\` should fetch the remote commits.`,
}),
ELOCALCOMMIT: ({branch: {name}}) => ({
message: `The branch \`${name}\` has local commit.`,
details: `Only local branch with their head present on the remote can be released.
The branch \`${name}\` should be pushed to the remote.`,
}),
};
1 change: 1 addition & 0 deletions lib/get-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = async (context, cliOptions) => {
'@semantic-release/npm',
'@semantic-release/github',
],
allowOutdatedBranch: false,
// Remove `null` and `undefined` options so they can be replaced with default ones
...pickBy(options, (option) => !isNil(option)),
...(options.branches ? {branches: castArray(options.branches)} : {}),
Expand Down
59 changes: 49 additions & 10 deletions lib/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ async function getTags(branch, execaOptions) {
.filter(Boolean);
}

/**
* Get all the tags that are not merged for a given branch.
*
* @param {String} branch The branch for which to retrieve the tags.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Array<String>} List of git tags.
* @throws {Error} If the `git` command fails.
*/
async function getNoMergeTags(branch, execaOptions) {
return (await execa('git', ['tag', '--no-merged', branch], execaOptions)).stdout
.split('\n')
.map((tag) => tag.trim())
.filter(Boolean);
}

/**
* Retrieve a range of commits.
*
Expand Down Expand Up @@ -163,6 +179,21 @@ async function getGitHead(execaOptions) {
return (await execa('git', ['rev-parse', 'HEAD'], execaOptions)).stdout;
}

/**
* Get the remote HEAD sha.
*
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The repository branch for which to get the remote.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {String} the sha of the remote HEAD commit.
*/
async function getGitRemoteHead(repositoryUrl, branch, execaOptions) {
return (await execa('git', ['ls-remote', '--heads', repositoryUrl, branch], execaOptions)).stdout.match(
/^(?<ref>\w+)?/
)[1];
}

/**
* Get the repository remote URL.
*
Expand Down Expand Up @@ -204,7 +235,7 @@ async function isGitRepo(execaOptions) {
*/
async function verifyAuth(repositoryUrl, branch, execaOptions) {
try {
await execa('git', ['push', '--dry-run', '--no-verify', repositoryUrl, `HEAD:${branch}`], execaOptions);
await execa('git', ['push', '--dry-run', '--force', '--no-verify', repositoryUrl, `HEAD:${branch}`], execaOptions);
} catch (error) {
debug(error);
throw error;
Expand Down Expand Up @@ -281,19 +312,25 @@ async function verifyBranchName(branch, execaOptions) {
}

/**
* Verify the local branch is up to date with the remote one.
* Check if the first commit is an ancestor of the second commit.
*
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The repository branch for which to verify status.
* @param {String} first The commit to validate.
* @param {String} second The commit that should contain the first one.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, falsy otherwise.
*/
async function isBranchUpToDate(repositoryUrl, branch, execaOptions) {
return (
(await getGitHead(execaOptions)) ===
(await execa('git', ['ls-remote', '--heads', repositoryUrl, branch], execaOptions)).stdout.match(/^(?<ref>\w+)?/)[1]
);
async function isAncestor(first, second, execaOptions) {
try {
return (await execa('git', ['merge-base', '--is-ancestor', first, second], execaOptions)).exitCode === 0;
} catch (error) {
if (error.exitCode === 1) {
return false;
}

debug(error);
throw error;
}
}

/**
Expand Down Expand Up @@ -331,20 +368,22 @@ async function addNote(note, ref, execaOptions) {
module.exports = {
getTagHead,
getTags,
getNoMergeTags,
getCommits,
getBranches,
isRefExists,
fetch,
fetchNotes,
getGitHead,
getGitRemoteHead,
repoUrl,
isGitRepo,
verifyAuth,
tag,
push,
pushNotes,
verifyTagName,
isBranchUpToDate,
isAncestor,
verifyBranchName,
getNote,
addNote,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "semantic-release",
"description": "Automated semver compliant package publishing",
"version": "0.0.0-development",
"author": "Stephan Bönnemann <stephan@boennemann.me> (http://boennemann.me)",
"version": "1.0.0-custom",
"author": "Aditya Gupta",
"ava": {
"files": [
"test/**/*.test.js"
Expand All @@ -16,8 +16,7 @@
"url": "https://github.com/semantic-release/semantic-release/issues"
},
"contributors": [
"Gregor Martynus (https://twitter.com/gr2m)",
"Pierre Vanduynslager (https://twitter.com/@pvdlg_)"
"Aditya Gupta"
],
"dependencies": {
"@semantic-release/commit-analyzer": "^9.0.2",
Expand Down Expand Up @@ -79,7 +78,7 @@
"index.js",
"cli.js"
],
"homepage": "https://github.com/semantic-release/semantic-release#readme",
"homepage": "https://github.com/adityahex27/semantic-release#readme",
"keywords": [
"author",
"automation",
Expand Down Expand Up @@ -115,7 +114,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/semantic-release/semantic-release.git"
"url": "git+https://github.com/adityahex27/semantic-release.git"
},
"scripts": {
"codecov": "codecov -f coverage/coverage-final.json",
Expand Down
3 changes: 3 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ test.serial('Pass options to semantic-release API', async (t) => {
'fail2',
'--debug',
'-d',
'--allow-outdated-branch',
];
td.replace('..', run);
process.argv = argv;
Expand All @@ -93,6 +94,7 @@ test.serial('Pass options to semantic-release API', async (t) => {
t.deepEqual(run.args[0][0].fail, ['fail1', 'fail2']);
t.is(run.args[0][0].debug, true);
t.is(run.args[0][0].dryRun, true);
t.is(run.args[0][0].allowOutdatedBranch, true);

t.is(exitCode, 0);
});
Expand Down Expand Up @@ -171,6 +173,7 @@ test.serial('Do not set properties in option for which arg is not in command lin

await cli();

t.false('allow-outdated-branch' in run.args[0][0]);
t.false('ci' in run.args[0][0]);
t.false('d' in run.args[0][0]);
t.false('dry-run' in run.args[0][0]);
Expand Down
Loading