From 576ba52e425d7702996a1094c4c32c0daca16a41 Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Mon, 25 Sep 2023 13:57:32 +0200 Subject: [PATCH] feat: add prerelease increment behavior (#1303) Co-authored-by: Emilien Escalle --- README.md | 35 +- action.yml | 5 + dist/index.js | 240 +++++++++---- index.js | 114 +++--- lib/default-config.js | 5 +- lib/releases.js | 72 +++- lib/schema.js | 4 + lib/versions.js | 45 ++- schema.json | 14 +- .../config-with-pre-release-identifier.yml | 4 + test/index.test.js | 36 ++ test/releases.test.js | 73 ++++ test/versions.test.js | 332 +++++++++--------- 13 files changed, 664 insertions(+), 315 deletions(-) create mode 100644 test/fixtures/config/config-with-pre-release-identifier.yml diff --git a/README.md b/README.md index 2170659c8..ea63cfffc 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,16 @@ autolabeler: - '/JIRA-[0-9]{1,4}/' ``` +## Prerelease increment + +When creating prerelease (`prerelease: true`), you can add a prerelease identifier to increment the prerelease version number, with the `prerelease-identifier` option. It accept any string, but it's recommended to use [Semantic Versioning](https://semver.org/) prerelease identifiers (alpha, beta, rc, etc). + +Using `prerelease-identifier` automatically enable `include-prereleases`. + +```yml +prerelease-identifier: 'alpha' # will create a prerelease with version number x.x.x-alpha.x +``` + ## Projects that don't use Semantic Versioning If your project doesn't follow [Semantic Versioning](https://semver.org) you can still use Release Drafter, but you may want to set the `version-template` option to customize how the `$NEXT_{PATCH,MINOR,MAJOR}_VERSION` environment variables are generated. @@ -349,18 +359,19 @@ For example, if your project doesn't use patch version numbers, you can set `ver The Release Drafter GitHub Action accepts a number of optional inputs directly in your workflow configuration. These will typically override default behavior specified in your `release-drafter.yml` config. -| Input | Description | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `config-name` | If your workflow requires multiple release-drafter configs it be helpful to override the config-name. The config should still be located inside `.github` as that's where we are looking for config files. | -| `name` | The name that will be used in the GitHub release that's created or updated. This will override any `name-template` specified in your `release-drafter.yml` if defined. | -| `tag` | The tag name to be associated with the GitHub release that's created or updated. This will override any `tag-template` specified in your `release-drafter.yml` if defined. | -| `version` | The version to be associated with the GitHub release that's created or updated. This will override any version calculated by the release-drafter. | -| `publish` | A boolean indicating whether the release being created or updated should be immediately published. This may be useful if the output of a previous workflow step determines that a new version of your project has been (or will be) released, as with [`salsify/action-detect-and-tag-new-version`](https://github.com/salsify/action-detect-and-tag-new-version). | -| `prerelease` | A boolean indicating whether the release being created or updated is a prerelease. | -| `latest` | A string indicating whether the release being created or updated should be marked as latest. | -| `commitish` | A string specifying the target branch for the release being created. | -| `header` | A string that would be added before the template body. | -| `footer` | A string that would be added after the template body. | +| Input | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `config-name` | If your workflow requires multiple release-drafter configs it be helpful to override the config-name. The config should still be located inside `.github` as that's where we are looking for config files. | +| `name` | The name that will be used in the GitHub release that's created or updated. This will override any `name-template` specified in your `release-drafter.yml` if defined. | +| `tag` | The tag name to be associated with the GitHub release that's created or updated. This will override any `tag-template` specified in your `release-drafter.yml` if defined. | +| `version` | The version to be associated with the GitHub release that's created or updated. This will override any version calculated by the release-drafter. | +| `publish` | A boolean indicating whether the release being created or updated should be immediately published. This may be useful if the output of a previous workflow step determines that a new version of your project has been (or will be) released, as with [`salsify/action-detect-and-tag-new-version`](https://github.com/salsify/action-detect-and-tag-new-version). | +| `prerelease` | A boolean indicating whether the release being created or updated is a prerelease. | +| `prerelease-identifier` | A string indicating an identifier (alpha, beta, rc, etc), to increment the prerelease version. number | +| `latest` | A string indicating whether the release being created or updated should be marked as latest. | +| `commitish` | A string specifying the target branch for the release being created. | +| `header` | A string that would be added before the template body. | +| `footer` | A string that would be added after the template body. | ## Action Outputs diff --git a/action.yml b/action.yml index 546a96ef8..8ad4b1bcb 100644 --- a/action.yml +++ b/action.yml @@ -43,6 +43,11 @@ inputs: A boolean indicating whether the release being created or updated is a prerelease. required: false default: '' + prerelease-identifier: + description: | + A string indicating an identifier (alpha, beta, rc, etc), to increment the prerelease version. + required: false + default: '' commitish: description: | The object that the release should be created to point to. diff --git a/dist/index.js b/dist/index.js index f96179435..96194eea5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -142235,11 +142235,11 @@ module.exports = (app, { getRouter }) => { 'pull_request_target.edited', ], async (context) => { - const { disableAutolabeler } = getInput() + const { configName, disableAutolabeler } = getInput() const config = await getConfig({ context, - configName: core.getInput('config-name'), + configName, }) if (config === null || disableAutolabeler) return @@ -142334,24 +142334,16 @@ module.exports = (app, { getRouter }) => { ) const drafter = async (context) => { - const { - shouldDraft, - configName, - version, - tag, - name, - disableReleaser, - commitish, - } = getInput() + const input = getInput() const config = await getConfig({ context, - configName, + configName: input.configName, }) - const { isPreRelease, latest } = getInput({ config }) + if (!config || input.disableReleaser) return - if (config === null || disableReleaser) return + updateConfigFromInput(config, input) // GitHub Actions merge payloads slightly differ, in that their ref points // to the PR branch instead of refs/heads/master @@ -142361,28 +142353,26 @@ module.exports = (app, { getRouter }) => { return } - const targetCommitish = commitish || config['commitish'] || ref + const targetCommitish = config.commitish || ref + const { 'filter-by-commitish': filterByCommitish, 'include-pre-releases': includePreReleases, + 'prerelease-identifier': preReleaseIdentifier, 'tag-prefix': tagPrefix, + latest, + prerelease, } = config - // override header and footer when passed as input - const header = core.getInput('header') - const footer = core.getInput('footer') - if (header) { - config['header'] = header - } - if (footer) { - config['footer'] = footer - } + const shouldIncludePreReleases = Boolean( + includePreReleases || preReleaseIdentifier + ) const { draftRelease, lastRelease } = await findReleases({ context, targetCommitish, filterByCommitish, - includePreReleases, + includePreReleases: shouldIncludePreReleases, tagPrefix, }) @@ -142400,6 +142390,8 @@ module.exports = (app, { getRouter }) => { config['sort-direction'] ) + const { shouldDraft, version, tag, name } = input + const releaseInfo = generateReleaseInfo({ context, commits, @@ -142409,7 +142401,7 @@ module.exports = (app, { getRouter }) => { version, tag, name, - isPreRelease, + isPreRelease: prerelease, latest, shouldDraft, targetCommitish, @@ -142445,40 +142437,56 @@ module.exports = (app, { getRouter }) => { } } -function getInput({ config } = {}) { - // Returns all the inputs that doesn't need a merge with the config file - if (!config) { - return { - shouldDraft: core.getInput('publish').toLowerCase() !== 'true', - configName: core.getInput('config-name'), - version: core.getInput('version') || undefined, - tag: core.getInput('tag') || undefined, - name: core.getInput('name') || undefined, - disableReleaser: - core.getInput('disable-releaser').toLowerCase() === 'true', - disableAutolabeler: - core.getInput('disable-autolabeler').toLowerCase() === 'true', - commitish: core.getInput('commitish') || undefined, - } +function getInput() { + return { + configName: core.getInput('config-name'), + shouldDraft: core.getInput('publish').toLowerCase() !== 'true', + version: core.getInput('version') || undefined, + tag: core.getInput('tag') || undefined, + name: core.getInput('name') || undefined, + disableReleaser: core.getInput('disable-releaser').toLowerCase() === 'true', + disableAutolabeler: + core.getInput('disable-autolabeler').toLowerCase() === 'true', + commitish: core.getInput('commitish') || undefined, + header: core.getInput('header') || undefined, + footer: core.getInput('footer') || undefined, + prerelease: + core.getInput('prerelease') !== '' + ? core.getInput('prerelease').toLowerCase() === 'true' + : undefined, + preReleaseIdentifier: core.getInput('prerelease-identifier') || undefined, + latest: core.getInput('latest')?.toLowerCase() || undefined, } +} - // Merges the config file with the input - // the input takes precedence, because it's more easy to change at runtime - const preRelease = core.getInput('prerelease').toLowerCase() +/** + * Merges the config file with the input + * the input takes precedence, because it's more easy to change at runtime + */ +function updateConfigFromInput(config, input) { + if (input.commitish) { + config.commitish = input.commitish + } - const isPreRelease = - preRelease === 'true' || (!preRelease && config.prerelease) + if (input.header) { + config.header = input.header + } - const latestInput = core.getInput('latest').toLowerCase() + if (input.footer) { + config.footer = input.footer + } - const latest = isPreRelease - ? 'false' - : (!latestInput && config.latest) || latestInput || undefined + if (input.prerelease !== undefined) { + config.prerelease = input.prerelease + } - return { - isPreRelease, - latest, + if (input.preReleaseIdentifier) { + config['prerelease-identifier'] = input.preReleaseIdentifier } + + config.latest = config.prerelease + ? 'false' + : input.latest || config.latest || undefined } function setActionOutput( @@ -142793,7 +142801,7 @@ const DEFAULT_CONFIG = Object.freeze({ 'change-template': `* $TITLE (#$NUMBER) @$AUTHOR`, 'change-title-escapes': '', 'no-changes-template': `* No changes`, - 'version-template': `$MAJOR.$MINOR.$PATCH`, + 'version-template': `$MAJOR.$MINOR.$PATCH$PRERELEASE`, 'version-resolver': { major: { labels: [] }, minor: { labels: [] }, @@ -142811,9 +142819,10 @@ const DEFAULT_CONFIG = Object.freeze({ 'sort-by': SORT_BY.mergedAt, 'sort-direction': SORT_DIRECTIONS.descending, prerelease: false, + 'prerelease-identifier': '', + 'include-pre-releases': false, latest: 'true', 'filter-by-commitish': false, - 'include-pre-releases': false, commitish: '', 'category-template': `## $TITLE`, header: '', @@ -142907,6 +142916,7 @@ exports.paginate = paginate const compareVersions = __nccwpck_require__(89296) const regexEscape = __nccwpck_require__(98691) +const core = __nccwpck_require__(42186) const { getVersionInfo } = __nccwpck_require__(49914) const { template } = __nccwpck_require__(47282) @@ -142915,19 +142925,17 @@ const { log } = __nccwpck_require__(71911) const sortReleases = (releases, tagPrefix) => { // For semver, we find the greatest release number // For non-semver, we use the most recently merged - try { - const tagPrefixRexExp = new RegExp(`^${regexEscape(tagPrefix)}`) - return releases.sort((r1, r2) => - compareVersions( + const tagPrefixRexExp = new RegExp(`^${regexEscape(tagPrefix)}`) + return releases.sort((r1, r2) => { + try { + return compareVersions( r1.tag_name.replace(tagPrefixRexExp, ''), r2.tag_name.replace(tagPrefixRexExp, '') ) - ) - } catch { - return releases.sort( - (r1, r2) => new Date(r1.created_at) - new Date(r2.created_at) - ) - } + } catch { + return new Date(r1.created_at) - new Date(r2.created_at) + } + }) } // GitHub API currently returns a 500 HTTP response if you attempt to fetch over 1000 releases. @@ -142986,7 +142994,12 @@ const findReleases = async ({ } if (lastRelease) { - log({ context, message: `Last release: ${lastRelease.tag_name}` }) + log({ + context, + message: `Last release${ + includePreReleases ? ' (including prerelease)' : '' + }: ${lastRelease.tag_name}`, + }) } else { log({ context, message: `No last release found` }) } @@ -143197,12 +143210,17 @@ const generateChangeLog = (mergedPullRequests, config) => { return changeLog.join('').trim() } -const resolveVersionKeyIncrement = (mergedPullRequests, config) => { +const resolveVersionKeyIncrement = ( + mergedPullRequests, + config, + isPreRelease +) => { const priorityMap = { patch: 1, minor: 2, major: 3, } + const labelToKeyMap = Object.fromEntries( Object.keys(priorityMap) .flatMap((key) => [ @@ -143210,17 +143228,35 @@ const resolveVersionKeyIncrement = (mergedPullRequests, config) => { ]) .flat() ) + + core.debug('labelToKeyMap: ' + JSON.stringify(labelToKeyMap)) + const keys = mergedPullRequests .filter(getFilterExcludedPullRequests(config['exclude-labels'])) .filter(getFilterIncludedPullRequests(config['include-labels'])) .flatMap((pr) => pr.labels.nodes.map((node) => labelToKeyMap[node.name])) .filter(Boolean) + + core.debug('keys: ' + JSON.stringify(keys)) + const keyPriorities = keys.map((key) => priorityMap[key]) const priority = Math.max(...keyPriorities) const versionKey = Object.keys(priorityMap).find( (key) => priorityMap[key] === priority ) - return versionKey || config['version-resolver'].default + + core.debug('versionKey: ' + versionKey) + + const versionKeyIncrement = versionKey || config['version-resolver'].default + + const shouldIncrementAsPrerelease = + isPreRelease && config['prerelease-identifier'] + + if (!shouldIncrementAsPrerelease) { + return versionKeyIncrement + } + + return `pre${versionKeyIncrement}` } const generateReleaseInfo = ({ @@ -143256,16 +143292,27 @@ const generateReleaseInfo = ({ config.replacers ) + const versionKeyIncrement = resolveVersionKeyIncrement( + mergedPullRequests, + config, + isPreRelease + ) + + core.debug('versionKeyIncrement: ' + versionKeyIncrement) + const versionInfo = getVersionInfo( lastRelease, config['version-template'], // Use the first override parameter to identify // a version, from the most accurate to the least version || tag || name, - resolveVersionKeyIncrement(mergedPullRequests, config), - config['tag-prefix'] + versionKeyIncrement, + config['tag-prefix'], + config['prerelease-identifier'] ) + core.debug('versionInfo: ' + JSON.stringify(versionInfo, null, 2)) + if (versionInfo) { body = template(body, versionInfo) } @@ -143276,6 +143323,8 @@ const generateReleaseInfo = ({ tag = template(tag, versionInfo) } + core.debug('tag: ' + tag) + if (name === undefined) { name = versionInfo ? template(config['name-template'] || '', versionInfo) @@ -143284,6 +143333,8 @@ const generateReleaseInfo = ({ name = template(name, versionInfo) } + core.debug('name: ' + name) + // Tags are not supported as `target_commitish` by Github API. // GITHUB_REF or the ref from webhook start with `refs/tags/`, so we handle // those here. If it doesn't but is still a tag - it must have been set @@ -143460,6 +143511,10 @@ const schema = (context) => { prerelease: Joi.boolean().default(DEFAULT_CONFIG.prerelease), + 'prerelease-identifier': Joi.string() + .allow('') + .default(DEFAULT_CONFIG['prerelease-identifier']), + latest: Joi.string() .allow('', 'true', 'false', 'legacy') .default(DEFAULT_CONFIG.latest), @@ -143807,15 +143862,18 @@ const splitSemVersion = (input, versionKey = 'version') => { } const version = input.inc - ? semver.inc(input[versionKey], input.inc, true) + ? semver.inc(input[versionKey], input.inc, true, input.preReleaseIdentifier) : input[versionKey].version + const prereleaseVersion = semver.prerelease(version)?.join('.') || '' + return { ...input, version, $MAJOR: semver.major(version), $MINOR: semver.minor(version), $PATCH: semver.patch(version), + $PRERELEASE: prereleaseVersion ? `-${prereleaseVersion}` : '', $COMPLETE: version, } } @@ -143830,6 +143888,7 @@ const defaultVersionInfo = { $MAJOR: 1, $MINOR: 0, $PATCH: 0, + $PRERELEASE: '', }, $NEXT_MINOR_VERSION: { version: '0.1.0', @@ -143840,6 +143899,7 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', }, $NEXT_PATCH_VERSION: { version: '0.1.0', @@ -143850,6 +143910,19 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', + }, + $NEXT_PRERELEASE_VERSION: { + version: '0.1.0-rc.0', + template: '$MAJOR.$MINOR.$PATCH$PRERELEASE', + inputVersion: null, + versionKeyIncrement: 'prerelease', + inc: 'prerelease', + preReleaseIdentifier: 'rc', + $MAJOR: 0, + $MINOR: 1, + $PATCH: 0, + $PRERELEASE: '-rc.0', }, $INPUT_VERSION: null, $RESOLVED_VERSION: { @@ -143861,6 +143934,7 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', }, } @@ -143914,6 +143988,11 @@ const getTemplatableVersion = (input) => { inc: 'patch', template: '$PATCH', }), + $NEXT_PRERELEASE_VERSION: splitSemVersion({ + ...input, + inc: 'prerelease', + template: '$PRERELEASE', + }), $INPUT_VERSION: splitSemVersion(input, 'inputVersion'), $RESOLVED_VERSION: splitSemVersion({ ...input, @@ -143957,21 +144036,38 @@ const getVersionInfo = ( template, inputVersion, versionKeyIncrement, - tagPrefix + tagPrefix, + preReleaseIdentifier ) => { const version = coerceVersion(release, tagPrefix) inputVersion = coerceVersion(inputVersion, tagPrefix) + const isPreVersionKeyIncrement = versionKeyIncrement?.startsWith('pre') + if (!version && !inputVersion) { + if (isPreVersionKeyIncrement) { + defaultVersionInfo['$RESOLVED_VERSION'] = { + ...defaultVersionInfo['$NEXT_PRERELEASE_VERSION'], + } + } + return defaultVersionInfo } + const shouldIncrementAsPrerelease = + isPreVersionKeyIncrement && version?.prerelease?.length + + if (shouldIncrementAsPrerelease) { + versionKeyIncrement = 'prerelease' + } + return { ...getTemplatableVersion({ version, template, inputVersion, versionKeyIncrement, + preReleaseIdentifier, }), } } diff --git a/index.js b/index.js index 635efc968..3f3366bbb 100644 --- a/index.js +++ b/index.js @@ -32,11 +32,11 @@ module.exports = (app, { getRouter }) => { 'pull_request_target.edited', ], async (context) => { - const { disableAutolabeler } = getInput() + const { configName, disableAutolabeler } = getInput() const config = await getConfig({ context, - configName: core.getInput('config-name'), + configName, }) if (config === null || disableAutolabeler) return @@ -131,24 +131,16 @@ module.exports = (app, { getRouter }) => { ) const drafter = async (context) => { - const { - shouldDraft, - configName, - version, - tag, - name, - disableReleaser, - commitish, - } = getInput() + const input = getInput() const config = await getConfig({ context, - configName, + configName: input.configName, }) - const { isPreRelease, latest } = getInput({ config }) + if (!config || input.disableReleaser) return - if (config === null || disableReleaser) return + updateConfigFromInput(config, input) // GitHub Actions merge payloads slightly differ, in that their ref points // to the PR branch instead of refs/heads/master @@ -158,28 +150,26 @@ module.exports = (app, { getRouter }) => { return } - const targetCommitish = commitish || config['commitish'] || ref + const targetCommitish = config.commitish || ref + const { 'filter-by-commitish': filterByCommitish, 'include-pre-releases': includePreReleases, + 'prerelease-identifier': preReleaseIdentifier, 'tag-prefix': tagPrefix, + latest, + prerelease, } = config - // override header and footer when passed as input - const header = core.getInput('header') - const footer = core.getInput('footer') - if (header) { - config['header'] = header - } - if (footer) { - config['footer'] = footer - } + const shouldIncludePreReleases = Boolean( + includePreReleases || preReleaseIdentifier + ) const { draftRelease, lastRelease } = await findReleases({ context, targetCommitish, filterByCommitish, - includePreReleases, + includePreReleases: shouldIncludePreReleases, tagPrefix, }) @@ -197,6 +187,8 @@ module.exports = (app, { getRouter }) => { config['sort-direction'] ) + const { shouldDraft, version, tag, name } = input + const releaseInfo = generateReleaseInfo({ context, commits, @@ -206,7 +198,7 @@ module.exports = (app, { getRouter }) => { version, tag, name, - isPreRelease, + isPreRelease: prerelease, latest, shouldDraft, targetCommitish, @@ -242,40 +234,56 @@ module.exports = (app, { getRouter }) => { } } -function getInput({ config } = {}) { - // Returns all the inputs that doesn't need a merge with the config file - if (!config) { - return { - shouldDraft: core.getInput('publish').toLowerCase() !== 'true', - configName: core.getInput('config-name'), - version: core.getInput('version') || undefined, - tag: core.getInput('tag') || undefined, - name: core.getInput('name') || undefined, - disableReleaser: - core.getInput('disable-releaser').toLowerCase() === 'true', - disableAutolabeler: - core.getInput('disable-autolabeler').toLowerCase() === 'true', - commitish: core.getInput('commitish') || undefined, - } +function getInput() { + return { + configName: core.getInput('config-name'), + shouldDraft: core.getInput('publish').toLowerCase() !== 'true', + version: core.getInput('version') || undefined, + tag: core.getInput('tag') || undefined, + name: core.getInput('name') || undefined, + disableReleaser: core.getInput('disable-releaser').toLowerCase() === 'true', + disableAutolabeler: + core.getInput('disable-autolabeler').toLowerCase() === 'true', + commitish: core.getInput('commitish') || undefined, + header: core.getInput('header') || undefined, + footer: core.getInput('footer') || undefined, + prerelease: + core.getInput('prerelease') !== '' + ? core.getInput('prerelease').toLowerCase() === 'true' + : undefined, + preReleaseIdentifier: core.getInput('prerelease-identifier') || undefined, + latest: core.getInput('latest')?.toLowerCase() || undefined, } +} - // Merges the config file with the input - // the input takes precedence, because it's more easy to change at runtime - const preRelease = core.getInput('prerelease').toLowerCase() +/** + * Merges the config file with the input + * the input takes precedence, because it's more easy to change at runtime + */ +function updateConfigFromInput(config, input) { + if (input.commitish) { + config.commitish = input.commitish + } - const isPreRelease = - preRelease === 'true' || (!preRelease && config.prerelease) + if (input.header) { + config.header = input.header + } - const latestInput = core.getInput('latest').toLowerCase() + if (input.footer) { + config.footer = input.footer + } - const latest = isPreRelease - ? 'false' - : (!latestInput && config.latest) || latestInput || undefined + if (input.prerelease !== undefined) { + config.prerelease = input.prerelease + } - return { - isPreRelease, - latest, + if (input.preReleaseIdentifier) { + config['prerelease-identifier'] = input.preReleaseIdentifier } + + config.latest = config.prerelease + ? 'false' + : input.latest || config.latest || undefined } function setActionOutput( diff --git a/lib/default-config.js b/lib/default-config.js index 74611b82c..22af5fc55 100644 --- a/lib/default-config.js +++ b/lib/default-config.js @@ -7,7 +7,7 @@ const DEFAULT_CONFIG = Object.freeze({ 'change-template': `* $TITLE (#$NUMBER) @$AUTHOR`, 'change-title-escapes': '', 'no-changes-template': `* No changes`, - 'version-template': `$MAJOR.$MINOR.$PATCH`, + 'version-template': `$MAJOR.$MINOR.$PATCH$PRERELEASE`, 'version-resolver': { major: { labels: [] }, minor: { labels: [] }, @@ -25,9 +25,10 @@ const DEFAULT_CONFIG = Object.freeze({ 'sort-by': SORT_BY.mergedAt, 'sort-direction': SORT_DIRECTIONS.descending, prerelease: false, + 'prerelease-identifier': '', + 'include-pre-releases': false, latest: 'true', 'filter-by-commitish': false, - 'include-pre-releases': false, commitish: '', 'category-template': `## $TITLE`, header: '', diff --git a/lib/releases.js b/lib/releases.js index bcb6f4c45..1f22d6eb1 100644 --- a/lib/releases.js +++ b/lib/releases.js @@ -1,5 +1,6 @@ const compareVersions = require('compare-versions') const regexEscape = require('escape-string-regexp') +const core = require('@actions/core') const { getVersionInfo } = require('./versions') const { template } = require('./template') @@ -8,19 +9,17 @@ const { log } = require('./log') const sortReleases = (releases, tagPrefix) => { // For semver, we find the greatest release number // For non-semver, we use the most recently merged - try { - const tagPrefixRexExp = new RegExp(`^${regexEscape(tagPrefix)}`) - return releases.sort((r1, r2) => - compareVersions( + const tagPrefixRexExp = new RegExp(`^${regexEscape(tagPrefix)}`) + return releases.sort((r1, r2) => { + try { + return compareVersions( r1.tag_name.replace(tagPrefixRexExp, ''), r2.tag_name.replace(tagPrefixRexExp, '') ) - ) - } catch { - return releases.sort( - (r1, r2) => new Date(r1.created_at) - new Date(r2.created_at) - ) - } + } catch { + return new Date(r1.created_at) - new Date(r2.created_at) + } + }) } // GitHub API currently returns a 500 HTTP response if you attempt to fetch over 1000 releases. @@ -79,7 +78,12 @@ const findReleases = async ({ } if (lastRelease) { - log({ context, message: `Last release: ${lastRelease.tag_name}` }) + log({ + context, + message: `Last release${ + includePreReleases ? ' (including prerelease)' : '' + }: ${lastRelease.tag_name}`, + }) } else { log({ context, message: `No last release found` }) } @@ -290,12 +294,17 @@ const generateChangeLog = (mergedPullRequests, config) => { return changeLog.join('').trim() } -const resolveVersionKeyIncrement = (mergedPullRequests, config) => { +const resolveVersionKeyIncrement = ( + mergedPullRequests, + config, + isPreRelease +) => { const priorityMap = { patch: 1, minor: 2, major: 3, } + const labelToKeyMap = Object.fromEntries( Object.keys(priorityMap) .flatMap((key) => [ @@ -303,17 +312,35 @@ const resolveVersionKeyIncrement = (mergedPullRequests, config) => { ]) .flat() ) + + core.debug('labelToKeyMap: ' + JSON.stringify(labelToKeyMap)) + const keys = mergedPullRequests .filter(getFilterExcludedPullRequests(config['exclude-labels'])) .filter(getFilterIncludedPullRequests(config['include-labels'])) .flatMap((pr) => pr.labels.nodes.map((node) => labelToKeyMap[node.name])) .filter(Boolean) + + core.debug('keys: ' + JSON.stringify(keys)) + const keyPriorities = keys.map((key) => priorityMap[key]) const priority = Math.max(...keyPriorities) const versionKey = Object.keys(priorityMap).find( (key) => priorityMap[key] === priority ) - return versionKey || config['version-resolver'].default + + core.debug('versionKey: ' + versionKey) + + const versionKeyIncrement = versionKey || config['version-resolver'].default + + const shouldIncrementAsPrerelease = + isPreRelease && config['prerelease-identifier'] + + if (!shouldIncrementAsPrerelease) { + return versionKeyIncrement + } + + return `pre${versionKeyIncrement}` } const generateReleaseInfo = ({ @@ -349,16 +376,27 @@ const generateReleaseInfo = ({ config.replacers ) + const versionKeyIncrement = resolveVersionKeyIncrement( + mergedPullRequests, + config, + isPreRelease + ) + + core.debug('versionKeyIncrement: ' + versionKeyIncrement) + const versionInfo = getVersionInfo( lastRelease, config['version-template'], // Use the first override parameter to identify // a version, from the most accurate to the least version || tag || name, - resolveVersionKeyIncrement(mergedPullRequests, config), - config['tag-prefix'] + versionKeyIncrement, + config['tag-prefix'], + config['prerelease-identifier'] ) + core.debug('versionInfo: ' + JSON.stringify(versionInfo, null, 2)) + if (versionInfo) { body = template(body, versionInfo) } @@ -369,6 +407,8 @@ const generateReleaseInfo = ({ tag = template(tag, versionInfo) } + core.debug('tag: ' + tag) + if (name === undefined) { name = versionInfo ? template(config['name-template'] || '', versionInfo) @@ -377,6 +417,8 @@ const generateReleaseInfo = ({ name = template(name, versionInfo) } + core.debug('name: ' + name) + // Tags are not supported as `target_commitish` by Github API. // GITHUB_REF or the ref from webhook start with `refs/tags/`, so we handle // those here. If it doesn't but is still a tag - it must have been set diff --git a/lib/schema.js b/lib/schema.js index 023a6090e..7f0d42c06 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -77,6 +77,10 @@ const schema = (context) => { prerelease: Joi.boolean().default(DEFAULT_CONFIG.prerelease), + 'prerelease-identifier': Joi.string() + .allow('') + .default(DEFAULT_CONFIG['prerelease-identifier']), + latest: Joi.string() .allow('', 'true', 'false', 'legacy') .default(DEFAULT_CONFIG.latest), diff --git a/lib/versions.js b/lib/versions.js index 83c8dbd5b..22ea4003f 100644 --- a/lib/versions.js +++ b/lib/versions.js @@ -6,15 +6,18 @@ const splitSemVersion = (input, versionKey = 'version') => { } const version = input.inc - ? semver.inc(input[versionKey], input.inc, true) + ? semver.inc(input[versionKey], input.inc, true, input.preReleaseIdentifier) : input[versionKey].version + const prereleaseVersion = semver.prerelease(version)?.join('.') || '' + return { ...input, version, $MAJOR: semver.major(version), $MINOR: semver.minor(version), $PATCH: semver.patch(version), + $PRERELEASE: prereleaseVersion ? `-${prereleaseVersion}` : '', $COMPLETE: version, } } @@ -29,6 +32,7 @@ const defaultVersionInfo = { $MAJOR: 1, $MINOR: 0, $PATCH: 0, + $PRERELEASE: '', }, $NEXT_MINOR_VERSION: { version: '0.1.0', @@ -39,6 +43,7 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', }, $NEXT_PATCH_VERSION: { version: '0.1.0', @@ -49,6 +54,19 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', + }, + $NEXT_PRERELEASE_VERSION: { + version: '0.1.0-rc.0', + template: '$MAJOR.$MINOR.$PATCH$PRERELEASE', + inputVersion: null, + versionKeyIncrement: 'prerelease', + inc: 'prerelease', + preReleaseIdentifier: 'rc', + $MAJOR: 0, + $MINOR: 1, + $PATCH: 0, + $PRERELEASE: '-rc.0', }, $INPUT_VERSION: null, $RESOLVED_VERSION: { @@ -60,6 +78,7 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', }, } @@ -113,6 +132,11 @@ const getTemplatableVersion = (input) => { inc: 'patch', template: '$PATCH', }), + $NEXT_PRERELEASE_VERSION: splitSemVersion({ + ...input, + inc: 'prerelease', + template: '$PRERELEASE', + }), $INPUT_VERSION: splitSemVersion(input, 'inputVersion'), $RESOLVED_VERSION: splitSemVersion({ ...input, @@ -156,21 +180,38 @@ const getVersionInfo = ( template, inputVersion, versionKeyIncrement, - tagPrefix + tagPrefix, + preReleaseIdentifier ) => { const version = coerceVersion(release, tagPrefix) inputVersion = coerceVersion(inputVersion, tagPrefix) + const isPreVersionKeyIncrement = versionKeyIncrement?.startsWith('pre') + if (!version && !inputVersion) { + if (isPreVersionKeyIncrement) { + defaultVersionInfo['$RESOLVED_VERSION'] = { + ...defaultVersionInfo['$NEXT_PRERELEASE_VERSION'], + } + } + return defaultVersionInfo } + const shouldIncrementAsPrerelease = + isPreVersionKeyIncrement && version?.prerelease?.length + + if (shouldIncrementAsPrerelease) { + versionKeyIncrement = 'prerelease' + } + return { ...getTemplatableVersion({ version, template, inputVersion, versionKeyIncrement, + preReleaseIdentifier, }), } } diff --git a/schema.json b/schema.json index fdaa5f19c..326c7c69a 100644 --- a/schema.json +++ b/schema.json @@ -32,7 +32,7 @@ "type": "string" }, "version-template": { - "default": "$MAJOR.$MINOR.$PATCH", + "default": "$MAJOR.$MINOR.$PATCH$PRERELEASE", "type": "string" }, "name-template": { @@ -137,6 +137,18 @@ "default": false, "type": "boolean" }, + "prerelease-identifier": { + "anyOf": [ + { + "type": "string", + "enum": [""] + }, + { + "default": "", + "type": "string" + } + ] + }, "commitish": { "anyOf": [ { diff --git a/test/fixtures/config/config-with-pre-release-identifier.yml b/test/fixtures/config/config-with-pre-release-identifier.yml new file mode 100644 index 000000000..0ac5971c5 --- /dev/null +++ b/test/fixtures/config/config-with-pre-release-identifier.yml @@ -0,0 +1,4 @@ +template: This is a Pre-release with identifier. +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +prerelease-identifier: alpha diff --git a/test/index.test.js b/test/index.test.js index 4a447683e..443b952ae 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -2557,6 +2557,11 @@ describe('release-drafter', () => { if (overrides.prerelease) { mockEnvironment['INPUT_PRERELEASE'] = overrides.prerelease } + + if (overrides.prereleaseIdentifier) { + mockEnvironment['INPUT_PRERELEASE-IDENTIFIER'] = + overrides.prereleaseIdentifier + } } let restoreEnvironment_ = mockedEnv(mockEnvironment) @@ -2706,6 +2711,37 @@ describe('release-drafter', () => { } ) }) + + it('resolves tag with incremented prerelease identifier', async () => { + return overridesTest( + { + prerelease: 'true', + configName: 'config-with-pre-release-identifier.yml', + }, + { + prerelease: true, + name: 'v2.0.1-alpha.0', + tag_name: 'v2.0.1-alpha.0', + } + ) + }) + }) + + describe('with input prerelease: true and input prerelease-identifier', () => { + it('resolves tag with incremented pre-release identifier', async () => { + return overridesTest( + { + prerelease: 'true', + prereleaseIdentifier: 'beta', + configName: 'config-with-pre-release-identifier.yml', + }, + { + prerelease: true, + name: 'v2.0.1-beta.0', + tag_name: 'v2.0.1-beta.0', + } + ) + }) }) describe('with input prerelease: false', () => { diff --git a/test/releases.test.js b/test/releases.test.js index 3d8e80f80..67b329fc3 100644 --- a/test/releases.test.js +++ b/test/releases.test.js @@ -211,6 +211,7 @@ describe('releases', () => { " `) }) + it('does not add proper details/summary markdown when collapse-after is set and less than 3 PRs', () => { const config = { ...baseConfig, @@ -275,5 +276,77 @@ describe('releases', () => { }) expect(lastRelease.tag_name).toEqual('test-1.0.1') }) + + const paginateMock = jest.fn() + const context = { + payload: { repository: { full_name: 'test' } }, + octokit: { + paginate: paginateMock, + repos: { listReleases: { endpoint: { merge: jest.fn() } } }, + }, + repo: jest.fn(), + log: { info: jest.fn(), warn: jest.fn() }, + } + + it('should return last release without draft and prerelease', async () => { + paginateMock.mockResolvedValueOnce([ + { tag_name: 'v1.0.0', draft: true, prerelease: false }, + { tag_name: 'v1.0.1', draft: false, prerelease: false }, + { tag_name: 'v1.0.2-rc.1', draft: false, prerelease: true }, + ]) + + const { lastRelease } = await findReleases({ + context, + targetCommitish: 'refs/heads/master', + tagPrefix: '', + }) + + expect(lastRelease).toEqual({ + tag_name: 'v1.0.1', + draft: false, + prerelease: false, + }) + }) + + it('should return last draft release', async () => { + paginateMock.mockResolvedValueOnce([ + { tag_name: 'v1.0.0', draft: true, prerelease: false }, + { tag_name: 'v1.0.1', draft: false, prerelease: false }, + { tag_name: 'v1.0.2-rc.1', draft: false, prerelease: true }, + ]) + + const { draftRelease } = await findReleases({ + context, + targetCommitish: 'refs/heads/master', + tagPrefix: '', + }) + + expect(draftRelease).toEqual({ + tag_name: 'v1.0.0', + draft: true, + prerelease: false, + }) + }) + + it('should return last prerelease as last release when includePreReleases is true', async () => { + paginateMock.mockResolvedValueOnce([ + { tag_name: 'v1.0.0', draft: true, prerelease: false }, + { tag_name: 'v1.0.1', draft: false, prerelease: false }, + { tag_name: 'v1.0.2-rc.1', draft: false, prerelease: true }, + ]) + + const { lastRelease } = await findReleases({ + context, + targetCommitish: 'refs/heads/master', + tagPrefix: '', + includePreReleases: true, + }) + + expect(lastRelease).toEqual({ + tag_name: 'v1.0.2-rc.1', + draft: false, + prerelease: true, + }) + }) }) }) diff --git a/test/versions.test.js b/test/versions.test.js index 4cb929bda..8bae0c33f 100644 --- a/test/versions.test.js +++ b/test/versions.test.js @@ -1,174 +1,190 @@ +const { test } = require('@jest/globals') + const { getVersionInfo, defaultVersionInfo } = require('../lib/versions') describe('versions', () => { - it('extracts a version-like string from the last tag', () => { - const versionInfo = getVersionInfo( + test.each([ + [ + 'extracts a version-like string from the last tag', { - tag_name: 'v10.0.3', - name: 'Some release', + release: { + tag_name: 'v10.0.3', + name: 'Some release', + }, + template: '$MAJOR.$MINOR.$PATCH', + expected: { + $MAJOR: '11.0.0', + $MINOR: '10.1.0', + $PATCH: '10.0.4', + $PRERELEASE: '10.0.4-0', + $RESOLVED: '10.0.4', + }, }, - '$MAJOR.$MINOR.$PATCH' - ) - - expect(versionInfo.$NEXT_MAJOR_VERSION.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_MINOR_VERSION.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_PATCH_VERSION.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.template).toEqual('$PATCH') - }) - - it('extracts a version-like string from the last release name if the tag isnt a version', () => { - const versionInfo = getVersionInfo( + ], + [ + 'extracts a version-like string from the last release name if the tag isnt a version', { - tag_name: 'notaproperversion', - name: '10.0.3', + release: { + tag_name: 'notaproperversion', + name: '10.0.3', + }, + template: '$MAJOR.$MINOR.$PATCH', + expected: { + $MAJOR: '11.0.0', + $MINOR: '10.1.0', + $PATCH: '10.0.4', + $RESOLVED: '10.0.4', + $PRERELEASE: '10.0.4-0', + }, }, - '$MAJOR.$MINOR.$PATCH' - ) - - expect(versionInfo.$NEXT_MAJOR_VERSION.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_MINOR_VERSION.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_PATCH_VERSION.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.template).toEqual('$PATCH') - }) - - it('preferences tags over release names', () => { - const versionInfo = getVersionInfo( + ], + [ + 'preferences tags over release names', { - tag_name: '10.0.3', - name: '8.1.0', + release: { + tag_name: '10.0.3', + name: '8.1.0', + }, + template: '$MAJOR.$MINOR.$PATCH', + expected: { + $MAJOR: '11.0.0', + $MINOR: '10.1.0', + $PATCH: '10.0.4', + $PRERELEASE: '10.0.4-0', + $RESOLVED: '10.0.4', + }, }, - '$MAJOR.$MINOR.$PATCH' - ) - - expect(versionInfo.$NEXT_MAJOR_VERSION.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_MINOR_VERSION.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_PATCH_VERSION.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.version).toEqual('10.0.4') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.template).toEqual('$PATCH') - }) - - it('handles alpha/beta releases', () => { - const versionInfo = getVersionInfo( + ], + [ + 'handles alpha/beta releases', + { + release: { + tag_name: 'v10.0.3-alpha', + name: 'Some release', + }, + template: '$MAJOR.$MINOR.$PATCH', + inputVersion: 'v10.0.3-alpha', + expected: { + $MAJOR: '11.0.0', + $MINOR: '10.1.0', + $PATCH: '10.0.3', + $PRERELEASE: '10.0.3-alpha.0', + $RESOLVED: '10.0.3-alpha', + $INPUT: '10.0.3-alpha', + }, + }, + ], + [ + 'handles incremental prereleases', + { + release: { + tag_name: 'v10.0.3', + name: 'Some release', + }, + template: '$MAJOR.$MINOR.$PATCH', + preReleaseIdentifier: 'alpha', + expected: { + $MAJOR: '11.0.0', + $MINOR: '10.1.0', + $PATCH: '10.0.4', + $PRERELEASE: '10.0.4-alpha.0', + $RESOLVED: '10.0.4', + }, + }, + ], + [ + 'handles incremental prereleases on existing prereleases', { - tag_name: 'v10.0.3-alpha', - name: 'Some release', + release: { + tag_name: 'v10.0.3-alpha.2', + name: 'Some release', + }, + template: '$MAJOR.$MINOR.$PATCH', + expected: { + $MAJOR: '11.0.0', + $MINOR: '10.1.0', + $PATCH: '10.0.3', + $PRERELEASE: '10.0.3-alpha.3', + $RESOLVED: '10.0.3', + }, }, - '$MAJOR.$MINOR.$PATCH', - 'v10.0.3-alpha' - ) + ], + ])( + `%s`, + ( + name, + { release, template, inputVersion, preReleaseIdentifier, expected } + ) => { + const versionInfo = getVersionInfo( + release, + template, + inputVersion, + undefined, + undefined, + preReleaseIdentifier + ) - expect(versionInfo.$NEXT_MAJOR_VERSION.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.version).toEqual('11.0.0') - expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_MINOR_VERSION.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_PATCH_VERSION.version).toEqual('10.0.3') - expect(versionInfo.$INPUT_VERSION.version).toEqual('10.0.3-alpha') - expect(versionInfo.$RESOLVED_VERSION.version).toEqual('10.0.3-alpha') + // Next major version checks + expect(versionInfo.$NEXT_MAJOR_VERSION.version).toEqual(expected.$MAJOR) + expect(versionInfo.$NEXT_MAJOR_VERSION.template).toEqual(template) + expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.version).toEqual( + expected.$MAJOR + ) + expect(versionInfo.$NEXT_MAJOR_VERSION_MAJOR.template).toEqual('$MAJOR') + expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.version).toEqual( + expected.$MAJOR + ) + expect(versionInfo.$NEXT_MAJOR_VERSION_MINOR.template).toEqual('$MINOR') + expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.version).toEqual( + expected.$MAJOR + ) + expect(versionInfo.$NEXT_MAJOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_MINOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.version).toEqual('10.1.0') - expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.template).toEqual('$PATCH') - expect(versionInfo.$NEXT_PATCH_VERSION.version).toEqual('10.0.3') - expect(versionInfo.$NEXT_PATCH_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.version).toEqual('10.0.3') - expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.template).toEqual('$MAJOR') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.version).toEqual('10.0.3') - expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.template).toEqual('$MINOR') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.version).toEqual('10.0.3') - expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.template).toEqual('$PATCH') - }) + // Next minor version checks + expect(versionInfo.$NEXT_MINOR_VERSION.version).toEqual(expected.$MINOR) + expect(versionInfo.$NEXT_MINOR_VERSION.template).toEqual(template) + expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.version).toEqual( + expected.$MINOR + ) + expect(versionInfo.$NEXT_MINOR_VERSION_MAJOR.template).toEqual('$MAJOR') + expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.version).toEqual( + expected.$MINOR + ) + expect(versionInfo.$NEXT_MINOR_VERSION_MINOR.template).toEqual('$MINOR') + expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.version).toEqual( + expected.$MINOR + ) + expect(versionInfo.$NEXT_MINOR_VERSION_PATCH.template).toEqual('$PATCH') + + // Next patch version checks + expect(versionInfo.$NEXT_PATCH_VERSION.version).toEqual(expected.$PATCH) + expect(versionInfo.$NEXT_PATCH_VERSION.template).toEqual(template) + expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.version).toEqual( + expected.$PATCH + ) + expect(versionInfo.$NEXT_PATCH_VERSION_MAJOR.template).toEqual('$MAJOR') + expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.version).toEqual( + expected.$PATCH + ) + expect(versionInfo.$NEXT_PATCH_VERSION_MINOR.template).toEqual('$MINOR') + expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.version).toEqual( + expected.$PATCH + ) + expect(versionInfo.$NEXT_PATCH_VERSION_PATCH.template).toEqual('$PATCH') + + // Next prerelease version checks + expect(versionInfo.$NEXT_PRERELEASE_VERSION.version).toEqual( + expected.$PRERELEASE + ) + expect(versionInfo.$NEXT_PRERELEASE_VERSION.template).toEqual( + '$PRERELEASE' + ) + + // Input & Resolved version checks + expect(versionInfo.$INPUT_VERSION?.version).toEqual(expected.$INPUT) + expect(versionInfo.$RESOLVED_VERSION.version).toEqual(expected.$RESOLVED) + } + ) it('returns default version info if no version was found in tag or name', () => { const versionInfo = getVersionInfo({})