diff --git a/README.md b/README.md index 24ae540381..2dcac5cd6b 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,14 @@ autolabeler: - '/JIRA-[0-9]{1,4}/' ``` +## Pre-release increment + +When creating Pre-release (`prerelease: true`), you can add a pre-release identifier to increment the pre-release version number, with the `prerelease-identifer` option. It accept any string, but it's recommended to use [Semantic Versioning](https://semver.org/) pre-release identifiers (alpha, beta, rc, etc). + +```yml +prerelease-identifer: 'alpha' # will create a pre-release 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. diff --git a/dist/index.js b/dist/index.js index b851d5f52c..dcb223480b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -142773,7 +142773,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: [] }, @@ -142791,6 +142791,7 @@ const DEFAULT_CONFIG = Object.freeze({ 'sort-by': SORT_BY.mergedAt, 'sort-direction': SORT_DIRECTIONS.descending, prerelease: false, + 'prerelease-identifier': '', 'filter-by-commitish': false, commitish: '', 'category-template': `## $TITLE`, @@ -143166,12 +143167,22 @@ 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, } + + // Should increment pre-release version + if (isPreRelease && config['prerelease-identifier']) { + return 'prerelease' + } + const labelToKeyMap = Object.fromEntries( Object.keys(priorityMap) .flatMap((key) => [ @@ -143225,14 +143236,21 @@ const generateReleaseInfo = ({ config.replacers ) + const versionKeyIncrement = resolveVersionKeyIncrement( + mergedPullRequests, + config, + isPreRelease + ) + 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'] ) if (versionInfo) { @@ -143416,6 +143434,9 @@ const schema = (context) => { .default(DEFAULT_CONFIG['sort-direction']), prerelease: Joi.boolean().default(DEFAULT_CONFIG.prerelease), + 'prerelease-identifier': Joi.string() + .allow('') + .default(DEFAULT_CONFIG['prerelease-identifier']), 'filter-by-commitish': Joi.boolean().default( DEFAULT_CONFIG['filter-by-commitish'] @@ -143756,15 +143777,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, } } @@ -143779,6 +143803,7 @@ const defaultVersionInfo = { $MAJOR: 1, $MINOR: 0, $PATCH: 0, + $PRERELEASE: '', }, $NEXT_MINOR_VERSION: { version: '0.1.0', @@ -143789,6 +143814,7 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', }, $NEXT_PATCH_VERSION: { version: '0.1.0', @@ -143799,6 +143825,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: { @@ -143810,6 +143849,7 @@ const defaultVersionInfo = { $MAJOR: 0, $MINOR: 1, $PATCH: 0, + $PRERELEASE: '', }, } @@ -143863,6 +143903,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, @@ -143906,7 +143951,8 @@ const getVersionInfo = ( template, inputVersion, versionKeyIncrement, - tagPrefix + tagPrefix, + prereleaseIdentifier ) => { const version = coerceVersion(release, tagPrefix) inputVersion = coerceVersion(inputVersion, tagPrefix) @@ -143921,6 +143967,7 @@ const getVersionInfo = ( template, inputVersion, versionKeyIncrement, + prereleaseIdentifier, }), } } diff --git a/lib/default-config.js b/lib/default-config.js index 5affa94652..0d9f23530f 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,6 +25,7 @@ const DEFAULT_CONFIG = Object.freeze({ 'sort-by': SORT_BY.mergedAt, 'sort-direction': SORT_DIRECTIONS.descending, prerelease: false, + 'prerelease-identifier': '', 'filter-by-commitish': false, commitish: '', 'category-template': `## $TITLE`, diff --git a/lib/releases.js b/lib/releases.js index 2d30ff1a2b..d07c2520bf 100644 --- a/lib/releases.js +++ b/lib/releases.js @@ -281,12 +281,22 @@ 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, } + + // Should increment pre-release version + if (isPreRelease && config['prerelease-identifier']) { + return 'prerelease' + } + const labelToKeyMap = Object.fromEntries( Object.keys(priorityMap) .flatMap((key) => [ @@ -340,14 +350,21 @@ const generateReleaseInfo = ({ config.replacers ) + const versionKeyIncrement = resolveVersionKeyIncrement( + mergedPullRequests, + config, + isPreRelease + ) + 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'] ) if (versionInfo) { diff --git a/lib/schema.js b/lib/schema.js index 60dd8209a7..d118711bd0 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -76,6 +76,9 @@ const schema = (context) => { .default(DEFAULT_CONFIG['sort-direction']), prerelease: Joi.boolean().default(DEFAULT_CONFIG.prerelease), + 'prerelease-identifier': Joi.string() + .allow('') + .default(DEFAULT_CONFIG['prerelease-identifier']), 'filter-by-commitish': Joi.boolean().default( DEFAULT_CONFIG['filter-by-commitish'] diff --git a/lib/versions.js b/lib/versions.js index 83c8dbd5ba..69323d6af9 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,7 +180,8 @@ const getVersionInfo = ( template, inputVersion, versionKeyIncrement, - tagPrefix + tagPrefix, + prereleaseIdentifier ) => { const version = coerceVersion(release, tagPrefix) inputVersion = coerceVersion(inputVersion, tagPrefix) @@ -171,6 +196,7 @@ const getVersionInfo = ( template, inputVersion, versionKeyIncrement, + prereleaseIdentifier, }), } } diff --git a/schema.json b/schema.json index cf5b7c8ebb..90bab01313 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": { @@ -117,6 +117,18 @@ "default": false, "type": "boolean" }, + "prerelease-identifier": { + "anyOf": [ + { + "type": "string", + "enum": [""] + }, + { + "default": "", + "type": "string" + } + ] + }, "filter-by-commitish": { "default": false, "type": "boolean" diff --git a/test/fixtures/config/config-with-prerelease-identifier.yml b/test/fixtures/config/config-with-prerelease-identifier.yml new file mode 100644 index 0000000000..0ac5971c55 --- /dev/null +++ b/test/fixtures/config/config-with-prerelease-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 812f878560..f26784be0b 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -4,7 +4,7 @@ const { getConfigMock } = require('./helpers/config-mock') const releaseDrafter = require('../index') const mockedEnv = require('mocked-env') const pino = require('pino') -const Stream = require('stream') +const Stream = require('node:stream') const pushPayload = require('./fixtures/push.json') const pushTagPayload = require('./fixtures/push-tag.json') const releasePayload = require('./fixtures/release.json') @@ -2610,6 +2610,20 @@ describe('release-drafter', () => { } ) }) + + it('resolves tag with incremented pre-release identifier', async () => { + return overridesTest( + { + prerelease: 'true', + configName: 'config-with-prerelease-identifier.yml', + }, + { + prerelease: true, + name: 'v2.0.1-alpha.0', + tag_name: 'v2.0.1-alpha.0', + } + ) + }) }) describe('with input prerelease: false', () => { diff --git a/test/versions.test.js b/test/versions.test.js index d524989ac0..d77c6e82ba 100644 --- a/test/versions.test.js +++ b/test/versions.test.js @@ -16,6 +16,7 @@ describe('versions', () => { $MAJOR: '11.0.0', $MINOR: '10.1.0', $PATCH: '10.0.4', + $PRERELEASE: '10.0.4-0', $RESOLVED: '10.0.4', }, }, @@ -33,6 +34,7 @@ describe('versions', () => { $MINOR: '10.1.0', $PATCH: '10.0.4', $RESOLVED: '10.0.4', + $PRERELEASE: '10.0.4-0', }, }, ], @@ -48,6 +50,7 @@ describe('versions', () => { $MAJOR: '11.0.0', $MINOR: '10.1.0', $PATCH: '10.0.4', + $PRERELEASE: '10.0.4-0', $RESOLVED: '10.0.4', }, }, @@ -65,72 +68,123 @@ describe('versions', () => { $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', }, }, ], - ])(`%s`, (name, { release, template, inputVersion, expected }) => { - const versionInfo = getVersionInfo(release, template, inputVersion) + [ + 'handles incremental pre-releases', + { + 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 pre-releases on existing pre-releases', + { + 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', + }, + }, + ], + ])( + `%s`, + ( + name, + { release, template, inputVersion, prereleaseIdentifier, expected } + ) => { + const versionInfo = getVersionInfo( + release, + template, + inputVersion, + undefined, + undefined, + prereleaseIdentifier + ) - // Next major version checks - expect(versionInfo.$NEXT_MAJOR_VERSION.version).toEqual(expected.$MAJOR) - expect(versionInfo.$NEXT_MAJOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - 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') + // 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') - // Next minor version checks - expect(versionInfo.$NEXT_MINOR_VERSION.version).toEqual(expected.$MINOR) - expect(versionInfo.$NEXT_MINOR_VERSION.template).toEqual( - '$MAJOR.$MINOR.$PATCH' - ) - 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 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( - '$MAJOR.$MINOR.$PATCH' - ) - 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 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') - expect(versionInfo.$NEXT_PATCH_VERSION.version).toEqual(expected.$PATCH) - expect(versionInfo.$INPUT_VERSION?.version).toEqual(expected.$INPUT) - expect(versionInfo.$RESOLVED_VERSION.version).toEqual(expected.$RESOLVED) - }) + // Next pre-release 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({})