From b2c1b2c670f8f2dd4da71721ffb329c26e8d2cd7 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Thu, 28 Nov 2019 01:15:00 -0500 Subject: [PATCH] feat: use Git notes to store the channels on which a version has been released BREAKING CHANGE: this feature change the way semantic-release keep track of the channels on which a version has been released. It now use a JSON object stored in a [Git note](https://git-scm.com/docs/git-notes) instead of Git tags formatted as v{version}@{channel}. The tags formatted as v{version}@{channel} will now be ignored. If you have made releases with v16.0.0 on branches other than the default one you will have to update your repository. The changes to make consist in: - Finding all the versions that have been released on a branch other than the default one by searching for all tags formatted v{version}@{channel} - For each of those version: - Create a tag without the {@channel} if none doesn't already exists - Add a Git note to the tag without the {@channel} containing the channels on which the version was released formatted as `{"channels":["channel1","channel2"]}` and using `null` for the default channel (for example.`{"channels":[null,"channel1","channel2"]}`) - Push the tags and notes - Update the GitHub releases that refer to a tag formatted as v{version}@{channel} to use the tag without it - Delete the tags formatted as v{version}@{channel} --- index.js | 15 ++-- lib/branches/get-tags.js | 31 ++++----- lib/branches/index.js | 4 +- lib/definitions/constants.js | 3 + lib/get-release-to-add.js | 8 +-- lib/git.js | 70 +++++++++++++++++++ lib/utils.js | 4 +- test/branches/get-tags.test.js | 75 ++++++++------------ test/get-last-release.test.js | 21 +++--- test/get-release-to-add.test.js | 109 +++++++++++++++-------------- test/git.test.js | 93 +++++++++++++++++++++++++ test/helpers/git-utils.js | 22 ++++++ test/index.test.js | 119 ++++++++++++++++++-------------- test/integration.test.js | 18 ++--- test/utils.test.js | 4 -- 15 files changed, 386 insertions(+), 210 deletions(-) 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 => {