Repository: release-drafter/release-drafter
Pull request #1303 (2023-01-22 09:40:50)

feat: add prerelease increment behavior
Allows to increment the prerelease number (https://semver.org/#spec-item-9).
It does NOT change ANY of the actual behavior
It introduces a new config variable or input "prerelease-identifier"

The prerelease number is increment ONLY when:
- prerelease is true
- prerelease-identifier is not empty

neilime: Hi @jetersen, any chance to review this PR, when you'll have time?

Thanks
jetersen: @neilime I think we are missing an update to action.yaml 🤔
neilime: > @neilime I think we are missing an update to [action.yml](https://github.com/release-drafter/release-drafter/blob/master/action.yml) thinking

@jetersen Which omission are you referring to?
jetersen: `pre-release-identifier` ? Would be useful to have in action as input option?

instead of only inside `config.yml`
jetersen: Reason I ask I could forsee people wanting the flexibility to determine the prerelease idenfier based on data and scripts in a GitHub action 🤔
neilime: > Reason I ask I could foresee people wanting the flexibility to determine the prerelease idenfier based on data and scripts in a GitHub action thinking

Yes sure, I'm on it
wadevries: @neilime thanks for this change, it almost fixes my use case where I want to maintain separate draft releases for next release candidate and next final release. I've made a PR https://github.com/neilime/release-drafter/pull/1 based on your fork.
jaap3: I've added the pre-release-identifier input to our fork, https://github.com/leukeleu/release-drafter/commit/e607143f76df11606933158d3e72fc6ef2afa6e3 feel free to cherry pick that commit.
jetersen: > Minor nit: the naming of `pre-release-identifier` is inconsistent with `prerelease`. Would be nice if `pre-release`/`prerelease` was spelled in a consistent manner.

I concur we should attempt to be consistent here.
neilime: > > Minor nit: the naming of `pre-release-identifier` is inconsistent with `prerelease`. Would be nice if `pre-release`/`prerelease` was spelled in a consistent manner.
> 
> I concur we should attempt to be consistent here.

FYI I've based the name to be consistent with `include-pre-releases` configuration key
neilime: @jetersen I've made a separate commit to handle naming of "prerelease-identifier" and input

Patch:
diff --git a/README.md b/README.md
index 2170659..ea63cff 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 546a96e..8ad4b1b 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 f961794..96194ee 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 635efc9..3f3366b 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 74611b8..22af5fc 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 bcb6f4c..1f22d6e 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 023a609..7f0d42c 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 83c8dbd..22ea400 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 fdaa5f1..326c7c6 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": [
         {


Test patch:
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 0000000..0ac5971
--- /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 4a44768..443b952 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 3d8e80f..67b329f 100644
--- a/test/releases.test.js
+++ b/test/releases.test.js
@@ -211,6 +211,7 @@ describe('releases', () => {
         </details>"
       `)
     })
+
     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 4cb929b..8bae0c3 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({})