diff --git a/action.yml b/action.yml index 25dbe75b..e9f5a102 100644 --- a/action.yml +++ b/action.yml @@ -28,6 +28,9 @@ inputs: custom_tag: description: "Custom tag name. If specified, it overrides bump settings." required: false + custom_release_rules: + description: "Comma separated list of release rules. Format: `:`. Example: `hotfix:patch,pre-feat:preminor`." + required: false release_branches: description: "Comma separated list of branches (bash reg exp accepted) that will generate the release tags. Other branches and pull-requests generate versions postfixed with the commit hash and do not generate any tag. Examples: `master` or `.*` or `release.*,hotfix.*,master`..." required: false @@ -36,11 +39,11 @@ inputs: description: "Comma separated list of branches (bash reg exp accepted) that will generate pre-release tags." required: false create_annotated_tag: - description: "Boolean to create an annotated tag rather than lightweight" + description: "Boolean to create an annotated tag rather than lightweight." required: false default: "false" dry_run: - description: "Do not perform tagging, just calculate next version and changelog, then exit" + description: "Do not perform tagging, just calculate next version and changelog, then exit." required: false default: "false" diff --git a/src/main.ts b/src/main.ts index 20010d45..42820bbf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ -import * as core from "@actions/core"; -import { gte, inc, parse, ReleaseType, SemVer, valid } from "semver"; -import { analyzeCommits } from "@semantic-release/commit-analyzer"; -import { generateNotes } from "@semantic-release/release-notes-generator"; +import * as core from '@actions/core'; +import { gte, inc, parse, ReleaseType, SemVer, valid } from 'semver'; +import { analyzeCommits } from '@semantic-release/commit-analyzer'; +import { generateNotes } from '@semantic-release/release-notes-generator'; import { getBranchFromRef, getCommits, @@ -9,20 +9,20 @@ import { getLatestTag, getValidTags, mapCustomReleaseRules, -} from "./utils"; -import { createTag } from "./github"; +} from './utils'; +import { createTag } from './github'; export default async () => { try { - const defaultBump = core.getInput("default_bump") as ReleaseType | "false"; - const tagPrefix = core.getInput("tag_prefix"); - const customTag = core.getInput("custom_tag"); - const releaseBranches = core.getInput("release_branches"); - const preReleaseBranches = core.getInput("pre_release_branches"); - const appendToPreReleaseTag = core.getInput("append_to_pre_release_tag"); - const createAnnotatedTag = !!core.getInput("create_annotated_tag"); - const dryRun = core.getInput("dry_run"); - const customReleaseRules = core.getInput("custom_release_rules"); + const defaultBump = core.getInput('default_bump') as ReleaseType | 'false'; + const tagPrefix = core.getInput('tag_prefix'); + const customTag = core.getInput('custom_tag'); + const releaseBranches = core.getInput('release_branches'); + const preReleaseBranches = core.getInput('pre_release_branches'); + const appendToPreReleaseTag = core.getInput('append_to_pre_release_tag'); + const createAnnotatedTag = !!core.getInput('create_annotated_tag'); + const dryRun = core.getInput('dry_run'); + const customReleaseRules = core.getInput('custom_release_rules'); let mappedReleaseRules; if (customReleaseRules) { @@ -32,21 +32,21 @@ export default async () => { const { GITHUB_REF, GITHUB_SHA } = process.env; if (!GITHUB_REF) { - core.setFailed("Missing GITHUB_REF."); + core.setFailed('Missing GITHUB_REF.'); return; } if (!GITHUB_SHA) { - core.setFailed("Missing GITHUB_SHA."); + core.setFailed('Missing GITHUB_SHA.'); return; } const currentBranch = getBranchFromRef(GITHUB_REF); const isReleaseBranch = releaseBranches - .split(",") + .split(',') .some((branch) => currentBranch.match(branch)); const isPreReleaseBranch = preReleaseBranches - .split(",") + .split(',') .some((branch) => currentBranch.match(branch)); const isPrerelease = !isReleaseBranch && isPreReleaseBranch; @@ -55,7 +55,7 @@ export default async () => { const latestTag = getLatestTag(validTags); const latestPrereleaseTag = getLatestPrereleaseTag( validTags, - identifier + identifier, ); const commits = await getCommits(latestTag.commit.sha); @@ -72,30 +72,34 @@ export default async () => { previousTag = parse( gte(latestTag.name, latestPrereleaseTag.name) ? latestTag.name - : latestPrereleaseTag.name + : latestPrereleaseTag.name, ); } if (!previousTag) { - core.setFailed("Could not parse previous tag."); + core.setFailed('Could not parse previous tag.'); return; } core.info(`Previous tag was ${previousTag}.`); - core.setOutput("previous_tag", previousTag.version); + core.setOutput('previous_tag', previousTag.version); - const bump = await analyzeCommits( + let bump = await analyzeCommits( { releaseRules: mappedReleaseRules }, - { commits, logger: { log: console.info.bind(console) } } + { commits, logger: { log: console.info.bind(console) } }, ); - if (!bump && defaultBump === "false") { - core.debug( - "No commit specifies the version bump. Skipping the tag creation." - ); + if (!bump && defaultBump === 'false') { + core.debug('No commit specifies the version bump. Skipping the tag creation.'); return; } + // If somebody uses custom release rules on a prerelease branch they might create a 'preprepatch' bump. + const preReg = /^pre/; + if (isPrerelease && preReg.test(bump)) { + bump = bump.replace(preReg,''); + } + const releaseType: ReleaseType = isPrerelease ? `pre${bump || defaultBump}` : bump || defaultBump; @@ -103,11 +107,11 @@ export default async () => { const incrementedVersion = inc( previousTag, releaseType, - identifier + identifier, ); if (!incrementedVersion) { - core.setFailed("Could not increment version."); + core.setFailed('Could not increment version.'); return; } @@ -120,11 +124,11 @@ export default async () => { } core.info(`New version is ${newVersion}.`); - core.setOutput("new_version", newVersion); + core.setOutput('new_version', newVersion); const newTag = `${tagPrefix}${newVersion}`; core.info(`New tag after applying prefix is ${newTag}.`); - core.setOutput("new_tag", newTag); + core.setOutput('new_tag', newTag); const changelog = await generateNotes( {}, @@ -136,25 +140,25 @@ export default async () => { }, lastRelease: { gitTag: latestTag.name }, nextRelease: { gitTag: newTag, version: newVersion }, - } + }, ); core.info(`Changelog is ${changelog}.`); - core.setOutput("changelog", changelog); + core.setOutput('changelog', changelog); if (!isReleaseBranch && !isPreReleaseBranch) { core.info( - "This branch is neither a release nor a pre-release branch. Skipping the tag creation." + 'This branch is neither a release nor a pre-release branch. Skipping the tag creation.', ); return; } if (validTags.map((tag) => tag.name).includes(newTag)) { - core.info("This tag already exists. Skipping the tag creation."); + core.info('This tag already exists. Skipping the tag creation.'); return; } if (/true/i.test(dryRun)) { - core.info("Dry run: not performing tag action."); + core.info('Dry run: not performing tag action.'); return; } diff --git a/tests/main.test.ts b/tests/main.test.ts index 51c2fe04..0ff4381d 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -126,6 +126,33 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith('v2.0.0', expect.any(Boolean), expect.any(String)); expect(mockSetFailed).not.toBeCalled(); }); + + it('does create tag using custom release types but non-custom commit message', async () => { + /* + * Given + */ + setInput('custom_release_rules', 'james:patch,bond:major'); + const commits = [{ message: 'fix: is the new cool guy' }, { message: 'feat: is his last name' }]; + jest + .spyOn(utils, 'getCommits') + .mockImplementation(async (sha) => commits); + + const validTags = [{ name: 'v1.2.3', commit: { sha: '012345' } }]; + jest + .spyOn(utils, 'getValidTags') + .mockImplementation(async () => validTags); + + /* + * When + */ + await main(); + + /* + * Then + */ + expect(mockCreateTag).toHaveBeenCalledWith('v1.3.0', expect.any(Boolean), expect.any(String)); + expect(mockSetFailed).not.toBeCalled(); + }); }); describe('release branches', () => { @@ -212,6 +239,66 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith('v2.0.0', expect.any(Boolean), expect.any(String)); expect(mockSetFailed).not.toBeCalled(); }); + + it('does create tag when pre-release tag is newer', async () => { + /* + * Given + */ + const commits = [{ message: 'feat: some new feature on a release branch' }]; + jest + .spyOn(utils, 'getCommits') + .mockImplementation(async (sha) => commits); + + const validTags = [ + { name: 'v1.2.3', commit: { sha: '012345' } }, + { name: 'v2.1.3-prerelease.0', commit: { sha: '678901' } }, + { name: 'v2.1.3-prerelease.1', commit: { sha: '234567' } }, + ]; + jest + .spyOn(utils, 'getValidTags') + .mockImplementation(async () => validTags); + + /* + * When + */ + await main(); + + /* + * Then + */ + expect(mockCreateTag).toHaveBeenCalledWith('v2.2.0', expect.any(Boolean), expect.any(String)); + expect(mockSetFailed).not.toBeCalled(); + }); + + it('does create tag with custom release rules', async () => { + /* + * Given + */ + setInput('custom_release_rules', 'james:preminor') + const commits = [ + { message: 'feat: some new feature on a pre-release branch' }, + { message: 'james: this should make a preminor' }, + ]; + jest + .spyOn(utils, 'getCommits') + .mockImplementation(async (sha) => commits); + + const validTags = [{ name: 'v1.2.3', commit: { sha: '012345' } }]; + jest + .spyOn(utils, 'getValidTags') + .mockImplementation(async () => validTags); + + /* + * When + */ + await main(); + + /* + * Then + */ + expect(mockCreateTag).toHaveBeenCalledWith('v1.3.0', expect.any(Boolean), expect.any(String)); + expect(mockSetFailed).not.toBeCalled(); + }); }); describe('pre-release branches', () => { @@ -298,6 +385,66 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith('v2.0.0-prerelease.0', expect.any(Boolean), expect.any(String)); expect(mockSetFailed).not.toBeCalled(); }); + + it('does create tag when release tag is newer', async () => { + /* + * Given + */ + const commits = [{ message: 'feat: some new feature on a pre-release branch' }]; + jest + .spyOn(utils, 'getCommits') + .mockImplementation(async (sha) => commits); + + const validTags = [ + { name: 'v1.2.3-prerelease.0', commit: { sha: '012345' } }, + { name: 'v3.1.2-feature.0', commit: { sha: '012345' } }, + { name: 'v2.1.4', commit: { sha: '234567' } }, + ]; + jest + .spyOn(utils, 'getValidTags') + .mockImplementation(async () => validTags); + + /* + * When + */ + await main(); + + /* + * Then + */ + expect(mockCreateTag).toHaveBeenCalledWith('v2.2.0-prerelease.0', expect.any(Boolean), expect.any(String)); + expect(mockSetFailed).not.toBeCalled(); + }); + + it('does create tag with custom release rules', async () => { + /* + * Given + */ + setInput('custom_release_rules', 'james:preminor') + const commits = [ + { message: 'feat: some new feature on a pre-release branch' }, + { message: 'james: this should make a preminor' }, + ]; + jest + .spyOn(utils, 'getCommits') + .mockImplementation(async (sha) => commits); + + const validTags = [{ name: 'v1.2.3', commit: { sha: '012345' } }]; + jest + .spyOn(utils, 'getValidTags') + .mockImplementation(async () => validTags); + + /* + * When + */ + await main(); + + /* + * Then + */ + expect(mockCreateTag).toHaveBeenCalledWith('v1.3.0-prerelease.0', expect.any(Boolean), expect.any(String)); + expect(mockSetFailed).not.toBeCalled(); + }); }); describe('other branches', () => {