diff --git a/packages/shipjs/jest.setup.js b/packages/shipjs/jest.setup.js index 8b6c2bd6f..35fc195bc 100644 --- a/packages/shipjs/jest.setup.js +++ b/packages/shipjs/jest.setup.js @@ -10,8 +10,16 @@ import { import { mockColor } from './tests/util'; jest.mock('shipjs-lib'); jest.mock('./src/color'); -jest.mock('./src/util'); jest.mock('./src/helper'); +jest.mock('./src/util', () => { + const actual = jest.requireActual('./src/util'); + const mock = jest.genMockFromModule('./src/util'); + + return { + ...mock, + arrayify: actual.arrayify, + }; +}); beforeEach(() => { [info, warning, error, slateblue, bold, underline, reset].forEach(mockColor); diff --git a/packages/shipjs/src/flow/prepare.js b/packages/shipjs/src/flow/prepare.js index 92791254f..139391e46 100644 --- a/packages/shipjs/src/flow/prepare.js +++ b/packages/shipjs/src/flow/prepare.js @@ -23,7 +23,7 @@ import validatePreparationConditions from '../step/prepare/validatePreparationCo import checkGitHubToken from '../step/checkGitHubToken'; import finished from '../step/prepare/finished'; -import { print } from '../util'; +import { arrayify, print, wrapExecWithDir } from '../util'; import { warning } from '../color'; async function prepare({ @@ -47,38 +47,67 @@ async function prepare({ checkGitHubToken({ dryRun }); const config = await loadConfig(dir); const { currentVersion, baseBranch } = validate({ config, dir }); - const { remote, forcePushBranches } = config; - pull({ remote, currentBranch: baseBranch, dir, dryRun }); - fetchTags({ dir, dryRun }); - push({ remote, currentBranch: baseBranch, forcePushBranches, dir, dryRun }); - const currentTag = config.getTagName({ version: currentVersion }); - const { revisionRange } = await getRevisionRange({ - yes, - commitFrom, - currentTag, - dir, - }); - let { nextVersion } = getNextVersion({ - config, - revisionRange, - currentVersion, - dir, - }); - nextVersion = await confirmNextVersion({ - yes, - currentVersion, - nextVersion, - dryRun, - }); - const releaseType = getReleaseType(nextVersion); - await validatePreparationConditions({ - config, - releaseType, - nextVersion, - revisionRange, - dir, - dryRun, - }); + const { remote, forcePushBranches, version } = config; + + const currentTag = arrayify( + config.getTagName({ version: currentVersion }) + )[0]; + let nextVersion; + let releaseType; + + if (version) { + ({ nextVersion } = await version({ + exec: wrapExecWithDir(dir), + })); + releaseType = getReleaseType(nextVersion); + } else { + pull({ remote, currentBranch: baseBranch, dir, dryRun }); + fetchTags({ dir, dryRun }); + push({ remote, currentBranch: baseBranch, forcePushBranches, dir, dryRun }); + const { revisionRange } = await getRevisionRange({ + yes, + commitFrom, + currentTag, + dir, + }); + ({ nextVersion } = getNextVersion({ + config, + revisionRange, + currentVersion, + dir, + })); + nextVersion = await confirmNextVersion({ + yes, + currentVersion, + nextVersion, + dryRun, + }); + releaseType = getReleaseType(nextVersion); + await validatePreparationConditions({ + config, + releaseType, + nextVersion, + revisionRange, + dir, + dryRun, + }); + + const updateVersionFn = config.monorepo + ? updateVersionMonorepo + : updateVersion; + await updateVersionFn({ config, nextVersion, releaseType, dir, dryRun }); + installDependencies({ config, dir, dryRun }); + await updateChangelog({ + config, + revisionRange, + firstRelease, + nextVersion, + releaseCount, + dir, + dryRun, + }); + } + const { stagingBranch } = prepareStagingBranch({ config, nextVersion, @@ -86,20 +115,6 @@ async function prepare({ dir, }); checkoutToStagingBranch({ stagingBranch, dir, dryRun }); - const updateVersionFn = config.monorepo - ? updateVersionMonorepo - : updateVersion; - await updateVersionFn({ config, nextVersion, releaseType, dir, dryRun }); - installDependencies({ config, dir, dryRun }); - await updateChangelog({ - config, - revisionRange, - firstRelease, - nextVersion, - releaseCount, - dir, - dryRun, - }); await commitChanges({ nextVersion, releaseType, diff --git a/packages/shipjs/src/flow/release.js b/packages/shipjs/src/flow/release.js index 2f69598d9..3f250011b 100644 --- a/packages/shipjs/src/flow/release.js +++ b/packages/shipjs/src/flow/release.js @@ -42,14 +42,14 @@ async function release({ help = false, dir = '.', dryRun = false }) { await runBeforePublish({ config, dir, dryRun }); runPublish({ isYarn, config, releaseTag, dir, dryRun }); await runAfterPublish({ version, releaseTag, config, dir, dryRun }); - const { tagName } = createGitTag({ version, config, dir, dryRun }); - gitPush({ tagName, config, dir, dryRun }); + const { tagNames } = createGitTag({ version, config, dir, dryRun }); + gitPush({ tagNames, config, dir, dryRun }); await createGitHubRelease({ version, config, dir, dryRun }); await notifyReleaseSuccess({ config, appName, version, - tagName, + tagNames, latestCommitHash, latestCommitUrl, repoURL, diff --git a/packages/shipjs/src/step/release/__tests__/createGitTag.spec.js b/packages/shipjs/src/step/release/__tests__/createGitTag.spec.js index 0f3b45d1d..f7e81c0c0 100644 --- a/packages/shipjs/src/step/release/__tests__/createGitTag.spec.js +++ b/packages/shipjs/src/step/release/__tests__/createGitTag.spec.js @@ -2,8 +2,8 @@ import { run } from '../../../util'; import createGitTag from '../createGitTag'; describe('createGitTag', () => { - it('works', () => { - const { tagName } = createGitTag({ + it('works with one tag', () => { + const { tagNames } = createGitTag({ version: '1.2.3', config: { getTagName: () => 'v1.2.3' }, dir: '.', @@ -15,6 +15,22 @@ describe('createGitTag', () => { dir: '.', dryRun: false, }); - expect(tagName).toEqual('v1.2.3'); + expect(tagNames).toEqual(['v1.2.3']); + }); + + it('works with multiple tags', () => { + const { tagNames } = createGitTag({ + version: '1.2.3', + config: { getTagName: () => ['instantsearch.js@1.2.3', 'swag@4'] }, + dir: '.', + dryRun: false, + }); + expect(run).toHaveBeenCalledTimes(1); + expect(run).toHaveBeenCalledWith({ + command: 'git tag instantsearch.js@1.2.3 && git tag swag@4', + dir: '.', + dryRun: false, + }); + expect(tagNames).toEqual(['instantsearch.js@1.2.3', 'swag@4']); }); }); diff --git a/packages/shipjs/src/step/release/__tests__/gitPush.spec.js b/packages/shipjs/src/step/release/__tests__/gitPush.spec.js index 6e233fe27..97d69718f 100644 --- a/packages/shipjs/src/step/release/__tests__/gitPush.spec.js +++ b/packages/shipjs/src/step/release/__tests__/gitPush.spec.js @@ -6,7 +6,7 @@ describe('gitPush', () => { it('works', () => { getCurrentBranch.mockImplementationOnce(() => 'master'); gitPush({ - tagName: 'v1.2.3', + tagNames: ['v1.2.3'], config: { remote: 'origin', }, diff --git a/packages/shipjs/src/step/release/createGitHubRelease.js b/packages/shipjs/src/step/release/createGitHubRelease.js index 919002aa9..89e021ab4 100644 --- a/packages/shipjs/src/step/release/createGitHubRelease.js +++ b/packages/shipjs/src/step/release/createGitHubRelease.js @@ -6,7 +6,7 @@ import mime from 'mime-types'; import { getRepoInfo } from 'shipjs-lib'; import runStep from '../runStep'; import { getChangelog } from '../../helper'; -import { print } from '../../util'; +import { arrayify, print } from '../../util'; export default async ({ version, config, dir, dryRun }) => await runStep( @@ -19,58 +19,60 @@ export default async ({ version, config, dir, dryRun }) => getTagName, releases: { assetsToUpload, extractChangelog } = {}, } = config; - const tagName = getTagName({ version }); + const tagNames = arrayify(getTagName({ version })); - // extract matching changelog - const getChangelogFn = extractChangelog || getChangelog; - const changelog = getChangelogFn({ version, dir }); - const content = changelog || ''; + for await (const tagName of tagNames) { + // extract matching changelog + const getChangelogFn = extractChangelog || getChangelog; + const changelog = getChangelogFn({ version, dir }); + const content = changelog || ''; - // handle assets - const assetPaths = await getAssetPaths({ - assetsToUpload, - dir, - version, - tagName, - }); + // handle assets + const assetPaths = await getAssetPaths({ + assetsToUpload, + dir, + version, + tagName, + }); - if (dryRun) { - print('Creating a release with the following:'); - print(` - content: ${content}`); - if (assetPaths.length > 0) { - print(` - assets: ${assetPaths.join(' ')}`); + if (dryRun) { + print('Creating a release with the following:'); + print(` - content: ${content}`); + if (assetPaths.length > 0) { + print(` - assets: ${assetPaths.join(' ')}`); + } + return; } - return; - } - const { owner, name: repo } = getRepoInfo(remote, dir); + const { owner, name: repo } = getRepoInfo(remote, dir); - const octokit = new Octokit({ - auth: `token ${process.env.GITHUB_TOKEN}`, - }); + const octokit = new Octokit({ + auth: `token ${process.env.GITHUB_TOKEN}`, + }); - const { - data: { upload_url }, // eslint-disable-line camelcase - } = await octokit.repos.createRelease({ - owner, - repo, - tag_name: tagName, // eslint-disable-line camelcase - name: tagName, - body: content, - }); + const { + data: { upload_url }, // eslint-disable-line camelcase + } = await octokit.repos.createRelease({ + owner, + repo, + tag_name: tagName, // eslint-disable-line camelcase + name: tagName, + body: content, + }); - if (assetPaths.length > 0) { - for (const assetPath of assetPaths) { - const file = path.resolve(dir, assetPath); - octokit.repos.uploadReleaseAsset({ - file: fs.readFileSync(file), - headers: { - 'content-length': fs.statSync(file).size, - 'content-type': mime.lookup(file), - }, - name: path.basename(file), - url: upload_url, // eslint-disable-line camelcase - }); + if (assetPaths.length > 0) { + for (const assetPath of assetPaths) { + const file = path.resolve(dir, assetPath); + octokit.repos.uploadReleaseAsset({ + file: fs.readFileSync(file), + headers: { + 'content-length': fs.statSync(file).size, + 'content-type': mime.lookup(file), + }, + name: path.basename(file), + url: upload_url, // eslint-disable-line camelcase + }); + } } } } diff --git a/packages/shipjs/src/step/release/createGitTag.js b/packages/shipjs/src/step/release/createGitTag.js index f69657b6b..9b7fe9a26 100644 --- a/packages/shipjs/src/step/release/createGitTag.js +++ b/packages/shipjs/src/step/release/createGitTag.js @@ -1,11 +1,11 @@ import runStep from '../runStep'; -import { run } from '../../util'; +import { arrayify, run } from '../../util'; export default ({ version, config, dir, dryRun }) => runStep({ title: 'Creating a git tag.' }, () => { const { getTagName } = config; - const tagName = getTagName({ version }); - const command = `git tag ${tagName}`; + const tagNames = arrayify(getTagName({ version })); + const command = tagNames.map((tag) => `git tag ${tag}`).join(' && '); run({ command, dir, dryRun }); - return { tagName }; + return { tagNames }; }); diff --git a/packages/shipjs/src/step/release/gitPush.js b/packages/shipjs/src/step/release/gitPush.js index 8dada56ac..96c241fca 100644 --- a/packages/shipjs/src/step/release/gitPush.js +++ b/packages/shipjs/src/step/release/gitPush.js @@ -2,13 +2,13 @@ import { getCurrentBranch } from 'shipjs-lib'; import runStep from '../runStep'; import { gitPush } from '../../helper'; -export default ({ tagName, config, dir, dryRun }) => +export default ({ tagNames, config, dir, dryRun }) => runStep({ title: 'Pushing to the remote.' }, () => { const currentBranch = getCurrentBranch(dir); const { remote, forcePushBranches } = config; gitPush({ remote, - refs: [currentBranch, tagName], + refs: [currentBranch, ...tagNames], forcePushBranches, dir, dryRun, diff --git a/packages/shipjs/src/step/release/notifyReleaseSuccess.js b/packages/shipjs/src/step/release/notifyReleaseSuccess.js index 777bd8c2e..02a52e344 100644 --- a/packages/shipjs/src/step/release/notifyReleaseSuccess.js +++ b/packages/shipjs/src/step/release/notifyReleaseSuccess.js @@ -5,7 +5,7 @@ export default async ({ config, appName, version, - tagName, + tagNames, latestCommitHash, latestCommitUrl, repoURL, @@ -23,7 +23,7 @@ export default async ({ config, appName, version, - tagName, + tagNames, latestCommitHash, latestCommitUrl, repoURL, diff --git a/packages/shipjs/src/util/arrayify.js b/packages/shipjs/src/util/arrayify.js new file mode 100644 index 000000000..9a9e09769 --- /dev/null +++ b/packages/shipjs/src/util/arrayify.js @@ -0,0 +1,2 @@ +export const arrayify = (itemOrArray) => + Array.isArray(itemOrArray) ? itemOrArray : [itemOrArray]; diff --git a/packages/shipjs/src/util/index.js b/packages/shipjs/src/util/index.js index 6fceb90b3..4a48563bf 100644 --- a/packages/shipjs/src/util/index.js +++ b/packages/shipjs/src/util/index.js @@ -1,3 +1,4 @@ +export { arrayify } from './arrayify'; export { default as detectYarn } from './detectYarn'; export { default as exitProcess } from './exitProcess'; export { default as indentedPrint } from './indentedPrint'; diff --git a/packages/shipjs/src/util/slack.js b/packages/shipjs/src/util/slack.js index 8c4da6685..2d6fef263 100644 --- a/packages/shipjs/src/util/slack.js +++ b/packages/shipjs/src/util/slack.js @@ -41,7 +41,7 @@ export async function notifyReleaseSuccess({ config, appName, version, - tagName, + tagNames, latestCommitHash, latestCommitUrl, repoURL, @@ -57,7 +57,7 @@ export async function notifyReleaseSuccess({ ? releaseSuccess({ appName, version, - tagName, + tagName: tagNames.join(', '), latestCommitHash, latestCommitUrl, repoURL, diff --git a/website/reference/all-config.md b/website/reference/all-config.md index 9bee540b3..d7a99a4dc 100644 --- a/website/reference/all-config.md +++ b/website/reference/all-config.md @@ -19,7 +19,7 @@ module.exports = { If `monorepo` is defined, Ship.js will treat the project as a monorepo. :::warning NOTICE -Ship.js currently does not provide independent versioning. It means all the packages in the monorepo must have the same version. +Ship.js currently does not support independent versioning out of the box. You can still support it yourself by leveraging the [`version`](#releases) lifecycle hook and use a separate tool (like Lerna) to update versions. ::: ### **`shipjs prepare`** @@ -52,6 +52,24 @@ Ship.js will check `dependencies`, `devDependencies` and `peerDependencies` and 1. Ship.js will only publish the packages from `packagesToPublish`. +## `version` + +_used at_: `shipjs prepare` + +_default_: `undefined` + +When provided, Ship.js skips all other validation and creation of changelog, and only runs `version` instead. The steps you need to recreate in this hook are: +- decide on the next version (for each package) +- validate the conditions in "shouldPrepare" +- update version numbers +- updating or writing changelog + +```js +version: ({ exec }) => { + exec('yarn lerna version --no-push --no-git-tag-version --yes --conventional-commits'); +} +``` + ## `shouldPrepare` _used at_: `shipjs prepare`