diff --git a/index.js b/index.js index b79a4dba..29a79f7d 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,7 @@ 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, getTagHead} = require('./lib/git'); +const {verifyAuth, isBranchUpToDate, getGitHead, tag, push, pushNotes, getTagHead, addNote} = require('./lib/git'); const getError = require('./lib/get-error'); const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants'); @@ -109,9 +109,10 @@ async function run(context, plugins) { if (options.dryRun) { logger.warn(`Skip ${nextRelease.gitTag} tag creation in dry-run mode`); } else { - await tag(nextRelease.gitTag, nextRelease.gitHead, {cwd, env}); + await addNote({channels: [...currentRelease.channels, nextRelease.channel]}, nextRelease.gitHead, {cwd, env}); await push(options.repositoryUrl, {cwd, env}); - logger.success(`Created tag ${nextRelease.gitTag}`); + await pushNotes(options.repositoryUrl, {cwd, env}); + logger.success(`Add channel ${nextRelease.channel} to tag ${nextRelease.gitTag}`); } context.branch.tags.push({ @@ -148,7 +149,7 @@ async function run(context, plugins) { const nextRelease = { type: await plugins.analyzeCommits(context), - channel: context.branch.channel, + channel: context.branch.channel || null, gitHead: await getGitHead({cwd, env}), }; if (!nextRelease.type) { @@ -158,8 +159,8 @@ async function run(context, plugins) { context.nextRelease = nextRelease; nextRelease.version = getNextVersion(context); - nextRelease.gitTag = makeTag(options.tagFormat, nextRelease.version, nextRelease.channel); - nextRelease.name = makeTag(options.tagFormat, nextRelease.version); + nextRelease.gitTag = makeTag(options.tagFormat, nextRelease.version); + nextRelease.name = nextRelease.gitTag; if (context.branch.type !== 'prerelease' && !semver.satisfies(nextRelease.version, context.branch.range)) { throw getError('EINVALIDNEXTVERSION', { @@ -181,7 +182,9 @@ async function run(context, plugins) { } else { // Create the tag before calling the publish plugins as some require the tag to exists await tag(nextRelease.gitTag, nextRelease.gitHead, {cwd, env}); + await addNote({channels: [nextRelease.channel]}, nextRelease.gitHead, {cwd, env}); await push(options.repositoryUrl, {cwd, env}); + await pushNotes(options.repositoryUrl, {cwd, env}); logger.success(`Created tag ${nextRelease.gitTag}`); } diff --git a/lib/branches/get-tags.js b/lib/branches/get-tags.js index bf26a331..8cffbeb4 100644 --- a/lib/branches/get-tags.js +++ b/lib/branches/get-tags.js @@ -1,34 +1,29 @@ -const {template, escapeRegExp, flatMap} = require('lodash'); +const {template, escapeRegExp} = require('lodash'); const semver = require('semver'); const pReduce = require('p-reduce'); const debug = require('debug')('semantic-release:get-tags'); -const {getTags} = require('../../lib/git'); +const {getTags, getNote} = require('../../lib/git'); module.exports = async ({cwd, env, options: {tagFormat}}, branches) => { // Generate a regex to parse tags formatted with `tagFormat` // by replacing the `version` variable in the template by `(.+)`. // The `tagFormat` is compiled with space as the `version` as it's an invalid tag character, // so it's guaranteed to no be present in the `tagFormat`. - const tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.[^@]+)@?(.+)?')}`; + const tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.+)')}`; return pReduce( branches, async (branches, branch) => { - const versions = (await getTags(branch.name, {cwd, env})).reduce((versions, tag) => { - const [, version, channel] = tag.match(tagRegexp) || []; - if (version && semver.valid(semver.clean(version))) { - return { - ...versions, - [version]: versions[version] - ? {...versions[version], channels: [...versions[version].channels, channel]} - : {gitTag: tag, version, channels: [channel]}, - }; - } - - return versions; - }, {}); - - const branchTags = flatMap(versions); + const branchTags = await pReduce( + await getTags(branch.name, {cwd, env}), + async (branchTags, tag) => { + const [, version] = tag.match(tagRegexp) || []; + return version && semver.valid(semver.clean(version)) + ? [...branchTags, {gitTag: tag, version, channels: (await getNote(tag, {cwd, env})).channels || [null]}] + : branchTags; + }, + [] + ); debug('found tags for branch %s: %o', branch.name, branchTags); return [...branches, {...branch, tags: branchTags}]; diff --git a/lib/branches/index.js b/lib/branches/index.js index 044289e8..b3781f09 100644 --- a/lib/branches/index.js +++ b/lib/branches/index.js @@ -3,7 +3,7 @@ const AggregateError = require('aggregate-error'); const pEachSeries = require('p-each-series'); const DEFINITIONS = require('../definitions/branches'); const getError = require('../get-error'); -const {fetch, verifyBranchName} = require('../git'); +const {fetch, fetchNotes, verifyBranchName} = require('../git'); const expand = require('./expand'); const getTags = require('./get-tags'); const normalize = require('./normalize'); @@ -21,6 +21,8 @@ module.exports = async (repositoryUrl, context) => { await fetch(repositoryUrl, name, {cwd, env}); }); + await fetchNotes(repositoryUrl, {cwd, env}); + const branches = await getTags(context, remoteBranches); const errors = []; diff --git a/lib/definitions/constants.js b/lib/definitions/constants.js index 491a1a71..999999c9 100644 --- a/lib/definitions/constants.js +++ b/lib/definitions/constants.js @@ -14,6 +14,8 @@ const SECRET_REPLACEMENT = '[secure]'; const SECRET_MIN_SIZE = 5; +const GIT_NOTE_REF = 'semantic-release'; + module.exports = { RELEASE_TYPE, FIRST_RELEASE, @@ -23,4 +25,5 @@ module.exports = { RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT, SECRET_MIN_SIZE, + GIT_NOTE_REF, }; diff --git a/lib/get-release-to-add.js b/lib/get-release-to-add.js index 09cefc82..a5ac7d88 100644 --- a/lib/get-release-to-add.js +++ b/lib/get-release-to-add.js @@ -23,12 +23,12 @@ module.exports = context => { .slice(branches.findIndex(({name}) => name === branch.name) + 1) // Exclude prerelease branches .filter(({type}) => type !== 'prerelease') - .map(({channel}) => channel); + .map(({channel}) => channel || null); const versiontoAdd = uniqBy( branch.tags.filter( ({channels, version}) => - !channels.includes(branch.channel) && + !channels.includes(branch.channel || null) && intersection(channels, higherChannels).length > 0 && (branch.type !== 'maintenance' || semver.gte(version, getLowerBound(branch.mergeRange))) ), @@ -50,8 +50,8 @@ module.exports = context => { nextRelease: { type, version, - channel: branch.channel, - gitTag: makeTag(tagFormat, version, branch.channel), + channel: branch.channel || null, + gitTag: makeTag(tagFormat, version), name, gitHead: gitTag, }, diff --git a/lib/git.js b/lib/git.js index 58304df6..0c2d7836 100644 --- a/lib/git.js +++ b/lib/git.js @@ -2,6 +2,7 @@ const gitLogParser = require('git-log-parser'); const getStream = require('get-stream'); const execa = require('execa'); const debug = require('debug')('semantic-release:git'); +const {GIT_NOTE_REF} = require('./definitions/constants'); Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}}); @@ -146,6 +147,27 @@ async function fetch(repositoryUrl, branch, execaOpts) { } } +/** + * Unshallow the git repository if necessary and fetch all the notes. + * + * @param {String} repositoryUrl The remote repository URL. + * @param {Object} [execaOpts] Options to pass to `execa`. + */ +async function fetchNotes(repositoryUrl, execaOpts) { + try { + await execa( + 'git', + ['fetch', '--unshallow', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`], + execaOpts + ); + } catch (_) { + await execa('git', ['fetch', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`], { + ...execaOpts, + reject: false, + }); + } +} + /** * Get the HEAD sha. * @@ -230,6 +252,18 @@ async function push(repositoryUrl, execaOpts) { await execa('git', ['push', '--tags', repositoryUrl], execaOpts); } +/** + * Push notes to the remote repository. + * + * @param {String} repositoryUrl The remote repository URL. + * @param {Object} [execaOpts] Options to pass to `execa`. + * + * @throws {Error} if the push failed. + */ +async function pushNotes(repositoryUrl, execaOpts) { + await execa('git', ['push', repositoryUrl, `refs/notes/${GIT_NOTE_REF}`], execaOpts); +} + /** * Verify a tag name is a valid Git reference. * @@ -280,6 +314,38 @@ async function isBranchUpToDate(repositoryUrl, branch, execaOpts) { } } +/** + * Get and parse the JSON note of a given reference. + * + * @param {String} ref The Git reference for which to retrieve the note. + * @param {Object} [execaOpts] Options to pass to `execa`. + * + * @return {Object} the parsed JSON note if there is one, an empty object otherwise. + */ +async function getNote(ref, execaOpts) { + try { + return JSON.parse((await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'show', ref], execaOpts)).stdout); + } catch (error) { + if (error.exitCode === 1) { + return {}; + } + + debug(error); + throw error; + } +} + +/** + * Get and parse the JSON note of a given reference. + * + * @param {Object} note The object to save in the reference note. + * @param {String} ref The Git reference to add the note to. + * @param {Object} [execaOpts] Options to pass to `execa`. + */ +async function addNote(note, ref, execaOpts) { + await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'add', '-f', '-m', JSON.stringify(note), ref], execaOpts); +} + module.exports = { getTagHead, getTags, @@ -288,13 +354,17 @@ module.exports = { isRefInHistory, isRefExists, fetch, + fetchNotes, getGitHead, repoUrl, isGitRepo, verifyAuth, tag, push, + pushNotes, verifyTagName, isBranchUpToDate, verifyBranchName, + getNote, + addNote, }; diff --git a/lib/utils.js b/lib/utils.js index ef8463f7..e40c5d19 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -67,8 +67,8 @@ function getRange(min, max) { return `>=${min}${max ? ` <${max}` : ''}`; } -function makeTag(tagFormat, version, channel) { - return template(tagFormat)({version: `${version}${channel ? `@${channel}` : ''}`}); +function makeTag(tagFormat, version) { + return template(tagFormat)({version}); } function isSameChannel(channel, otherChannel) { diff --git a/test/branches/get-tags.test.js b/test/branches/get-tags.test.js index c234ebe5..7321f923 100644 --- a/test/branches/get-tags.test.js +++ b/test/branches/get-tags.test.js @@ -1,6 +1,6 @@ import test from 'ava'; import getTags from '../../lib/branches/get-tags'; -import {gitRepo, gitCommits, gitTagVersion, gitCheckout} from '../helpers/git-utils'; +import {gitRepo, gitCommits, gitTagVersion, gitCheckout, gitAddNote} from '../helpers/git-utils'; test('Get the valid tags', async t => { const {cwd} = await gitRepo(); @@ -20,9 +20,9 @@ test('Get the valid tags', async t => { { name: 'master', tags: [ - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined]}, - {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined]}, - {gitTag: 'v3.0.0-beta.1', version: '3.0.0-beta.1', channels: [undefined]}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: [null]}, + {gitTag: 'v3.0.0-beta.1', version: '3.0.0-beta.1', channels: [null]}, ], }, ]); @@ -30,20 +30,21 @@ test('Get the valid tags', async t => { test('Get the valid tags from multiple branches', async t => { const {cwd} = await gitRepo(); - const commits = await gitCommits(['First'], {cwd}); + await gitCommits(['First'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); - await gitTagVersion('v1.0.0@1.x', undefined, {cwd}); - commits.push(...(await gitCommits(['Second'], {cwd}))); + await gitAddNote(JSON.stringify({channels: [null, '1.x']}), 'v1.0.0', {cwd}); + await gitCommits(['Second'], {cwd}); await gitTagVersion('v1.1.0', undefined, {cwd}); - await gitTagVersion('v1.1.0@1.x', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: [null, '1.x']}), 'v1.1.0', {cwd}); await gitCheckout('1.x', true, {cwd}); await gitCheckout('master', false, {cwd}); - commits.push(...(await gitCommits(['Third'], {cwd}))); + await gitCommits(['Third'], {cwd}); await gitTagVersion('v2.0.0', undefined, {cwd}); - await gitTagVersion('v2.0.0@next', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v2.0.0', {cwd}); await gitCheckout('next', true, {cwd}); - commits.push(...(await gitCommits(['Fourth'], {cwd}))); - await gitTagVersion('v3.0.0@next', undefined, {cwd}); + await gitCommits(['Fourth'], {cwd}); + await gitTagVersion('v3.0.0', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: ['next']}), 'v3.0.0', {cwd}); const result = await getTags({cwd, options: {tagFormat: `v\${version}`}}, [ {name: '1.x'}, @@ -55,39 +56,17 @@ test('Get the valid tags from multiple branches', async t => { { name: '1.x', tags: [ - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined, '1.x']}, - {gitTag: 'v1.1.0', version: '1.1.0', channels: [undefined, '1.x']}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null, '1.x']}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: [null, '1.x']}, ], }, { name: 'master', - tags: [...result[0].tags, {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined, 'next']}], + tags: [...result[0].tags, {gitTag: 'v2.0.0', version: '2.0.0', channels: [null, 'next']}], }, { name: 'next', - tags: [...result[1].tags, {gitTag: 'v3.0.0@next', version: '3.0.0', channels: ['next']}], - }, - ]); -}); - -test('Match the tag name from the begining of the string and the channel from the last "@"', async t => { - const {cwd} = await gitRepo(); - await gitCommits(['First'], {cwd}); - await gitTagVersion('prefix@v1.0.0', undefined, {cwd}); - await gitTagVersion('prefix@v1.0.0@next', undefined, {cwd}); - await gitTagVersion('prefix@v2.0.0', undefined, {cwd}); - await gitTagVersion('prefix@v2.0.0@next', undefined, {cwd}); - await gitTagVersion('other-prefix@v3.0.0', undefined, {cwd}); - - const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}]); - - t.deepEqual(result, [ - { - name: 'master', - tags: [ - {gitTag: 'prefix@v1.0.0', version: '1.0.0', channels: [undefined, 'next']}, - {gitTag: 'prefix@v2.0.0', version: '2.0.0', channels: [undefined, 'next']}, - ], + tags: [...result[1].tags, {gitTag: 'v3.0.0', version: '3.0.0', channels: ['next']}], }, ]); }); @@ -112,11 +91,13 @@ test('Return branches with and empty tags array if no valid tag is found in hist await gitCheckout('next', true, {cwd}); await gitCommits(['Second'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); - await gitTagVersion('v1.0.0@next', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v1.0.0', {cwd}); + await gitCommits(['Third'], {cwd}); await gitTagVersion('v2.0.0', undefined, {cwd}); - await gitTagVersion('v2.0.0@next', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v2.0.0', {cwd}); + await gitCommits(['Fourth'], {cwd}); await gitTagVersion('v3.0.0', undefined, {cwd}); - await gitTagVersion('v3.0.0@next', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v3.0.0', {cwd}); await gitCheckout('master', false, {cwd}); const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}, {name: 'next'}]); @@ -133,19 +114,19 @@ test('Get the highest valid tag corresponding to the "tagFormat"', async t => { await gitTagVersion('1.0.0', undefined, {cwd}); t.deepEqual(await getTags({cwd, options: {tagFormat: `\${version}`}}, [{name: 'master'}]), [ - {name: 'master', tags: [{gitTag: '1.0.0', version: '1.0.0', channels: [undefined]}]}, + {name: 'master', tags: [{gitTag: '1.0.0', version: '1.0.0', channels: [null]}]}, ]); await gitTagVersion('foo-1.0.0-bar', undefined, {cwd}); t.deepEqual(await getTags({cwd, options: {tagFormat: `foo-\${version}-bar`}}, [{name: 'master'}]), [ - {name: 'master', tags: [{gitTag: 'foo-1.0.0-bar', version: '1.0.0', channels: [undefined]}]}, + {name: 'master', tags: [{gitTag: 'foo-1.0.0-bar', version: '1.0.0', channels: [null]}]}, ]); await gitTagVersion('foo-v1.0.0-bar', undefined, {cwd}); t.deepEqual(await getTags({cwd, options: {tagFormat: `foo-v\${version}-bar`}}, [{name: 'master'}]), [ { name: 'master', - tags: [{gitTag: 'foo-v1.0.0-bar', version: '1.0.0', channels: [undefined]}], + tags: [{gitTag: 'foo-v1.0.0-bar', version: '1.0.0', channels: [null]}], }, ]); @@ -153,7 +134,7 @@ test('Get the highest valid tag corresponding to the "tagFormat"', async t => { t.deepEqual(await getTags({cwd, options: {tagFormat: `(.+)/\${version}/(a-z)`}}, [{name: 'master'}]), [ { name: 'master', - tags: [{gitTag: '(.+)/1.0.0/(a-z)', version: '1.0.0', channels: [undefined]}], + tags: [{gitTag: '(.+)/1.0.0/(a-z)', version: '1.0.0', channels: [null]}], }, ]); @@ -161,12 +142,12 @@ test('Get the highest valid tag corresponding to the "tagFormat"', async t => { t.deepEqual(await getTags({cwd, options: {tagFormat: `2.0.0-\${version}-bar.1`}}, [{name: 'master'}]), [ { name: 'master', - tags: [{gitTag: '2.0.0-1.0.0-bar.1', version: '1.0.0', channels: [undefined]}], + tags: [{gitTag: '2.0.0-1.0.0-bar.1', version: '1.0.0', channels: [null]}], }, ]); await gitTagVersion('3.0.0-bar.2', undefined, {cwd}); t.deepEqual(await getTags({cwd, options: {tagFormat: `\${version}-bar.2`}}, [{name: 'master'}]), [ - {name: 'master', tags: [{gitTag: '3.0.0-bar.2', version: '3.0.0', channels: [undefined]}]}, + {name: 'master', tags: [{gitTag: '3.0.0-bar.2', version: '3.0.0', channels: [null]}]}, ]); }); diff --git a/test/get-last-release.test.js b/test/get-last-release.test.js index fa62e200..bb3a2ee2 100644 --- a/test/get-last-release.test.js +++ b/test/get-last-release.test.js @@ -8,7 +8,7 @@ test('Get the highest non-prerelease valid tag', t => { tags: [ {version: '2.0.0', gitTag: 'v2.0.0', gitHead: 'v2.0.0'}, {version: '1.0.0', gitTag: 'v1.0.0', gitHead: 'v1.0.0'}, - {version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1@beta', gitHead: 'v3.0.0-beta.1@beta'}, + {version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1', gitHead: 'v3.0.0-beta.1'}, ], type: 'release', }, @@ -25,14 +25,9 @@ test('Get the highest prerelease valid tag, ignoring other tags from other prere prerelease: 'beta', channel: 'beta', tags: [ - {version: '1.0.0-beta.1', gitTag: 'v1.0.0-beta.1@beta', gitHead: 'v1.0.0-beta.1@beta', channels: ['beta']}, - {version: '1.0.0-beta.2', gitTag: 'v1.0.0-beta.2@beta', gitHead: 'v1.0.0-beta.2@beta', channels: ['beta']}, - { - version: '1.0.0-alpha.1', - gitTag: 'v1.0.0-alpha.1@alpha', - gitHead: 'v1.0.0-alpha.1@alpha', - channels: ['alpha'], - }, + {version: '1.0.0-beta.1', gitTag: 'v1.0.0-beta.1', gitHead: 'v1.0.0-beta.1', channels: ['beta']}, + {version: '1.0.0-beta.2', gitTag: 'v1.0.0-beta.2', gitHead: 'v1.0.0-beta.2', channels: ['beta']}, + {version: '1.0.0-alpha.1', gitTag: 'v1.0.0-alpha.1', gitHead: 'v1.0.0-alpha.1', channels: ['alpha']}, ], type: 'prerelease', }, @@ -41,9 +36,9 @@ test('Get the highest prerelease valid tag, ignoring other tags from other prere t.deepEqual(result, { version: '1.0.0-beta.2', - gitTag: 'v1.0.0-beta.2@beta', + gitTag: 'v1.0.0-beta.2', name: 'v1.0.0-beta.2', - gitHead: 'v1.0.0-beta.2@beta', + gitHead: 'v1.0.0-beta.2', channels: ['beta'], }); }); @@ -52,7 +47,7 @@ test('Return empty object if no valid tag is found', t => { const result = getLastRelease({ branch: { name: 'master', - tags: [{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1@beta', gitHead: 'v3.0.0-beta.1@beta'}], + tags: [{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1', gitHead: 'v3.0.0-beta.1'}], type: 'release', }, options: {tagFormat: `v\${version}`}, @@ -70,7 +65,7 @@ test('Get the highest non-prerelease valid tag before a certain version', t => { tags: [ {version: '2.0.0', gitTag: 'v2.0.0', gitHead: 'v2.0.0'}, {version: '1.0.0', gitTag: 'v1.0.0', gitHead: 'v1.0.0'}, - {version: '2.0.0-beta.1', gitTag: 'v2.0.0-beta.1@beta', gitHead: 'v2.0.0-beta.1@beta'}, + {version: '2.0.0-beta.1', gitTag: 'v2.0.0-beta.1', gitHead: 'v2.0.0-beta.1'}, {version: '2.1.0', gitTag: 'v2.1.0', gitHead: 'v2.1.0'}, {version: '2.1.1', gitTag: 'v2.1.1', gitHead: 'v2.1.1'}, ], diff --git a/test/get-release-to-add.test.js b/test/get-release-to-add.test.js index 2b131113..823e2817 100644 --- a/test/get-release-to-add.test.js +++ b/test/get-release-to-add.test.js @@ -9,12 +9,12 @@ test('Return versions merged from release to maintenance branch, excluding lower type: 'maintenance', mergeRange: '>=2.0.0 <3.0.0', tags: [ - {gitTag: 'v2.0.0@2.x', version: '2.0.0', channels: ['2.x']}, - {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined]}, - {gitTag: 'v2.1.0', version: '2.1.0', channels: [undefined]}, - {gitTag: 'v2.1.1', version: '2.1.1', channels: [undefined]}, - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined]}, - {gitTag: 'v1.1.0', version: '1.1.0', channels: [undefined]}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: ['2.x']}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: [null]}, + {gitTag: 'v2.1.0', version: '2.1.0', channels: [null]}, + {gitTag: 'v2.1.1', version: '2.1.1', channels: [null]}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: [null]}, ], }, branches: [{name: '2.x', channel: '2.x'}, {name: 'master'}], @@ -22,11 +22,11 @@ test('Return versions merged from release to maintenance branch, excluding lower }); t.deepEqual(result, { - lastRelease: {version: '2.1.0', channels: [undefined], gitTag: 'v2.1.0', name: 'v2.1.0', gitHead: 'v2.1.0'}, + lastRelease: {version: '2.1.0', channels: [null], gitTag: 'v2.1.0', name: 'v2.1.0', gitHead: 'v2.1.0'}, currentRelease: { type: 'patch', version: '2.1.1', - channels: [undefined], + channels: [null], gitTag: 'v2.1.1', name: 'v2.1.1', gitHead: 'v2.1.1', @@ -35,7 +35,7 @@ test('Return versions merged from release to maintenance branch, excluding lower type: 'patch', version: '2.1.1', channel: '2.x', - gitTag: 'v2.1.1@2.x', + gitTag: 'v2.1.1', name: 'v2.1.1', gitHead: 'v2.1.1', }, @@ -47,9 +47,9 @@ test('Return versions merged between release branches', t => { branch: { name: 'master', tags: [ - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined, 'next']}, - {gitTag: 'v1.1.0@next', version: '1.1.0', channels: ['next']}, - {gitTag: 'v2.0.0@next-major', version: '2.0.0', channels: ['next-major']}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null, 'next']}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: ['next']}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: ['next-major']}, ], }, branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}], @@ -59,26 +59,26 @@ test('Return versions merged between release branches', t => { t.deepEqual(result, { lastRelease: { version: '1.1.0', - gitTag: 'v1.1.0@next', + gitTag: 'v1.1.0', name: 'v1.1.0', - gitHead: 'v1.1.0@next', + gitHead: 'v1.1.0', channels: ['next'], }, currentRelease: { type: 'major', version: '2.0.0', channels: ['next-major'], - gitTag: 'v2.0.0@next-major', + gitTag: 'v2.0.0', name: 'v2.0.0', - gitHead: 'v2.0.0@next-major', + gitHead: 'v2.0.0', }, nextRelease: { type: 'major', version: '2.0.0', - channel: undefined, + channel: null, gitTag: 'v2.0.0', name: 'v2.0.0', - gitHead: 'v2.0.0@next-major', + gitHead: 'v2.0.0', }, }); }); @@ -88,10 +88,9 @@ test('Return releases sorted by ascending order', t => { branch: { name: 'master', tags: [ - {gitTag: 'v2.0.0@next-major', version: '2.0.0', channels: ['next-major']}, - {gitTag: 'v1.1.0@next', version: '1.1.0', channels: ['next']}, - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined, 'next']}, - // {gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next'}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: ['next-major']}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: ['next']}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null, 'next']}, ], }, branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}], @@ -99,22 +98,22 @@ test('Return releases sorted by ascending order', t => { }); t.deepEqual(result, { - lastRelease: {version: '1.1.0', gitTag: 'v1.1.0@next', name: 'v1.1.0', gitHead: 'v1.1.0@next', channels: ['next']}, + lastRelease: {version: '1.1.0', gitTag: 'v1.1.0', name: 'v1.1.0', gitHead: 'v1.1.0', channels: ['next']}, currentRelease: { type: 'major', version: '2.0.0', channels: ['next-major'], - gitTag: 'v2.0.0@next-major', + gitTag: 'v2.0.0', name: 'v2.0.0', - gitHead: 'v2.0.0@next-major', + gitHead: 'v2.0.0', }, nextRelease: { type: 'major', version: '2.0.0', - channel: undefined, + channel: null, gitTag: 'v2.0.0', name: 'v2.0.0', - gitHead: 'v2.0.0@next-major', + gitHead: 'v2.0.0', }, }); }); @@ -123,7 +122,7 @@ test('No lastRelease', t => { const result = getReleaseToAdd({ branch: { name: 'master', - tags: [{gitTag: 'v1.0.0@next', version: '1.0.0', channels: ['next']}], + tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: ['next']}], }, branches: [{name: 'master'}, {name: 'next', channel: 'next'}], options: {tagFormat: `v\${version}`}, @@ -135,17 +134,17 @@ test('No lastRelease', t => { type: 'major', version: '1.0.0', channels: ['next'], - gitTag: 'v1.0.0@next', + gitTag: 'v1.0.0', name: 'v1.0.0', - gitHead: 'v1.0.0@next', + gitHead: 'v1.0.0', }, nextRelease: { type: 'major', version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', name: 'v1.0.0', - gitHead: 'v1.0.0@next', + gitHead: 'v1.0.0', }, }); }); @@ -155,9 +154,9 @@ test('Ignore pre-release versions', t => { branch: { name: 'master', tags: [ - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined, 'next']}, - {gitTag: 'v1.1.0@next', version: '1.1.0', channels: ['next']}, - {gitTag: 'v2.0.0-alpha.1@alpha', version: '2.0.0-alpha.1', channels: ['alpha']}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null, 'next']}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: ['next']}, + {gitTag: 'v2.0.0-alpha.1', version: '2.0.0-alpha.1', channels: ['alpha']}, ], }, branches: [ @@ -169,22 +168,22 @@ test('Ignore pre-release versions', t => { }); t.deepEqual(result, { - lastRelease: {version: '1.0.0', channels: [undefined, 'next'], gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: 'v1.0.0'}, + lastRelease: {version: '1.0.0', channels: [null, 'next'], gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: 'v1.0.0'}, currentRelease: { type: 'minor', version: '1.1.0', channels: ['next'], - gitTag: 'v1.1.0@next', + gitTag: 'v1.1.0', name: 'v1.1.0', - gitHead: 'v1.1.0@next', + gitHead: 'v1.1.0', }, nextRelease: { type: 'minor', version: '1.1.0', - channel: undefined, + channel: null, gitTag: 'v1.1.0', name: 'v1.1.0', - gitHead: 'v1.1.0@next', + gitHead: 'v1.1.0', }, }); }); @@ -197,12 +196,12 @@ test('Exclude versions merged from release to maintenance branch if they have th type: 'maintenance', mergeRange: '>=2.0.0 <3.0.0', tags: [ - {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined]}, - {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined]}, - {gitTag: 'v2.1.0', version: '2.1.0', channels: [undefined]}, - {gitTag: 'v2.1.1', version: '2.1.1', channels: [undefined]}, - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined]}, - {gitTag: 'v1.1.0', version: '1.1.0', channels: [undefined]}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: [null]}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: [null]}, + {gitTag: 'v2.1.0', version: '2.1.0', channels: [null]}, + {gitTag: 'v2.1.1', version: '2.1.1', channels: [null]}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: [null]}, ], }, branches: [ @@ -243,9 +242,9 @@ test('Exclude versions merged between release branches if they all have "channel name: 'master', channel: false, tags: [ - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined]}, - {gitTag: 'v1.1.0', version: '1.1.0', channels: [undefined]}, - {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined]}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: [null]}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: [null]}, ], }, branches: [ @@ -267,12 +266,12 @@ test('Exclude versions number less than the latest version already released on t type: 'maintenance', mergeRange: '>=2.0.0 <3.0.0', tags: [ - {gitTag: 'v2.0.0@2.x', version: '2.0.0', channels: ['2.x']}, - {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined]}, - {gitTag: 'v2.1.0', version: '2.1.0', channels: [undefined]}, - {gitTag: 'v2.1.1', version: '2.1.1', channels: [undefined, '2.x']}, - {gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined]}, - {gitTag: 'v1.1.0', version: '1.1.0', channels: [undefined]}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: ['2.x']}, + {gitTag: 'v2.0.0', version: '2.0.0', channels: [null]}, + {gitTag: 'v2.1.0', version: '2.1.0', channels: [null]}, + {gitTag: 'v2.1.1', version: '2.1.1', channels: [null, '2.x']}, + {gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}, + {gitTag: 'v1.1.0', version: '1.1.0', channels: [null]}, ], }, branches: [{name: '2.x', channel: '2.x'}, {name: 'master'}], diff --git a/test/git.test.js b/test/git.test.js index 7383f156..9df6d03c 100644 --- a/test/git.test.js +++ b/test/git.test.js @@ -14,6 +14,9 @@ import { isGitRepo, verifyTagName, isBranchUpToDate, + getNote, + addNote, + fetchNotes, } from '../lib/git'; import { gitRepo, @@ -27,6 +30,8 @@ import { gitRemoteTagHead, gitPush, gitDetachedHead, + gitAddNote, + gitGetNote, } from './helpers/git-utils'; test('Get the last commit sha', async t => { @@ -276,3 +281,91 @@ test('Return "true" if local repository is ahead', async t => { t.true(await isBranchUpToDate(repositoryUrl, 'master', {cwd})); }); + +test('Get a commit note', async t => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + // Add commits to the master branch + const commits = await gitCommits(['First'], {cwd}); + + await gitAddNote(JSON.stringify({note: 'note'}), commits[0].hash, {cwd}); + + t.deepEqual(await getNote(commits[0].hash, {cwd}), {note: 'note'}); +}); + +test('Return empty object if there is no commit note', async t => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + // Add commits to the master branch + const commits = await gitCommits(['First'], {cwd}); + + t.deepEqual(await getNote(commits[0].hash, {cwd}), {}); +}); + +test('Throw error if a commit note in invalid', async t => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + // Add commits to the master branch + const commits = await gitCommits(['First'], {cwd}); + + await gitAddNote('non-json note', commits[0].hash, {cwd}); + + await t.throwsAsync(getNote(commits[0].hash, {cwd})); +}); + +test('Add a commit note', async t => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + // Add commits to the master branch + const commits = await gitCommits(['First'], {cwd}); + + await addNote({note: 'note'}, commits[0].hash, {cwd}); + + t.is(await gitGetNote(commits[0].hash, {cwd}), '{"note":"note"}'); +}); + +test('Overwrite a commit note', async t => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + // Add commits to the master branch + const commits = await gitCommits(['First'], {cwd}); + + await addNote({note: 'note'}, commits[0].hash, {cwd}); + await addNote({note: 'note2'}, commits[0].hash, {cwd}); + + t.is(await gitGetNote(commits[0].hash, {cwd}), '{"note":"note2"}'); +}); + +test('Unshallow and fetch repository with notes', async t => { + // Create a git repository, set the current working directory at the root of the repo + let {cwd, repositoryUrl} = await gitRepo(); + // Add commits to the master branch + const commits = await gitCommits(['First', 'Second'], {cwd}); + await gitAddNote(JSON.stringify({note: 'note'}), commits[0].hash, {cwd}); + // Create a shallow clone with only 1 commit + cwd = await gitShallowClone(repositoryUrl); + + // Verify the shallow clone doesn't contains the note + await t.throwsAsync(gitGetNote(commits[0].hash, {cwd})); + + await fetch(repositoryUrl, 'master', {cwd}); + await fetchNotes(repositoryUrl, {cwd}); + + // Verify the shallow clone contains the note + t.is(await gitGetNote(commits[0].hash, {cwd}), '{"note":"note"}'); +}); + +test('Fetch all notes on a detached head repository', async t => { + let {cwd, repositoryUrl} = await gitRepo(); + + await gitCommits(['First'], {cwd}); + const [commit] = await gitCommits(['Second'], {cwd}); + await gitPush(repositoryUrl, 'master', {cwd}); + await gitAddNote(JSON.stringify({note: 'note'}), commit.hash, {cwd}); + cwd = await gitDetachedHead(repositoryUrl, commit.hash); + + await fetch(repositoryUrl, 'master', {cwd}); + await fetchNotes(repositoryUrl, {cwd}); + + t.is(await gitGetNote(commit.hash, {cwd}), '{"note":"note"}'); +}); diff --git a/test/helpers/git-utils.js b/test/helpers/git-utils.js index 0473bb3d..b5ea65b3 100644 --- a/test/helpers/git-utils.js +++ b/test/helpers/git-utils.js @@ -4,6 +4,7 @@ import fileUrl from 'file-url'; import pEachSeries from 'p-each-series'; import gitLogParser from 'git-log-parser'; import getStream from 'get-stream'; +import {GIT_NOTE_REF} from '../../lib/definitions/constants'; /** * Commit message informations. @@ -258,3 +259,24 @@ export async function mergeFf(ref, execaOpts) { export async function rebase(ref, execaOpts) { await execa('git', ['rebase', ref], execaOpts); } + +/** + * Add a note to a Git reference. + * + * @param {String} note The note to add. + * @param {String} ref The ref to add the note to. + * @param {Object} [execaOpts] Options to pass to `execa`. + */ +export async function gitAddNote(note, ref, execaOpts) { + await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'add', '-m', note, ref], execaOpts); +} + +/** + * Get the note associated with a Git reference. + * + * @param {String} ref The ref to get the note from. + * @param {Object} [execaOpts] Options to pass to `execa`. + */ +export async function gitGetNote(ref, execaOpts) { + return (await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'show', ref], execaOpts)).stdout; +} diff --git a/test/index.test.js b/test/index.test.js index 11f35ca2..0c3d867d 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -19,6 +19,8 @@ import { merge, mergeFf, rebase, + gitAddNote, + gitGetNote, } from './helpers/git-utils'; const requireNoCache = proxyquire.noPreserveCache(); @@ -45,7 +47,8 @@ test('Plugins are called with expected values', async t => { // 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@next', undefined, {cwd}); + 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}); @@ -55,7 +58,7 @@ test('Plugins are called with expected values', async t => { const lastRelease = { version: '1.0.0', gitHead: commits[commits.length - 1].hash, - gitTag: 'v1.0.0@next', + gitTag: 'v1.0.0', name: 'v1.0.0', channels: ['next'], }; @@ -65,7 +68,7 @@ test('Plugins are called with expected values', async t => { version: '1.1.0', gitHead: await getGitHead({cwd}), gitTag: 'v1.1.0', - channel: undefined, + channel: null, }; const notes1 = 'Release notes 1'; const notes2 = 'Release notes 2'; @@ -96,7 +99,7 @@ test('Plugins are called with expected values', async t => { name: 'master', range: '>=1.0.0 <2.0.0', accept: ['patch', 'minor'], - tags: [{channels: ['next'], gitTag: 'v1.0.0@next', version: '1.0.0'}], + tags: [{channels: ['next'], gitTag: 'v1.0.0', version: '1.0.0'}], type: 'release', main: true, }, @@ -105,7 +108,7 @@ test('Plugins are called with expected values', async t => { name: 'next', range: '>=2.0.0', accept: ['patch', 'minor', 'major'], - tags: [{channels: ['next'], gitTag: 'v1.0.0@next', version: '1.0.0'}], + tags: [{channels: ['next'], gitTag: 'v1.0.0', version: '1.0.0'}], type: 'release', main: false, }, @@ -130,7 +133,7 @@ test('Plugins are called with expected values', async t => { ...release1, type: 'major', version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: '[Function: functionStub]', @@ -181,7 +184,7 @@ test('Plugins are called with expected values', async t => { ...omit(lastRelease, 'channels'), type: 'major', version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', name: 'v1.0.0', }); @@ -198,7 +201,7 @@ test('Plugins are called with expected values', async t => { ...omit(lastRelease, 'channels'), type: 'major', version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', name: 'v1.0.0', notes: notes1, @@ -216,7 +219,7 @@ test('Plugins are called with expected values', async t => { ...omit(lastRelease, 'channels'), type: 'major', version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', name: 'v1.0.0', notes: `${notes1}\n\n${notes2}`, @@ -224,7 +227,7 @@ test('Plugins are called with expected values', async t => { branch.tags.push({ version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', gitHead: commits[commits.length - 1].hash, }); @@ -241,7 +244,7 @@ test('Plugins are called with expected values', async t => { ...omit(lastRelease, 'channels'), type: 'major', version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', name: 'v1.0.0', notes: `${notes1}\n\n${notes2}\n\n${notes3}`, @@ -335,7 +338,7 @@ test('Plugins are called with expected values', async t => { ...omit(lastRelease, 'channels'), type: 'major', version: '1.0.0', - channel: undefined, + channel: null, gitTag: 'v1.0.0', name: 'v1.0.0', notes: `${notes1}\n\n${notes2}\n\n${notes3}`, @@ -435,7 +438,7 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c version: '2.0.0', gitHead: await getGitHead({cwd}), gitTag: 'v2.0.0', - channel: undefined, + channel: null, }; const notes = 'Release notes'; @@ -494,15 +497,16 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c test('Make a new release when a commit is forward-ported to an upper branch', async t => { const {cwd, repositoryUrl} = await gitRepo(true); - const commits = await gitCommits(['feat: initial release'], {cwd}); + await gitCommits(['feat: initial release'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); - await gitTagVersion('v1.0.0@1.0.x', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: [null, '1.0.x']}), 'v1.0.0', {cwd}); await gitCheckout('1.0.x', true, {cwd}); - commits.push(...(await gitCommits(['fix: fix on maintenance version 1.0.x'], {cwd}))); - await gitTagVersion('v1.0.1@1.0.x', undefined, {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}); - commits.push(...(await gitCommits(['feat: new feature on master'], {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}); @@ -568,7 +572,8 @@ test('Publish a pre-release version', async t => { 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@beta'); + 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, { @@ -580,7 +585,8 @@ test('Publish a pre-release version', async t => { 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@beta'); + t.is(releases[0].gitTag, 'v1.1.0-beta.2'); + t.is(await gitGetNote('v1.1.0-beta.2', {cwd}), '{"channels":["beta"]}'); }); test('Publish releases from different branch on the same channel', async t => { @@ -696,14 +702,16 @@ test('Publish pre-releases the same channel as regular releases', async t => { test('Do not add pre-releases to a different channel', async t => { const {cwd, repositoryUrl} = await gitRepo(true); - const commits = await gitCommits(['feat: initial release'], {cwd}); + await gitCommits(['feat: initial release'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); - await gitTagVersion('v1.0.0@beta', undefined, {cwd}); + await gitAddNote(JSON.stringify({channels: [null, 'beta']}), 'v1.0.0', {cwd}); await gitCheckout('beta', true, {cwd}); - commits.push(...(await gitCommits(['feat: breaking change/n/nBREAKING CHANGE: break something'], {cwd}))); - await gitTagVersion('v2.0.0-beta.1@beta', undefined, {cwd}); - commits.push(...(await gitCommits(['fix: a fix'], {cwd}))); - await gitTagVersion('v2.0.0-beta.2@beta', undefined, {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}); @@ -748,14 +756,18 @@ 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 gitTagVersion('v1.0.0@next', 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@next', undefined, {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@next', undefined, {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@next', undefined, {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 @@ -788,7 +800,7 @@ async function addChannelMacro(t, mergeFunction) { name: 'v2.0.1', type: 'patch', version: '2.0.1', - channel: undefined, + channel: null, gitTag: 'v2.0.1', gitHead: commits[2].hash, }; @@ -832,7 +844,7 @@ test('Call all "success" plugins even if one errors out', async t => { version: '2.0.0', gitHead: await getGitHead({cwd}), gitTag: 'v2.0.0', - channel: undefined, + channel: null, }; const notes = 'Release notes'; const verifyConditions1 = stub().resolves(); @@ -967,9 +979,11 @@ test('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 gitTagVersion('v1.0.0@next', undefined, {cwd}); - await gitTagVersion('v1.1.0@next', 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}); @@ -1249,8 +1263,10 @@ test('Accept "undefined" value returned by "generateNotes" and "false" by "publi const {cwd, repositoryUrl} = await gitRepo(true); await gitCommits(['First'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); - await gitTagVersion('v1.0.0@next', undefined, {cwd}); - await gitTagVersion('v1.1.0@next', 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}); @@ -1262,7 +1278,7 @@ test('Accept "undefined" value returned by "generateNotes" and "false" by "publi version: '1.2.0', gitHead: await getGitHead({cwd}), gitTag: 'v1.2.0', - channel: undefined, + channel: null, }; const analyzeCommits = stub().resolves(nextRelease.type); const verifyRelease = stub().resolves(); @@ -1335,16 +1351,16 @@ test('Returns false if triggered by a PR', async t => { test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current maintenance branch', async t => { const {cwd, repositoryUrl} = await gitRepo(true); - const commits = await gitCommits(['feat: initial commit'], {cwd}); + await gitCommits(['feat: initial commit'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); - await gitTagVersion('v1.0.0@1.x', 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}); - commits.push(...(await gitCommits(['feat: new feature on master'], {cwd}))); + await gitCommits(['feat: new feature on master'], {cwd}); await gitTagVersion('v1.1.0', undefined, {cwd}); await gitCheckout('1.x', false, {cwd}); - commits.push(...(await gitCommits(['feat: feature on maintenance version 1.x'], {cwd}))); + await gitCommits(['feat: feature on maintenance version 1.x'], {cwd}); await gitPush('origin', 'master', {cwd}); const verifyConditions = stub().resolves(); @@ -1386,16 +1402,17 @@ test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the curren test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current release branch', async t => { const {cwd, repositoryUrl} = await gitRepo(true); - const commits = await gitCommits(['feat: initial commit'], {cwd}); + await gitCommits(['feat: initial commit'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); await gitCheckout('next', true, {cwd}); - commits.push(...(await gitCommits(['feat: new feature on next'], {cwd}))); - await gitTagVersion('v1.1.0@next', undefined, {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}); - commits.push(...(await gitCommits(['feat: new feature on master', 'fix: new fix on master'], {cwd}))); + await gitCommits(['feat: new feature on master', 'fix: new fix on master'], {cwd}); await gitPush('origin', 'master', {cwd}); const verifyConditions = stub().resolves(); @@ -1437,18 +1454,18 @@ test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the curren test('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a maintenance branch', async t => { const {cwd, repositoryUrl} = await gitRepo(true); - const commits = await gitCommits(['First'], {cwd}); + await gitCommits(['First'], {cwd}); await gitTagVersion('v1.0.0', undefined, {cwd}); - await gitTagVersion('v1.0.0@1.1.x', undefined, {cwd}); - commits.push(...(await gitCommits(['Second'], {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 gitTagVersion('v1.1.0@1.1.x', 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}); - commits.push(...(await gitCommits(['Third'], {cwd}))); + await gitCommits(['Third'], {cwd}); await gitTagVersion('v1.1.1', undefined, {cwd}); - commits.push(...(await gitCommits(['Fourth'], {cwd}))); + await gitCommits(['Fourth'], {cwd}); await gitTagVersion('v1.2.0', undefined, {cwd}); await gitPush('origin', 'master', {cwd}); await gitCheckout('1.1.x', false, {cwd}); diff --git a/test/integration.test.js b/test/integration.test.js index 576f2cd1..8840a188 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -16,6 +16,7 @@ import { gitPush, gitCheckout, merge, + gitGetNote, } from './helpers/git-utils'; import {npmView} from './helpers/npm-utils'; import gitbox from './helpers/gitbox'; @@ -220,7 +221,7 @@ test('Release patch, minor and major versions', async t => { createReleaseMock = await mockServer.mock( `/repos/${owner}/${packageName}/releases`, { - body: {tag_name: `v${version}@next`, name: `v${version}`}, + body: {tag_name: `v${version}`, name: `v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}], }, {body: {html_url: `release-url/${version}`}} @@ -246,8 +247,9 @@ test('Release patch, minor and major versions', async t => { } = await npmView(packageName, testEnv)); head = await gitHead({cwd}); t.is(releasedVersion, version); - t.is(await gitTagHead(`v${version}@next`, {cwd}), head); - t.is(await gitRemoteTagHead(authUrl, `v${version}@next`, {cwd}), head); + t.is(await gitGetNote(`v${version}`, {cwd}), '{"channels":["next"]}'); + t.is(await gitTagHead(`v${version}`, {cwd}), head); + t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head); t.log(`+ released ${releasedVersion} on @next`); await mockServer.verify(verifyMock); @@ -262,7 +264,7 @@ test('Release patch, minor and major versions', async t => { {body: {permissions: {push: true}}, method: 'GET'} ); const getReleaseMock = await mockServer.mock( - `/repos/${owner}/${packageName}/releases/tags/v2.0.0@next`, + `/repos/${owner}/${packageName}/releases/tags/v2.0.0`, {headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]}, {body: {id: releaseId}, method: 'GET'} ); @@ -292,11 +294,9 @@ test('Release patch, minor and major versions', async t => { 'dist-tags': {latest: releasedVersion}, } = await npmView(packageName, testEnv)); t.is(releasedVersion, version); - t.is(await gitTagHead(`v${version}`, {cwd}), await gitTagHead(`v${version}@next`, {cwd})); - t.is( - await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), - await gitRemoteTagHead(authUrl, `v${version}@next`, {cwd}) - ); + t.is(await gitGetNote(`v${version}`, {cwd}), '{"channels":["next",null]}'); + t.is(await gitTagHead(`v${version}`, {cwd}), await gitTagHead(`v${version}`, {cwd})); + t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), await gitRemoteTagHead(authUrl, `v${version}`, {cwd})); t.log(`+ added ${releasedVersion}`); await mockServer.verify(verifyMock); diff --git a/test/utils.test.js b/test/utils.test.js index 21308838..dd05a049 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -174,10 +174,6 @@ test('getRange', t => { test('makeTag', t => { t.is(makeTag(`v\${version}`, '1.0.0'), 'v1.0.0'); - t.is(makeTag(`v\${version}`, '1.0.0', false), 'v1.0.0'); - t.is(makeTag(`v\${version}`, '1.0.0', null), 'v1.0.0'); - t.is(makeTag(`v\${version}`, '1.0.0', 'next'), 'v1.0.0@next'); - t.is(makeTag(`v\${version}@test`, '1.0.0', 'next'), 'v1.0.0@next@test'); }); test('isSameChannel', t => {