diff --git a/README.md b/README.md index d7130ee..0dc5202 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,6 @@ Specify the action as part of your GitHub actions workflow: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -By default the action will try to automatically retrieve the `tag` from the current commit and automatically resolve the `tag` before. Automatic previous tag resolving is done using `semver`. - -If you require a different versioning scheme please open an issue [issue](https://github.com/mikepenz/release-changelog-builder-action/issues). Alternative you can always specifically supply the `fromTag` via the [configuration](#advanced-workflow-specification). - ### Action outputs After action execution it will return the `changelog` and additional information as step output. You can use it in any follow-up step by referencing the output by referencing it via the id of the step. For example `build_changelog`. @@ -135,40 +131,45 @@ The action supports flexible configuration options to modify vast areas of its b GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` +๐Ÿ’ก By default not specifying `fromTag` or `toTag` will resolve `toTag` from either the `ref` or alternatively fallback to the latest tag from git. `fromTag` is resolved by sorting tags using [semver](https://semver.org/). Check the [configuration](#configuration-specification) for alternatives. + This configuration is a `.json` file in the following format. ```json { "categories": [ - { - "title": "## ๐Ÿš€ Features", - "labels": ["feature"] - }, - { - "title": "## ๐Ÿ› Fixes", - "labels": ["fix"] - }, - { - "title": "## ๐Ÿงช Tests", - "labels": ["test"] - } + { + "title": "## ๐Ÿš€ Features", + "labels": ["feature"] + }, + { + "title": "## ๐Ÿ› Fixes", + "labels": ["fix"] + }, + { + "title": "## ๐Ÿงช Tests", + "labels": ["test"] + } ], "sort": "ASC", "template": "${{CHANGELOG}}\n\n
\nUncategorized\n\n${{UNCATEGORIZED}}\n
", "pr_template": "- ${{TITLE}}\n - PR: #${{NUMBER}}", "empty_template": "- no changes", "transformers": [ - { - "pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)", - "target": "- $4\n - $6" - } + { + "pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)", + "target": "- $4\n - $6" + } ], "max_tags_to_fetch": 200, "max_pull_requests": 200, "max_back_track_time_days": 90, "exclude_merge_branches": [ - "Owner/qa" - ] + "Owner/qa" + ], + "tag_resolver": { + "method": "semver" + } } ``` @@ -266,6 +267,8 @@ Table of descriptions for the `configuration.json` options. | max_pull_requests | The maximum amount of pull requests to load from the API. Loaded paginated with 30 per page | | max_back_track_time_days | Defines the max amount of days to go back in time per changelog | | exclude_merge_branches | An array of branches to be ignored from processing as merge commits | +| tag_resolver | Section to provide configuration for the tag resolving logic. Used if no `fromTag` is provided | +| tag_resolver.method | Defines the method to use. Current options are: `semver`, `sort`. Default: `semver` | ## Contribute ๐Ÿงฌ diff --git a/__tests__/tags.test.ts b/__tests__/tags.test.ts index d82b09c..edaeab1 100644 --- a/__tests__/tags.test.ts +++ b/__tests__/tags.test.ts @@ -2,7 +2,7 @@ import { resolveConfiguration } from '../src/utils'; import { ReleaseNotesBuilder } from '../src/releaseNotesBuilder'; import { TagInfo, sortTags } from '../src/tags'; -it('Should order tags correctly', async () => { +it('Should order tags correctly using semver', async () => { jest.setTimeout(180000) const tags: TagInfo[] = [ @@ -16,7 +16,10 @@ it('Should order tags correctly', async () => { { name: "v2020.3.0", commit: "" } ] - const sorted = sortTags(tags).map(function (tag) { + const tagResolver = { + method: "semver" + } + const sorted = sortTags(tags, tagResolver).map(function (tag) { return tag.name }).join(",") @@ -24,7 +27,7 @@ it('Should order tags correctly', async () => { }) -it('Should order tags correctly', async () => { +it('Should order tags correctly using semver', async () => { jest.setTimeout(180000) const tags: TagInfo[] = [ @@ -43,9 +46,42 @@ it('Should order tags correctly', async () => { { name: "1000.0.0", commit: "" }, ] - const sorted = sortTags(tags).map(function (tag) { + const tagResolver = { + method: "non-existing-method" + } + const sorted = sortTags(tags, tagResolver).map(function (tag) { return tag.name }).join(",") expect(sorted).toStrictEqual(`1000.0.0,100.0.0,20.0.2,10.1.0,10.1.0-2,10.0.0,2.0.0,1.0.0,1.0.0-a01,0.1.0,0.1.0-b01,0.0.1,0.0.1-rc01`) +}) + + +it('Should order tags alphabetical', async () => { + jest.setTimeout(180000) + + const tags: TagInfo[] = [ + { name: "0.0.1", commit: "" }, + { name: "0.0.1-rc01", commit: "" }, + { name: "0.1.0-b01", commit: "" }, + { name: "1.0.0", commit: "" }, + { name: "a", commit: "" }, + { name: "1.0.0-a01", commit: "" }, + { name: "2.0.0", commit: "" }, + { name: "10.0.0", commit: "" }, + { name: "v1", commit: "" }, + { name: "10.1.0", commit: "" }, + { name: "10.1.0-2", commit: "" }, + { name: "20.0.2", commit: "" }, + { name: "1000.0.0", commit: "" }, + ] + + const tagResolver = { + method: "sort" + } + const sorted = sortTags(tags, tagResolver).map(function (tag) { + return tag.name + }).join(",") + + expect(sorted).toStrictEqual(`a,20.0.2,2.0.0,1000.0.0,10.1.0,10.1.0-2,10.0.0,1.0.0,1.0.0-a01,v1,0.1.0-b01,0.0.1,0.0.1-rc01`) }) \ No newline at end of file diff --git a/src/configuration.ts b/src/configuration.ts index e2c25af..4e421f3 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -9,6 +9,7 @@ export interface Configuration { empty_template: string categories: Category[] transformers: Transformer[] + tag_resolver: TagResolver } export interface Category { @@ -21,6 +22,10 @@ export interface Transformer { target: string } +export interface TagResolver { + method: string // semver, sort +} + export const DefaultConfiguration: Configuration = { max_tags_to_fetch: 200, // the amount of tags to fetch from the github API max_pull_requests: 200, // the amount of pull requests to process @@ -44,5 +49,9 @@ export const DefaultConfiguration: Configuration = { labels: ['test'] } ], // the categories to support for the ordering - transformers: [] // transformers to apply on the PR description according to the `pr_template` + transformers: [], // transformers to apply on the PR description according to the `pr_template` + tag_resolver: { + // defines the logic on how to resolve the previous tag, only relevant if `fromTag` is not specified + method: 'semver' // defines which method to use, by default it will use `semver` (dropping all non matching tags). Alternative `sort` is also available. + } } diff --git a/src/releaseNotesBuilder.ts b/src/releaseNotesBuilder.ts index 2b9aafd..a229a79 100644 --- a/src/releaseNotesBuilder.ts +++ b/src/releaseNotesBuilder.ts @@ -83,7 +83,8 @@ export class ReleaseNotesBuilder { this.toTag, this.ignorePreReleases, this.configuration.max_tags_to_fetch || - DefaultConfiguration.max_tags_to_fetch + DefaultConfiguration.max_tags_to_fetch, + this.configuration.tag_resolver || DefaultConfiguration.tag_resolver ) if (previousTag == null) { failOrError( diff --git a/src/tags.ts b/src/tags.ts index 4bd1c17..07c7b5d 100755 --- a/src/tags.ts +++ b/src/tags.ts @@ -2,6 +2,7 @@ import {Octokit, RestEndpointMethodTypes} from '@octokit/rest' import * as core from '@actions/core' import * as semver from 'semver' import {SemVer} from 'semver' +import {TagResolver} from './configuration' export interface TagInfo { name: string @@ -52,9 +53,13 @@ export class Tags { repo: string, tag: string, ignorePreReleases: boolean, - maxTagsToFetch: number + maxTagsToFetch: number, + tagResolver: TagResolver ): Promise { - const tags = sortTags(await this.getTags(owner, repo, maxTagsToFetch)) + const tags = sortTags( + await this.getTags(owner, repo, maxTagsToFetch), + tagResolver + ) try { const length = tags.length @@ -94,12 +99,22 @@ export class Tags { 2020.3.1-a01 2020.3.0 */ -export function sortTags(tags: TagInfo[]): TagInfo[] { +export function sortTags(tags: TagInfo[], tagResolver: TagResolver): TagInfo[] { + if (tagResolver.method === 'sort') { + return stringSorting(tags) + } else { + return semVerSorting(tags) + } +} + +function semVerSorting(tags: TagInfo[]): TagInfo[] { // filter out tags which do not follow semver const validatedTags = tags.filter(tag => { const isValid = semver.valid(tag.name) !== null - if(!isValid) { - core.debug(`โš ๏ธ dropped tag ${tag.name} because it is not a valid semver tag`) + if (!isValid) { + core.debug( + `โš ๏ธ dropped tag ${tag.name} because it is not a valid semver tag` + ) } return isValid }) @@ -110,3 +125,22 @@ export function sortTags(tags: TagInfo[]): TagInfo[] { }) return validatedTags } + +function stringSorting(tags: TagInfo[]): TagInfo[] { + return tags.sort((b, a) => { + const partsA = a.name.replace(/^v/, '').split('-') + const partsB = b.name.replace(/^v/, '').split('-') + const versionCompare = partsA[0].localeCompare(partsB[0]) + if (versionCompare !== 0) { + return versionCompare + } else { + if (partsA.length === 1) { + return 0 + } else if (partsB.length === 1) { + return 1 + } else { + return partsA[1].localeCompare(partsB[1]) + } + } + }) +}