├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── action.yml ├── docs └── how-to-publish-new-version.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── action.ts ├── defaults.ts ├── github.ts ├── main.ts ├── ts.ts └── utils.ts ├── tests ├── action.test.ts ├── github.test.ts ├── helper.test.ts └── utils.test.ts ├── tsconfig.json └── types └── semantic.d.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mathieudutour] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test typescript-action" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version-file: package.json 16 | - run: npm ci 17 | - run: npm run test 18 | - run: npm run check 19 | - run: npm run build 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __tests__/runner/* 2 | 3 | # comment out in distribution branches 4 | node_modules/ 5 | lib/ 6 | 7 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # next.js build output 79 | .next 80 | 81 | # nuxt.js build output 82 | .nuxt 83 | 84 | # vuepress build output 85 | .vuepress/dist 86 | 87 | # Serverless directories 88 | .serverless/ 89 | 90 | # FuseBox cache 91 | .fusebox/ 92 | 93 | # DynamoDB Local files 94 | .dynamodb/ 95 | 96 | # IntelliJ 97 | .idea/ 98 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /lib/ 4 | /docs/ 5 | /*.json 6 | /*.js 7 | /*.yml 8 | /*.md 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "parser": "typescript" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellright.language": [ 3 | "en" 4 | ], 5 | "spellright.documentTypes": [ 6 | "markdown", 7 | "latex", 8 | "plaintext" 9 | ] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 GitHub, Inc. and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Tag Action 2 | 3 | A GitHub Action to automatically bump and tag master, on merge, with the latest SemVer formatted version. Works on any platform. 4 | 5 | ## Usage 6 | 7 | ```yaml 8 | name: Bump version 9 | on: 10 | push: 11 | branches: 12 | - master 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Bump version and push tag 19 | id: tag_version 20 | uses: mathieudutour/github-tag-action@v6.2 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | - name: Create a GitHub release 24 | uses: ncipollo/release-action@v1 25 | with: 26 | tag: ${{ steps.tag_version.outputs.new_tag }} 27 | name: Release ${{ steps.tag_version.outputs.new_tag }} 28 | body: ${{ steps.tag_version.outputs.changelog }} 29 | ``` 30 | 31 | ### 📥 Inputs 32 | 33 | - **github_token** _(required)_ - Required for permission to tag the repo. Usually `${{ secrets.GITHUB_TOKEN }}`. 34 | - **commit_sha** _(optional)_ - The commit SHA value to add the tag. If specified, it uses this value instead GITHUB_SHA. It could be useful when a previous step merged a branch into github.ref. 35 | 36 | #### Fetch all tags 37 | 38 | - **fetch_all_tags** _(optional)_ - By default, this action fetch the last 100 tags from Github. Sometimes, this is not enough and using this action will fetch all tags recursively (default: `false`). 39 | 40 | #### Filter branches 41 | 42 | - **release_branches** _(optional)_ - Comma separated list of branches (JavaScript regular expression accepted) that will generate the release tags. Other branches and pull-requests generate versions postfixed with the commit hash and do not generate any repository tag. Examples: `master` or `.*` or `release.*,hotfix.*,master`... (default: `master,main`). 43 | - **pre_release_branches** _(optional)_ - Comma separated list of branches (JavaScript regular expression accepted) that will generate the pre-release tags. 44 | 45 | #### Customize the tag 46 | 47 | - **default_bump** _(optional)_ - Which type of bump to use when [none is explicitly provided](#bumping) when commiting to a release branch (default: `patch`). You can also set `false` to avoid generating a new tag when none is explicitly provided. Can be `patch, minor or major`. 48 | - **default_prerelease_bump** _(optional)_ - Which type of bump to use when [none is explicitly provided](#bumping) when commiting to a prerelease branch (default: `prerelease`). You can also set `false` to avoid generating a new tag when none is explicitly provided. Can be `prerelease, prepatch, preminor or premajor`. 49 | - **custom_tag** _(optional)_ - Custom tag name. If specified, it overrides bump settings. 50 | - **create_annotated_tag** _(optional)_ - Boolean to create an annotated rather than a lightweight one (default: `false`). 51 | - **tag_prefix** _(optional)_ - A prefix to the tag name (default: `v`). 52 | - **append_to_pre_release_tag** _(optional)_ - A suffix to the pre-release tag name (default: ``). 53 | 54 | #### Customize the conventional commit messages & titles of changelog sections 55 | 56 | - **custom_release_rules** _(optional)_ - Comma separated list of release rules. 57 | 58 | __Format__: `::` where `` is optional and will default to [Angular's conventions](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 59 | 60 | __Examples__: 61 | 1. `hotfix:patch,pre-feat:preminor`, 62 | 2. `bug:patch:Bug Fixes,chore:patch:Chores` 63 | 64 | #### Debugging 65 | 66 | - **dry_run** _(optional)_ - Do not perform tagging, just calculate next version and changelog, then exit 67 | 68 | ### 📤 Outputs 69 | 70 | - **new_tag** - The value of the newly calculated tag. Note that if there hasn't been any new commit, this will be `undefined`. 71 | - **new_version** - The value of the newly created tag without the prefix. Note that if there hasn't been any new commit, this will be `undefined`. 72 | - **previous_tag** - The value of the previous tag (or `v0.0.0` if none). Note that if `custom_tag` is set, this will be `undefined`. 73 | - **previous_version** - The value of the previous tag (or `0.0.0` if none) without the prefix. Note that if `custom_tag` is set, this will be `undefined`. 74 | - **release_type** - The computed release type (`major`, `minor`, `patch` or `custom` - can be prefixed with `pre`). 75 | - **changelog** - The [conventional changelog](https://github.com/conventional-changelog/conventional-changelog) since the previous tag. 76 | 77 | > **_Note:_** This action creates a [lightweight tag](https://developer.github.com/v3/git/refs/#create-a-reference) by default. 78 | 79 | ### Bumping 80 | 81 | The action will parse the new commits since the last tag using the [semantic-release](https://github.com/semantic-release/semantic-release) conventions. 82 | 83 | semantic-release uses the commit messages to determine the type of changes in the codebase. Following formalized conventions for commit messages, semantic-release automatically determines the next [semantic version](https://semver.org) number. 84 | 85 | By default semantic-release uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). 86 | 87 | Here is an example of the release type that will be done based on a commit messages: 88 | 89 | 90 | 91 | 92 | 93 | 94 | 101 | 102 | 103 | 104 | 111 | 112 | 113 | 114 | 124 | 125 | 126 |
Commit message Release type
95 | 96 | ``` 97 | fix(pencil): stop graphite breaking when too much pressure applied 98 | ``` 99 | 100 | Patch Release
105 | 106 | ``` 107 | feat(pencil): add 'graphiteWidth' option 108 | ``` 109 | 110 | Minor Release
115 | 116 | ``` 117 | perf(pencil): remove graphiteWidth option 118 | 119 | BREAKING CHANGE: The graphiteWidth option has been removed. 120 | The default graphite width of 10mm is always used for performance reasons. 121 | ``` 122 | 123 | Major Release
127 | 128 | If no commit message contains any information, then **default_bump** will be used. 129 | 130 | ## Credits 131 | 132 | [anothrNick/github-tag-action](https://github.com/anothrNick/github-tag-action) - a similar action using a Dockerfile (hence not working on macOS) 133 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "GitHub Tag" 2 | description: "Bump and push git tag on merge" 3 | author: "Mathieu Dutour" 4 | outputs: 5 | new_tag: 6 | description: "Generated tag" 7 | new_version: 8 | description: "Generated tag without the prefix" 9 | previous_tag: 10 | description: "Previous tag (or `0.0.0`)" 11 | previous_version: 12 | description: "The value of the previous tag (or 0.0.0 if none) without the prefix. Note that if custom_tag is set, this will be undefined." 13 | release_type: 14 | description: "The computed release type (`major`, `minor`, `patch` or `custom` - can be prefixed with `pre`)" 15 | changelog: 16 | description: "The conventional changelog since the previous tag" 17 | inputs: 18 | github_token: 19 | description: "Required for permission to tag the repo." 20 | required: true 21 | default_bump: 22 | description: "Which type of bump to use when none explicitly provided when commiting to a release branch (default: `patch`)." 23 | required: false 24 | default: "patch" 25 | default_prerelease_bump: 26 | description: "Which type of bump to use when none explicitly provided when commiting to a prerelease branch (default: `prerelease`)." 27 | required: false 28 | default: "prerelease" 29 | tag_prefix: 30 | description: "A prefix to the tag name (default: `v`)." 31 | required: false 32 | default: "v" 33 | append_to_pre_release_tag: 34 | description: "A suffix to a pre-release tag name (default: ``)." 35 | required: false 36 | custom_tag: 37 | description: "Custom tag name. If specified, it overrides bump settings." 38 | required: false 39 | custom_release_rules: 40 | description: "Comma separated list of release rules. Format: `:`. Example: `hotfix:patch,pre-feat:preminor`." 41 | required: false 42 | release_branches: 43 | 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`..." 44 | required: false 45 | default: "master,main" 46 | pre_release_branches: 47 | description: "Comma separated list of branches (bash reg exp accepted) that will generate pre-release tags." 48 | required: false 49 | commit_sha: 50 | description: "The commit SHA value to add the tag. If specified, it uses this value instead GITHUB_SHA. It could be useful when a previous step merged a branch into github.ref." 51 | required: false 52 | create_annotated_tag: 53 | description: "Boolean to create an annotated tag rather than lightweight." 54 | required: false 55 | default: "false" 56 | fetch_all_tags: 57 | description: "Boolean to fetch all tags for a repo (if false, only the last 100 will be fetched)." 58 | required: false 59 | default: "false" 60 | dry_run: 61 | description: "Do not perform tagging, just calculate next version and changelog, then exit." 62 | required: false 63 | default: "false" 64 | 65 | runs: 66 | using: "node20" 67 | main: "lib/main.js" 68 | branding: 69 | icon: "git-merge" 70 | color: "purple" 71 | -------------------------------------------------------------------------------- /docs/how-to-publish-new-version.md: -------------------------------------------------------------------------------- 1 | # How to publish a new version of the action 2 | 3 | ## Publish to a distribution branch 4 | 5 | Actions are run from GitHub repos. We will create a releases branch and only checkin production modules (core in this case). 6 | 7 | Comment out node_modules in .gitignore and create a releases/v1 branch 8 | 9 | ```bash 10 | # comment out in distribution branches 11 | # node_modules/ 12 | # lib/ 13 | ``` 14 | 15 | ```bash 16 | $ git checkout -b releases/v1 17 | $ npm install 18 | $ npm run build 19 | $ npm prune --production 20 | $ git add . 21 | $ git commit -a -m "prod dependencies" 22 | $ git push 23 | ``` 24 | 25 | Your action is now published! :rocket: 26 | 27 | See the [versioning documentation](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) 28 | 29 | ## Validate 30 | 31 | You can now validate the action by referencing the releases/v1 branch 32 | 33 | ```yaml 34 | uses: mathieudutour/github-tag-action@releases/v1 35 | ``` 36 | 37 | See the [actions tab](https://github.com/actions/javascript-action/actions) for runs of this action! :rocket: 38 | 39 | ## Create a tag 40 | 41 | After testing you can [create a tag](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) to reference the stable and tested action 42 | 43 | ```yaml 44 | uses: mathieudutour/github-tag-action@v1 45 | ``` 46 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-tag-action", 3 | "version": "6.2.0", 4 | "private": true, 5 | "description": "A GitHub Action to automatically bump and tag master, on merge, with the latest SemVer formatted version.", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "jest --testTimeout 10000", 10 | "check": "prettier --check ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mathieudutour/github-tag-action.git" 15 | }, 16 | "keywords": [ 17 | "actions", 18 | "node", 19 | "setup" 20 | ], 21 | "engines": { 22 | "node": ">=20" 23 | }, 24 | "author": "Mathieu Dutour", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@actions/core": "^1.10.0", 28 | "@actions/exec": "^1.1.0", 29 | "@actions/github": "^4.0.0", 30 | "@semantic-release/commit-analyzer": "^8.0.1", 31 | "@semantic-release/release-notes-generator": "^9.0.1", 32 | "conventional-changelog-conventionalcommits": "^4.6.1", 33 | "semver": "^7.3.5" 34 | }, 35 | "devDependencies": { 36 | "@octokit/rest": "^18.12.0", 37 | "@types/jest": "^27.0.2", 38 | "@types/js-yaml": "^4.0.4", 39 | "@types/node": "^20.11.16", 40 | "@types/semver": "^7.3.9", 41 | "jest": "^27.3.1", 42 | "jest-circus": "^27.3.1", 43 | "js-yaml": "^4.1.0", 44 | "prettier": "2.4.1", 45 | "ts-jest": "^27.0.7", 46 | "typescript": "^4.4.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { gte, inc, parse, ReleaseType, SemVer, valid } from 'semver'; 3 | import { analyzeCommits } from '@semantic-release/commit-analyzer'; 4 | import { generateNotes } from '@semantic-release/release-notes-generator'; 5 | import { 6 | getBranchFromRef, 7 | isPr, 8 | getCommits, 9 | getLatestPrereleaseTag, 10 | getLatestTag, 11 | getValidTags, 12 | mapCustomReleaseRules, 13 | mergeWithDefaultChangelogRules, 14 | } from './utils'; 15 | import { createTag } from './github'; 16 | import { Await } from './ts'; 17 | 18 | export default async function main() { 19 | const defaultBump = core.getInput('default_bump') as ReleaseType | 'false'; 20 | const defaultPreReleaseBump = core.getInput('default_prerelease_bump') as 21 | | ReleaseType 22 | | 'false'; 23 | const tagPrefix = core.getInput('tag_prefix'); 24 | const customTag = core.getInput('custom_tag'); 25 | const releaseBranches = core.getInput('release_branches'); 26 | const preReleaseBranches = core.getInput('pre_release_branches'); 27 | const appendToPreReleaseTag = core.getInput('append_to_pre_release_tag'); 28 | const createAnnotatedTag = /true/i.test( 29 | core.getInput('create_annotated_tag') 30 | ); 31 | const dryRun = core.getInput('dry_run'); 32 | const customReleaseRules = core.getInput('custom_release_rules'); 33 | const shouldFetchAllTags = core.getInput('fetch_all_tags'); 34 | const commitSha = core.getInput('commit_sha'); 35 | 36 | let mappedReleaseRules; 37 | if (customReleaseRules) { 38 | mappedReleaseRules = mapCustomReleaseRules(customReleaseRules); 39 | } 40 | 41 | const { GITHUB_REF, GITHUB_SHA } = process.env; 42 | 43 | if (!GITHUB_REF) { 44 | core.setFailed('Missing GITHUB_REF.'); 45 | return; 46 | } 47 | 48 | const commitRef = commitSha || GITHUB_SHA; 49 | if (!commitRef) { 50 | core.setFailed('Missing commit_sha or GITHUB_SHA.'); 51 | return; 52 | } 53 | 54 | const currentBranch = getBranchFromRef(GITHUB_REF); 55 | const isReleaseBranch = releaseBranches 56 | .split(',') 57 | .some((branch) => currentBranch.match(branch)); 58 | const isPreReleaseBranch = preReleaseBranches 59 | .split(',') 60 | .some((branch) => currentBranch.match(branch)); 61 | const isPullRequest = isPr(GITHUB_REF); 62 | const isPrerelease = !isReleaseBranch && !isPullRequest && isPreReleaseBranch; 63 | 64 | // Sanitize identifier according to 65 | // https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions 66 | const identifier = ( 67 | appendToPreReleaseTag ? appendToPreReleaseTag : currentBranch 68 | ).replace(/[^a-zA-Z0-9-]/g, '-'); 69 | 70 | const prefixRegex = new RegExp(`^${tagPrefix}`); 71 | 72 | const validTags = await getValidTags( 73 | prefixRegex, 74 | /true/i.test(shouldFetchAllTags) 75 | ); 76 | const latestTag = getLatestTag(validTags, prefixRegex, tagPrefix); 77 | const latestPrereleaseTag = getLatestPrereleaseTag( 78 | validTags, 79 | identifier, 80 | prefixRegex 81 | ); 82 | 83 | let commits: Await>; 84 | 85 | let newVersion: string; 86 | 87 | if (customTag) { 88 | commits = await getCommits(latestTag.commit.sha, commitRef); 89 | 90 | core.setOutput('release_type', 'custom'); 91 | newVersion = customTag; 92 | } else { 93 | let previousTag: ReturnType | null; 94 | let previousVersion: SemVer | null; 95 | if (!latestPrereleaseTag) { 96 | previousTag = latestTag; 97 | } else { 98 | previousTag = gte( 99 | latestTag.name.replace(prefixRegex, ''), 100 | latestPrereleaseTag.name.replace(prefixRegex, '') 101 | ) 102 | ? latestTag 103 | : latestPrereleaseTag; 104 | } 105 | 106 | if (!previousTag) { 107 | core.setFailed('Could not find previous tag.'); 108 | return; 109 | } 110 | 111 | previousVersion = parse(previousTag.name.replace(prefixRegex, '')); 112 | 113 | if (!previousVersion) { 114 | core.setFailed('Could not parse previous tag.'); 115 | return; 116 | } 117 | 118 | core.info( 119 | `Previous tag was ${previousTag.name}, previous version was ${previousVersion.version}.` 120 | ); 121 | core.setOutput('previous_version', previousVersion.version); 122 | core.setOutput('previous_tag', previousTag.name); 123 | 124 | commits = await getCommits(previousTag.commit.sha, commitRef); 125 | 126 | let bump = await analyzeCommits( 127 | { 128 | releaseRules: mappedReleaseRules 129 | ? // analyzeCommits doesn't appreciate rules with a section /shrug 130 | mappedReleaseRules.map(({ section, ...rest }) => ({ ...rest })) 131 | : undefined, 132 | }, 133 | { commits, logger: { log: console.info.bind(console) } } 134 | ); 135 | 136 | // Determine if we should continue with tag creation based on main vs prerelease branch 137 | let shouldContinue = true; 138 | if (isPrerelease) { 139 | if (!bump && defaultPreReleaseBump === 'false') { 140 | shouldContinue = false; 141 | } 142 | } else { 143 | if (!bump && defaultBump === 'false') { 144 | shouldContinue = false; 145 | } 146 | } 147 | 148 | // Default bump is set to false and we did not find an automatic bump 149 | if (!shouldContinue) { 150 | core.debug( 151 | 'No commit specifies the version bump. Skipping the tag creation.' 152 | ); 153 | return; 154 | } 155 | 156 | // If we don't have an automatic bump for the prerelease, just set our bump as the default 157 | if (isPrerelease && !bump) { 158 | bump = defaultPreReleaseBump; 159 | } 160 | 161 | // If somebody uses custom release rules on a prerelease branch they might create a 'preprepatch' bump. 162 | const preReg = /^pre/; 163 | if (isPrerelease && preReg.test(bump)) { 164 | bump = bump.replace(preReg, ''); 165 | } 166 | 167 | const releaseType: ReleaseType = isPrerelease 168 | ? `pre${bump}` 169 | : bump || defaultBump; 170 | core.setOutput('release_type', releaseType); 171 | 172 | const incrementedVersion = inc(previousVersion, releaseType, identifier); 173 | 174 | if (!incrementedVersion) { 175 | core.setFailed('Could not increment version.'); 176 | return; 177 | } 178 | 179 | if (!valid(incrementedVersion)) { 180 | core.setFailed(`${incrementedVersion} is not a valid semver.`); 181 | return; 182 | } 183 | 184 | newVersion = incrementedVersion; 185 | } 186 | 187 | core.info(`New version is ${newVersion}.`); 188 | core.setOutput('new_version', newVersion); 189 | 190 | const newTag = `${tagPrefix}${newVersion}`; 191 | core.info(`New tag after applying prefix is ${newTag}.`); 192 | core.setOutput('new_tag', newTag); 193 | 194 | const changelog = await generateNotes( 195 | { 196 | preset: 'conventionalcommits', 197 | presetConfig: { 198 | types: mergeWithDefaultChangelogRules(mappedReleaseRules), 199 | }, 200 | }, 201 | { 202 | commits, 203 | logger: { log: console.info.bind(console) }, 204 | options: { 205 | repositoryUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}`, 206 | }, 207 | lastRelease: { gitTag: latestTag.name }, 208 | nextRelease: { gitTag: newTag, version: newVersion }, 209 | } 210 | ); 211 | core.info(`Changelog is ${changelog}.`); 212 | core.setOutput('changelog', changelog); 213 | 214 | if (!isReleaseBranch && !isPreReleaseBranch) { 215 | core.info( 216 | 'This branch is neither a release nor a pre-release branch. Skipping the tag creation.' 217 | ); 218 | return; 219 | } 220 | 221 | if (validTags.map((tag) => tag.name).includes(newTag)) { 222 | core.info('This tag already exists. Skipping the tag creation.'); 223 | return; 224 | } 225 | 226 | if (/true/i.test(dryRun)) { 227 | core.info('Dry run: not performing tag action.'); 228 | return; 229 | } 230 | 231 | await createTag(newTag, createAnnotatedTag, commitRef); 232 | } 233 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | type ChangelogRule = { 2 | /** 3 | * Commit type. 4 | * Eg: feat, fix etc. 5 | */ 6 | type: string; 7 | /** 8 | * Section in changelog to group commits by type. 9 | * Eg: 'Bug Fix', 'Features' etc. 10 | */ 11 | section?: string; 12 | }; 13 | 14 | /** 15 | * Default sections & changelog rules mentioned in `conventional-changelog-angular` & `conventional-changelog-conventionalcommits`. 16 | * References: 17 | * https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-angular/writer-opts.js 18 | * https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-conventionalcommits/writer-opts.js 19 | */ 20 | export const defaultChangelogRules: Readonly> = 21 | Object.freeze({ 22 | feat: { type: 'feat', section: 'Features' }, 23 | fix: { type: 'fix', section: 'Bug Fixes' }, 24 | perf: { type: 'perf', section: 'Performance Improvements' }, 25 | revert: { type: 'revert', section: 'Reverts' }, 26 | docs: { type: 'docs', section: 'Documentation' }, 27 | style: { type: 'style', section: 'Styles' }, 28 | refactor: { type: 'refactor', section: 'Code Refactoring' }, 29 | test: { type: 'test', section: 'Tests' }, 30 | build: { type: 'build', section: 'Build Systems' }, 31 | ci: { type: 'ci', section: 'Continuous Integration' }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/github.ts: -------------------------------------------------------------------------------- 1 | import { context, getOctokit } from '@actions/github'; 2 | import * as core from '@actions/core'; 3 | import { Await } from './ts'; 4 | 5 | let octokitSingleton: ReturnType; 6 | 7 | type Tag = { 8 | name: string; 9 | commit: { 10 | sha: string; 11 | url: string; 12 | }; 13 | zipball_url: string; 14 | tarball_url: string; 15 | node_id: string; 16 | }; 17 | 18 | export function getOctokitSingleton() { 19 | if (octokitSingleton) { 20 | return octokitSingleton; 21 | } 22 | const githubToken = core.getInput('github_token'); 23 | octokitSingleton = getOctokit(githubToken); 24 | return octokitSingleton; 25 | } 26 | 27 | /** 28 | * Fetch all tags for a given repository recursively 29 | */ 30 | export async function listTags( 31 | shouldFetchAllTags = false, 32 | fetchedTags: Tag[] = [], 33 | page = 1 34 | ): Promise { 35 | const octokit = getOctokitSingleton(); 36 | 37 | const tags = await octokit.repos.listTags({ 38 | ...context.repo, 39 | per_page: 100, 40 | page, 41 | }); 42 | 43 | if (tags.data.length < 100 || shouldFetchAllTags === false) { 44 | return [...fetchedTags, ...tags.data]; 45 | } 46 | 47 | return listTags(shouldFetchAllTags, [...fetchedTags, ...tags.data], page + 1); 48 | } 49 | 50 | /** 51 | * Compare `headRef` to `baseRef` (i.e. baseRef...headRef) 52 | * @param baseRef - old commit 53 | * @param headRef - new commit 54 | */ 55 | export async function compareCommits(baseRef: string, headRef: string) { 56 | const octokit = getOctokitSingleton(); 57 | core.debug(`Comparing commits (${baseRef}...${headRef})`); 58 | 59 | const commits = await octokit.repos.compareCommits({ 60 | ...context.repo, 61 | base: baseRef, 62 | head: headRef, 63 | }); 64 | 65 | return commits.data.commits; 66 | } 67 | 68 | export async function createTag( 69 | newTag: string, 70 | createAnnotatedTag: boolean, 71 | GITHUB_SHA: string 72 | ) { 73 | const octokit = getOctokitSingleton(); 74 | let annotatedTag: 75 | | Await> 76 | | undefined = undefined; 77 | if (createAnnotatedTag) { 78 | core.debug(`Creating annotated tag.`); 79 | annotatedTag = await octokit.git.createTag({ 80 | ...context.repo, 81 | tag: newTag, 82 | message: newTag, 83 | object: GITHUB_SHA, 84 | type: 'commit', 85 | }); 86 | } 87 | 88 | core.debug(`Pushing new tag to the repo.`); 89 | await octokit.git.createRef({ 90 | ...context.repo, 91 | ref: `refs/tags/${newTag}`, 92 | sha: annotatedTag ? annotatedTag.data.sha : GITHUB_SHA, 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import action from './action'; 3 | 4 | async function run() { 5 | try { 6 | await action(); 7 | } catch (error: any) { 8 | core.setFailed(error.message); 9 | } 10 | } 11 | 12 | run(); 13 | -------------------------------------------------------------------------------- /src/ts.ts: -------------------------------------------------------------------------------- 1 | export type Await = T extends { 2 | then(onfulfilled?: (value: infer U) => unknown): unknown; 3 | } 4 | ? U 5 | : T; 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { prerelease, rcompare, valid } from 'semver'; 3 | // @ts-ignore 4 | import DEFAULT_RELEASE_TYPES from '@semantic-release/commit-analyzer/lib/default-release-types'; 5 | import { compareCommits, listTags } from './github'; 6 | import { defaultChangelogRules } from './defaults'; 7 | import { Await } from './ts'; 8 | 9 | type Tags = Await>; 10 | 11 | export async function getValidTags( 12 | prefixRegex: RegExp, 13 | shouldFetchAllTags: boolean 14 | ) { 15 | const tags = await listTags(shouldFetchAllTags); 16 | 17 | const invalidTags = tags.filter( 18 | (tag) => 19 | !prefixRegex.test(tag.name) || !valid(tag.name.replace(prefixRegex, '')) 20 | ); 21 | 22 | invalidTags.forEach((name) => core.debug(`Found Invalid Tag: ${name}.`)); 23 | 24 | const validTags = tags 25 | .filter( 26 | (tag) => 27 | prefixRegex.test(tag.name) && valid(tag.name.replace(prefixRegex, '')) 28 | ) 29 | .sort((a, b) => 30 | rcompare(a.name.replace(prefixRegex, ''), b.name.replace(prefixRegex, '')) 31 | ); 32 | 33 | validTags.forEach((tag) => core.debug(`Found Valid Tag: ${tag.name}.`)); 34 | 35 | return validTags; 36 | } 37 | 38 | export async function getCommits( 39 | baseRef: string, 40 | headRef: string 41 | ): Promise<{ message: string; hash: string | null }[]> { 42 | const commits = await compareCommits(baseRef, headRef); 43 | 44 | return commits 45 | .filter((commit) => !!commit.commit.message) 46 | .map((commit) => ({ 47 | message: commit.commit.message, 48 | hash: commit.sha, 49 | })); 50 | } 51 | 52 | export function getBranchFromRef(ref: string) { 53 | return ref.replace('refs/heads/', ''); 54 | } 55 | 56 | export function isPr(ref: string) { 57 | return ref.includes('refs/pull/'); 58 | } 59 | 60 | export function getLatestTag( 61 | tags: Tags, 62 | prefixRegex: RegExp, 63 | tagPrefix: string 64 | ) { 65 | return ( 66 | tags.find( 67 | (tag) => 68 | prefixRegex.test(tag.name) && 69 | !prerelease(tag.name.replace(prefixRegex, '')) 70 | ) || { 71 | name: `${tagPrefix}0.0.0`, 72 | commit: { 73 | sha: 'HEAD', 74 | }, 75 | } 76 | ); 77 | } 78 | 79 | export function getLatestPrereleaseTag( 80 | tags: Tags, 81 | identifier: string, 82 | prefixRegex: RegExp 83 | ) { 84 | return tags 85 | .filter((tag) => prerelease(tag.name.replace(prefixRegex, ''))) 86 | .find((tag) => tag.name.replace(prefixRegex, '').match(identifier)); 87 | } 88 | 89 | export function mapCustomReleaseRules(customReleaseTypes: string) { 90 | const releaseRuleSeparator = ','; 91 | const releaseTypeSeparator = ':'; 92 | 93 | return customReleaseTypes 94 | .split(releaseRuleSeparator) 95 | .filter((customReleaseRule) => { 96 | const parts = customReleaseRule.split(releaseTypeSeparator); 97 | 98 | if (parts.length < 2) { 99 | core.warning( 100 | `${customReleaseRule} is not a valid custom release definition.` 101 | ); 102 | return false; 103 | } 104 | 105 | const defaultRule = defaultChangelogRules[parts[0].toLowerCase()]; 106 | if (customReleaseRule.length !== 3) { 107 | core.debug( 108 | `${customReleaseRule} doesn't mention the section for the changelog.` 109 | ); 110 | core.debug( 111 | defaultRule 112 | ? `Default section (${defaultRule.section}) will be used instead.` 113 | : "The commits matching this rule won't be included in the changelog." 114 | ); 115 | } 116 | 117 | if (!DEFAULT_RELEASE_TYPES.includes(parts[1])) { 118 | core.warning(`${parts[1]} is not a valid release type.`); 119 | return false; 120 | } 121 | 122 | return true; 123 | }) 124 | .map((customReleaseRule) => { 125 | const [type, release, section] = 126 | customReleaseRule.split(releaseTypeSeparator); 127 | const defaultRule = defaultChangelogRules[type.toLowerCase()]; 128 | 129 | return { 130 | type, 131 | release, 132 | section: section || defaultRule?.section, 133 | }; 134 | }); 135 | } 136 | 137 | export function mergeWithDefaultChangelogRules( 138 | mappedReleaseRules: ReturnType = [] 139 | ) { 140 | const mergedRules = mappedReleaseRules.reduce( 141 | (acc, curr) => ({ 142 | ...acc, 143 | [curr.type]: curr, 144 | }), 145 | { ...defaultChangelogRules } 146 | ); 147 | 148 | return Object.values(mergedRules).filter((rule) => !!rule.section); 149 | } 150 | -------------------------------------------------------------------------------- /tests/action.test.ts: -------------------------------------------------------------------------------- 1 | import action from '../src/action'; 2 | import * as utils from '../src/utils'; 3 | import * as github from '../src/github'; 4 | import * as core from '@actions/core'; 5 | import { 6 | loadDefaultInputs, 7 | setBranch, 8 | setCommitSha, 9 | setInput, 10 | setRepository, 11 | } from './helper.test'; 12 | 13 | jest.spyOn(core, 'debug').mockImplementation(() => {}); 14 | jest.spyOn(core, 'info').mockImplementation(() => {}); 15 | jest.spyOn(console, 'info').mockImplementation(() => {}); 16 | 17 | beforeAll(() => { 18 | setRepository('https://github.com', 'org/repo'); 19 | }); 20 | 21 | const mockCreateTag = jest 22 | .spyOn(github, 'createTag') 23 | .mockResolvedValue(undefined); 24 | 25 | const mockSetOutput = jest 26 | .spyOn(core, 'setOutput') 27 | .mockImplementation(() => {}); 28 | 29 | const mockSetFailed = jest.spyOn(core, 'setFailed'); 30 | 31 | describe('github-tag-action', () => { 32 | beforeEach(() => { 33 | jest.clearAllMocks(); 34 | setBranch('master'); 35 | setCommitSha('79e0ea271c26aa152beef77c3275ff7b8f8d8274'); 36 | loadDefaultInputs(); 37 | }); 38 | 39 | describe('special cases', () => { 40 | it('does create initial tag', async () => { 41 | /* 42 | * Given 43 | */ 44 | const commits = [{ message: 'fix: this is my first fix', hash: null }]; 45 | jest 46 | .spyOn(utils, 'getCommits') 47 | .mockImplementation(async (sha) => commits); 48 | 49 | const validTags: any[] = []; 50 | jest 51 | .spyOn(utils, 'getValidTags') 52 | .mockImplementation(async () => validTags); 53 | 54 | /* 55 | * When 56 | */ 57 | await action(); 58 | 59 | /* 60 | * Then 61 | */ 62 | expect(mockCreateTag).toHaveBeenCalledWith( 63 | 'v0.0.1', 64 | expect.any(Boolean), 65 | expect.any(String) 66 | ); 67 | expect(mockSetFailed).not.toBeCalled(); 68 | }); 69 | 70 | it('does create patch tag without commits', async () => { 71 | /* 72 | * Given 73 | */ 74 | const commits: any[] = []; 75 | jest 76 | .spyOn(utils, 'getCommits') 77 | .mockImplementation(async (sha) => commits); 78 | 79 | const validTags: any[] = []; 80 | jest 81 | .spyOn(utils, 'getValidTags') 82 | .mockImplementation(async () => validTags); 83 | 84 | /* 85 | * When 86 | */ 87 | await action(); 88 | 89 | /* 90 | * Then 91 | */ 92 | expect(mockCreateTag).toHaveBeenCalledWith( 93 | 'v0.0.1', 94 | expect.any(Boolean), 95 | expect.any(String) 96 | ); 97 | expect(mockSetFailed).not.toBeCalled(); 98 | }); 99 | 100 | it('does not create tag without commits and default_bump set to false', async () => { 101 | /* 102 | * Given 103 | */ 104 | setInput('default_bump', 'false'); 105 | const commits: any[] = []; 106 | jest 107 | .spyOn(utils, 'getCommits') 108 | .mockImplementation(async (sha) => commits); 109 | 110 | const validTags = [ 111 | { 112 | name: 'v1.2.3', 113 | commit: { sha: '012345', url: '' }, 114 | zipball_url: '', 115 | tarball_url: 'string', 116 | node_id: 'string', 117 | }, 118 | ]; 119 | jest 120 | .spyOn(utils, 'getValidTags') 121 | .mockImplementation(async () => validTags); 122 | 123 | /* 124 | * When 125 | */ 126 | await action(); 127 | 128 | /* 129 | * Then 130 | */ 131 | expect(mockCreateTag).not.toBeCalled(); 132 | expect(mockSetFailed).not.toBeCalled(); 133 | }); 134 | 135 | it('does create tag using custom release types', async () => { 136 | /* 137 | * Given 138 | */ 139 | setInput('custom_release_rules', 'james:patch,bond:major'); 140 | const commits = [ 141 | { message: 'james: is the new cool guy', hash: null }, 142 | { message: 'bond: is his last name', hash: null }, 143 | ]; 144 | jest 145 | .spyOn(utils, 'getCommits') 146 | .mockImplementation(async (sha) => commits); 147 | 148 | const validTags = [ 149 | { 150 | name: 'v1.2.3', 151 | commit: { sha: '012345', url: '' }, 152 | zipball_url: '', 153 | tarball_url: 'string', 154 | node_id: 'string', 155 | }, 156 | ]; 157 | jest 158 | .spyOn(utils, 'getValidTags') 159 | .mockImplementation(async () => validTags); 160 | 161 | /* 162 | * When 163 | */ 164 | await action(); 165 | 166 | /* 167 | * Then 168 | */ 169 | expect(mockCreateTag).toHaveBeenCalledWith( 170 | 'v2.0.0', 171 | expect.any(Boolean), 172 | expect.any(String) 173 | ); 174 | expect(mockSetFailed).not.toBeCalled(); 175 | }); 176 | 177 | it('does create tag using custom release types but non-custom commit message', async () => { 178 | /* 179 | * Given 180 | */ 181 | setInput('custom_release_rules', 'james:patch,bond:major'); 182 | const commits = [ 183 | { message: 'fix: is the new cool guy', hash: null }, 184 | { message: 'feat: is his last name', hash: null }, 185 | ]; 186 | jest 187 | .spyOn(utils, 'getCommits') 188 | .mockImplementation(async (sha) => commits); 189 | 190 | const validTags = [ 191 | { 192 | name: 'v1.2.3', 193 | commit: { sha: '012345', url: '' }, 194 | zipball_url: '', 195 | tarball_url: 'string', 196 | node_id: 'string', 197 | }, 198 | ]; 199 | jest 200 | .spyOn(utils, 'getValidTags') 201 | .mockImplementation(async () => validTags); 202 | 203 | /* 204 | * When 205 | */ 206 | await action(); 207 | 208 | /* 209 | * Then 210 | */ 211 | expect(mockCreateTag).toHaveBeenCalledWith( 212 | 'v1.3.0', 213 | expect.any(Boolean), 214 | expect.any(String) 215 | ); 216 | expect(mockSetFailed).not.toBeCalled(); 217 | }); 218 | }); 219 | 220 | describe('release branches', () => { 221 | beforeEach(() => { 222 | jest.clearAllMocks(); 223 | setBranch('release'); 224 | setInput('release_branches', 'release'); 225 | }); 226 | 227 | it('does create patch tag', async () => { 228 | /* 229 | * Given 230 | */ 231 | const commits = [{ message: 'fix: this is my first fix', hash: null }]; 232 | jest 233 | .spyOn(utils, 'getCommits') 234 | .mockImplementation(async (sha) => commits); 235 | 236 | const validTags = [ 237 | { 238 | name: 'v1.2.3', 239 | commit: { sha: '012345', url: '' }, 240 | zipball_url: '', 241 | tarball_url: 'string', 242 | node_id: 'string', 243 | }, 244 | ]; 245 | jest 246 | .spyOn(utils, 'getValidTags') 247 | .mockImplementation(async () => validTags); 248 | 249 | /* 250 | * When 251 | */ 252 | await action(); 253 | 254 | /* 255 | * Then 256 | */ 257 | expect(mockCreateTag).toHaveBeenCalledWith( 258 | 'v1.2.4', 259 | expect.any(Boolean), 260 | expect.any(String) 261 | ); 262 | expect(mockSetFailed).not.toBeCalled(); 263 | }); 264 | 265 | it('does create minor tag', async () => { 266 | /* 267 | * Given 268 | */ 269 | const commits = [ 270 | { message: 'feat: this is my first feature', hash: null }, 271 | ]; 272 | jest 273 | .spyOn(utils, 'getCommits') 274 | .mockImplementation(async (sha) => commits); 275 | 276 | const validTags = [ 277 | { 278 | name: 'v1.2.3', 279 | commit: { sha: '012345', url: '' }, 280 | zipball_url: '', 281 | tarball_url: 'string', 282 | node_id: 'string', 283 | }, 284 | ]; 285 | jest 286 | .spyOn(utils, 'getValidTags') 287 | .mockImplementation(async () => validTags); 288 | 289 | /* 290 | * When 291 | */ 292 | await action(); 293 | 294 | /* 295 | * Then 296 | */ 297 | expect(mockCreateTag).toHaveBeenCalledWith( 298 | 'v1.3.0', 299 | expect.any(Boolean), 300 | expect.any(String) 301 | ); 302 | expect(mockSetFailed).not.toBeCalled(); 303 | }); 304 | 305 | it('does create major tag', async () => { 306 | /* 307 | * Given 308 | */ 309 | const commits = [ 310 | { 311 | message: 312 | 'my commit message\nBREAKING CHANGE:\nthis is a breaking change', 313 | hash: null, 314 | }, 315 | ]; 316 | jest 317 | .spyOn(utils, 'getCommits') 318 | .mockImplementation(async (sha) => commits); 319 | 320 | const validTags = [ 321 | { 322 | name: 'v1.2.3', 323 | commit: { sha: '012345', url: '' }, 324 | zipball_url: '', 325 | tarball_url: 'string', 326 | node_id: 'string', 327 | }, 328 | ]; 329 | jest 330 | .spyOn(utils, 'getValidTags') 331 | .mockImplementation(async () => validTags); 332 | 333 | /* 334 | * When 335 | */ 336 | await action(); 337 | 338 | /* 339 | * Then 340 | */ 341 | expect(mockCreateTag).toHaveBeenCalledWith( 342 | 'v2.0.0', 343 | expect.any(Boolean), 344 | expect.any(String) 345 | ); 346 | expect(mockSetFailed).not.toBeCalled(); 347 | }); 348 | 349 | it('does create tag when pre-release tag is newer', async () => { 350 | /* 351 | * Given 352 | */ 353 | const commits = [ 354 | { message: 'feat: some new feature on a release branch', hash: null }, 355 | ]; 356 | jest 357 | .spyOn(utils, 'getCommits') 358 | .mockImplementation(async (sha) => commits); 359 | 360 | const validTags = [ 361 | { 362 | name: 'v1.2.3', 363 | commit: { sha: '012345', url: '' }, 364 | zipball_url: '', 365 | tarball_url: 'string', 366 | node_id: 'string', 367 | }, 368 | { 369 | name: 'v2.1.3-prerelease.0', 370 | commit: { sha: '678901', url: '' }, 371 | zipball_url: '', 372 | tarball_url: 'string', 373 | node_id: 'string', 374 | }, 375 | { 376 | name: 'v2.1.3-prerelease.1', 377 | commit: { sha: '234567', url: '' }, 378 | zipball_url: '', 379 | tarball_url: 'string', 380 | node_id: 'string', 381 | }, 382 | ]; 383 | jest 384 | .spyOn(utils, 'getValidTags') 385 | .mockImplementation(async () => validTags); 386 | 387 | /* 388 | * When 389 | */ 390 | await action(); 391 | 392 | /* 393 | * Then 394 | */ 395 | expect(mockCreateTag).toHaveBeenCalledWith( 396 | 'v2.2.0', 397 | expect.any(Boolean), 398 | expect.any(String) 399 | ); 400 | expect(mockSetFailed).not.toBeCalled(); 401 | }); 402 | 403 | it('does create tag with custom release rules', async () => { 404 | /* 405 | * Given 406 | */ 407 | setInput('custom_release_rules', 'james:preminor'); 408 | const commits = [ 409 | { 410 | message: 'feat: some new feature on a pre-release branch', 411 | hash: null, 412 | }, 413 | { message: 'james: this should make a preminor', hash: null }, 414 | ]; 415 | jest 416 | .spyOn(utils, 'getCommits') 417 | .mockImplementation(async (sha) => commits); 418 | 419 | const validTags = [ 420 | { 421 | name: 'v1.2.3', 422 | commit: { sha: '012345', url: '' }, 423 | zipball_url: '', 424 | tarball_url: 'string', 425 | node_id: 'string', 426 | }, 427 | ]; 428 | jest 429 | .spyOn(utils, 'getValidTags') 430 | .mockImplementation(async () => validTags); 431 | 432 | /* 433 | * When 434 | */ 435 | await action(); 436 | 437 | /* 438 | * Then 439 | */ 440 | expect(mockCreateTag).toHaveBeenCalledWith( 441 | 'v1.3.0', 442 | expect.any(Boolean), 443 | expect.any(String) 444 | ); 445 | expect(mockSetFailed).not.toBeCalled(); 446 | }); 447 | }); 448 | 449 | describe('pre-release branches', () => { 450 | beforeEach(() => { 451 | jest.clearAllMocks(); 452 | setBranch('prerelease'); 453 | setInput('pre_release_branches', 'prerelease'); 454 | }); 455 | 456 | it('does not create tag without commits and default_bump set to false', async () => { 457 | /* 458 | * Given 459 | */ 460 | setInput('default_prerelease_bump', 'false'); 461 | const commits: any[] = []; 462 | jest 463 | .spyOn(utils, 'getCommits') 464 | .mockImplementation(async (sha) => commits); 465 | 466 | const validTags = [ 467 | { 468 | name: 'v1.2.3', 469 | commit: { sha: '012345', url: '' }, 470 | zipball_url: '', 471 | tarball_url: 'string', 472 | node_id: 'string', 473 | }, 474 | ]; 475 | jest 476 | .spyOn(utils, 'getValidTags') 477 | .mockImplementation(async () => validTags); 478 | 479 | /* 480 | * When 481 | */ 482 | await action(); 483 | 484 | /* 485 | * Then 486 | */ 487 | expect(mockCreateTag).not.toBeCalled(); 488 | expect(mockSetFailed).not.toBeCalled(); 489 | }); 490 | 491 | it('does create prerelease tag', async () => { 492 | /* 493 | * Given 494 | */ 495 | setInput('default_prerelease_bump', 'prerelease'); 496 | const commits = [{ message: 'this is my first fix', hash: null }]; 497 | jest 498 | .spyOn(utils, 'getCommits') 499 | .mockImplementation(async (sha) => commits); 500 | 501 | const validTags = [ 502 | { 503 | name: 'v1.2.3', 504 | commit: { sha: '012345', url: '' }, 505 | zipball_url: '', 506 | tarball_url: 'string', 507 | node_id: 'string', 508 | }, 509 | ]; 510 | jest 511 | .spyOn(utils, 'getValidTags') 512 | .mockImplementation(async () => validTags); 513 | 514 | /* 515 | * When 516 | */ 517 | await action(); 518 | 519 | /* 520 | * Then 521 | */ 522 | expect(mockCreateTag).toHaveBeenCalledWith( 523 | 'v1.2.4-prerelease.0', 524 | expect.any(Boolean), 525 | expect.any(String) 526 | ); 527 | expect(mockSetFailed).not.toBeCalled(); 528 | }); 529 | 530 | it('does create prepatch tag', async () => { 531 | /* 532 | * Given 533 | */ 534 | const commits = [{ message: 'fix: this is my first fix', hash: null }]; 535 | jest 536 | .spyOn(utils, 'getCommits') 537 | .mockImplementation(async (sha) => commits); 538 | 539 | const validTags = [ 540 | { 541 | name: 'v1.2.3', 542 | commit: { sha: '012345', url: '' }, 543 | zipball_url: '', 544 | tarball_url: 'string', 545 | node_id: 'string', 546 | }, 547 | ]; 548 | jest 549 | .spyOn(utils, 'getValidTags') 550 | .mockImplementation(async () => validTags); 551 | 552 | /* 553 | * When 554 | */ 555 | await action(); 556 | 557 | /* 558 | * Then 559 | */ 560 | expect(mockCreateTag).toHaveBeenCalledWith( 561 | 'v1.2.4-prerelease.0', 562 | expect.any(Boolean), 563 | expect.any(String) 564 | ); 565 | expect(mockSetFailed).not.toBeCalled(); 566 | }); 567 | 568 | it('does create preminor tag', async () => { 569 | /* 570 | * Given 571 | */ 572 | const commits = [ 573 | { message: 'feat: this is my first feature', hash: null }, 574 | ]; 575 | jest 576 | .spyOn(utils, 'getCommits') 577 | .mockImplementation(async (sha) => commits); 578 | 579 | const validTags = [ 580 | { 581 | name: 'v1.2.3', 582 | commit: { sha: '012345', url: '' }, 583 | zipball_url: '', 584 | tarball_url: 'string', 585 | node_id: 'string', 586 | }, 587 | ]; 588 | jest 589 | .spyOn(utils, 'getValidTags') 590 | .mockImplementation(async () => validTags); 591 | 592 | /* 593 | * When 594 | */ 595 | await action(); 596 | 597 | /* 598 | * Then 599 | */ 600 | expect(mockCreateTag).toHaveBeenCalledWith( 601 | 'v1.3.0-prerelease.0', 602 | expect.any(Boolean), 603 | expect.any(String) 604 | ); 605 | expect(mockSetFailed).not.toBeCalled(); 606 | }); 607 | 608 | it('does create premajor tag', async () => { 609 | /* 610 | * Given 611 | */ 612 | const commits = [ 613 | { 614 | message: 615 | 'my commit message\nBREAKING CHANGE:\nthis is a breaking change', 616 | hash: null, 617 | }, 618 | ]; 619 | jest 620 | .spyOn(utils, 'getCommits') 621 | .mockImplementation(async (sha) => commits); 622 | 623 | const validTags = [ 624 | { 625 | name: 'v1.2.3', 626 | commit: { sha: '012345', url: '' }, 627 | zipball_url: '', 628 | tarball_url: 'string', 629 | node_id: 'string', 630 | }, 631 | ]; 632 | jest 633 | .spyOn(utils, 'getValidTags') 634 | .mockImplementation(async () => validTags); 635 | 636 | /* 637 | * When 638 | */ 639 | await action(); 640 | 641 | /* 642 | * Then 643 | */ 644 | expect(mockCreateTag).toHaveBeenCalledWith( 645 | 'v2.0.0-prerelease.0', 646 | expect.any(Boolean), 647 | expect.any(String) 648 | ); 649 | expect(mockSetFailed).not.toBeCalled(); 650 | }); 651 | 652 | it('does create tag when release tag is newer', async () => { 653 | /* 654 | * Given 655 | */ 656 | const commits = [ 657 | { 658 | message: 'feat: some new feature on a pre-release branch', 659 | hash: null, 660 | }, 661 | ]; 662 | jest 663 | .spyOn(utils, 'getCommits') 664 | .mockImplementation(async (sha) => commits); 665 | 666 | const validTags = [ 667 | { 668 | name: 'v1.2.3-prerelease.0', 669 | commit: { sha: '012345', url: '' }, 670 | zipball_url: '', 671 | tarball_url: 'string', 672 | node_id: 'string', 673 | }, 674 | { 675 | name: 'v3.1.2-feature.0', 676 | commit: { sha: '012345', url: '' }, 677 | zipball_url: '', 678 | tarball_url: 'string', 679 | node_id: 'string', 680 | }, 681 | { 682 | name: 'v2.1.4', 683 | commit: { sha: '234567', url: '' }, 684 | zipball_url: '', 685 | tarball_url: 'string', 686 | node_id: 'string', 687 | }, 688 | ]; 689 | jest 690 | .spyOn(utils, 'getValidTags') 691 | .mockImplementation(async () => validTags); 692 | 693 | /* 694 | * When 695 | */ 696 | await action(); 697 | 698 | /* 699 | * Then 700 | */ 701 | expect(mockCreateTag).toHaveBeenCalledWith( 702 | 'v2.2.0-prerelease.0', 703 | expect.any(Boolean), 704 | expect.any(String) 705 | ); 706 | expect(mockSetFailed).not.toBeCalled(); 707 | }); 708 | 709 | it('does create tag with custom release rules', async () => { 710 | /* 711 | * Given 712 | */ 713 | setInput('custom_release_rules', 'james:preminor'); 714 | const commits = [ 715 | { 716 | message: 'feat: some new feature on a pre-release branch', 717 | hash: null, 718 | }, 719 | { message: 'james: this should make a preminor', hash: null }, 720 | ]; 721 | jest 722 | .spyOn(utils, 'getCommits') 723 | .mockImplementation(async (sha) => commits); 724 | 725 | const validTags = [ 726 | { 727 | name: 'v1.2.3', 728 | commit: { sha: '012345', url: '' }, 729 | zipball_url: '', 730 | tarball_url: 'string', 731 | node_id: 'string', 732 | }, 733 | ]; 734 | jest 735 | .spyOn(utils, 'getValidTags') 736 | .mockImplementation(async () => validTags); 737 | 738 | /* 739 | * When 740 | */ 741 | await action(); 742 | 743 | /* 744 | * Then 745 | */ 746 | expect(mockCreateTag).toHaveBeenCalledWith( 747 | 'v1.3.0-prerelease.0', 748 | expect.any(Boolean), 749 | expect.any(String) 750 | ); 751 | expect(mockSetFailed).not.toBeCalled(); 752 | }); 753 | }); 754 | 755 | describe('other branches', () => { 756 | beforeEach(() => { 757 | jest.clearAllMocks(); 758 | setBranch('development'); 759 | setInput('pre_release_branches', 'prerelease'); 760 | setInput('release_branches', 'release'); 761 | }); 762 | 763 | it('does output patch tag', async () => { 764 | /* 765 | * Given 766 | */ 767 | const commits = [{ message: 'fix: this is my first fix', hash: null }]; 768 | jest 769 | .spyOn(utils, 'getCommits') 770 | .mockImplementation(async (sha) => commits); 771 | 772 | const validTags = [ 773 | { 774 | name: 'v1.2.3', 775 | commit: { sha: '012345', url: '' }, 776 | zipball_url: '', 777 | tarball_url: 'string', 778 | node_id: 'string', 779 | }, 780 | ]; 781 | jest 782 | .spyOn(utils, 'getValidTags') 783 | .mockImplementation(async () => validTags); 784 | 785 | /* 786 | * When 787 | */ 788 | await action(); 789 | 790 | /* 791 | * Then 792 | */ 793 | expect(mockSetOutput).toHaveBeenCalledWith('new_version', '1.2.4'); 794 | expect(mockCreateTag).not.toBeCalled(); 795 | expect(mockSetFailed).not.toBeCalled(); 796 | }); 797 | 798 | it('does output minor tag', async () => { 799 | /* 800 | * Given 801 | */ 802 | const commits = [ 803 | { message: 'feat: this is my first feature', hash: null }, 804 | ]; 805 | jest 806 | .spyOn(utils, 'getCommits') 807 | .mockImplementation(async (sha) => commits); 808 | 809 | const validTags = [ 810 | { 811 | name: 'v1.2.3', 812 | commit: { sha: '012345', url: '' }, 813 | zipball_url: '', 814 | tarball_url: 'string', 815 | node_id: 'string', 816 | }, 817 | ]; 818 | jest 819 | .spyOn(utils, 'getValidTags') 820 | .mockImplementation(async () => validTags); 821 | 822 | /* 823 | * When 824 | */ 825 | await action(); 826 | 827 | /* 828 | * Then 829 | */ 830 | expect(mockSetOutput).toHaveBeenCalledWith('new_version', '1.3.0'); 831 | expect(mockCreateTag).not.toBeCalled(); 832 | expect(mockSetFailed).not.toBeCalled(); 833 | }); 834 | 835 | it('does output major tag', async () => { 836 | /* 837 | * Given 838 | */ 839 | const commits = [ 840 | { 841 | message: 842 | 'my commit message\nBREAKING CHANGE:\nthis is a breaking change', 843 | hash: null, 844 | }, 845 | ]; 846 | jest 847 | .spyOn(utils, 'getCommits') 848 | .mockImplementation(async (sha) => commits); 849 | 850 | const validTags = [ 851 | { 852 | name: 'v1.2.3', 853 | commit: { sha: '012345', url: '' }, 854 | zipball_url: '', 855 | tarball_url: 'string', 856 | node_id: 'string', 857 | }, 858 | ]; 859 | jest 860 | .spyOn(utils, 'getValidTags') 861 | .mockImplementation(async () => validTags); 862 | 863 | /* 864 | * When 865 | */ 866 | await action(); 867 | 868 | /* 869 | * Then 870 | */ 871 | expect(mockSetOutput).toHaveBeenCalledWith('new_version', '2.0.0'); 872 | expect(mockCreateTag).not.toBeCalled(); 873 | expect(mockSetFailed).not.toBeCalled(); 874 | }); 875 | }); 876 | }); 877 | -------------------------------------------------------------------------------- /tests/github.test.ts: -------------------------------------------------------------------------------- 1 | import { listTags } from '../src/github'; 2 | 3 | jest.mock( 4 | '@actions/github', 5 | jest.fn().mockImplementation(() => ({ 6 | context: { repo: { owner: 'mock-owner', repo: 'mock-repo' } }, 7 | getOctokit: jest.fn().mockReturnValue({ 8 | repos: { 9 | listTags: jest.fn().mockImplementation(({ page }: { page: number }) => { 10 | if (page === 6) { 11 | return { data: [] }; 12 | } 13 | 14 | const res = [...new Array(100).keys()].map((_) => ({ 15 | name: `v0.0.${_ + (page - 1) * 100}`, 16 | commit: { sha: 'string', url: 'string' }, 17 | zipball_url: 'string', 18 | tarball_url: 'string', 19 | node_id: 'string', 20 | })); 21 | 22 | return { data: res }; 23 | }), 24 | }, 25 | }), 26 | })) 27 | ); 28 | 29 | describe('github', () => { 30 | it('returns all tags', async () => { 31 | const tags = await listTags(true); 32 | 33 | expect(tags.length).toEqual(500); 34 | expect(tags[499]).toEqual({ 35 | name: 'v0.0.499', 36 | commit: { sha: 'string', url: 'string' }, 37 | zipball_url: 'string', 38 | tarball_url: 'string', 39 | node_id: 'string', 40 | }); 41 | }); 42 | 43 | it('returns only the last 100 tags', async () => { 44 | const tags = await listTags(true); 45 | 46 | expect(tags.length).toEqual(500); 47 | expect(tags[99]).toEqual({ 48 | name: 'v0.0.99', 49 | commit: { sha: 'string', url: 'string' }, 50 | zipball_url: 'string', 51 | tarball_url: 'string', 52 | node_id: 'string', 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/helper.test.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'js-yaml'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | export function setRepository( 6 | GITHUB_SERVER_URL: string, 7 | GITHUB_REPOSITORY: string 8 | ) { 9 | process.env['GITHUB_SERVER_URL'] = GITHUB_SERVER_URL; 10 | process.env['GITHUB_REPOSITORY'] = GITHUB_REPOSITORY; 11 | } 12 | 13 | export function setBranch(branch: string) { 14 | process.env['GITHUB_REF'] = `refs/heads/${branch}`; 15 | } 16 | 17 | export function setCommitSha(sha: string) { 18 | process.env['GITHUB_SHA'] = sha; 19 | } 20 | 21 | export function setInput(key: string, value: string) { 22 | process.env[`INPUT_${key.toUpperCase()}`] = value; 23 | } 24 | 25 | export function setInputs(map: { [key: string]: string }) { 26 | Object.keys(map).forEach((key) => setInput(key, map[key])); 27 | } 28 | 29 | export function loadDefaultInputs() { 30 | const actionYaml = fs.readFileSync( 31 | path.join(process.cwd(), 'action.yml'), 32 | 'utf-8' 33 | ); 34 | const actionJson = yaml.load(actionYaml) as { 35 | inputs: { [key: string]: { default?: string } }; 36 | }; 37 | const defaultInputs = Object.keys(actionJson['inputs']) 38 | .filter((key) => actionJson['inputs'][key].default) 39 | .reduce( 40 | (obj, key) => ({ ...obj, [key]: actionJson['inputs'][key].default }), 41 | {} 42 | ); 43 | setInputs(defaultInputs); 44 | } 45 | 46 | // Don't know how to have this file only for test but not have 'tsc' complain. So I made it a test file... 47 | describe('helper', () => { 48 | it('works', () => {}); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../src/utils'; 2 | import { getValidTags } from '../src/utils'; 3 | import * as core from '@actions/core'; 4 | import * as github from '../src/github'; 5 | import { defaultChangelogRules } from '../src/defaults'; 6 | 7 | jest.spyOn(core, 'debug').mockImplementation(() => {}); 8 | jest.spyOn(core, 'warning').mockImplementation(() => {}); 9 | 10 | const regex = /^v/; 11 | 12 | describe('utils', () => { 13 | it('extracts branch from ref', () => { 14 | /* 15 | * Given 16 | */ 17 | const remoteRef = 'refs/heads/master'; 18 | 19 | /* 20 | * When 21 | */ 22 | const branch = utils.getBranchFromRef(remoteRef); 23 | 24 | /* 25 | * Then 26 | */ 27 | expect(branch).toEqual('master'); 28 | }); 29 | 30 | it('test if ref is PR', () => { 31 | /* 32 | * Given 33 | */ 34 | const remoteRef = 'refs/pull/123/merge'; 35 | 36 | /* 37 | * When 38 | */ 39 | const isPullRequest = utils.isPr(remoteRef); 40 | 41 | /* 42 | * Then 43 | */ 44 | expect(isPullRequest).toEqual(true); 45 | }); 46 | 47 | it('returns valid tags', async () => { 48 | /* 49 | * Given 50 | */ 51 | const testTags = [ 52 | { 53 | name: 'release-1.2.3', 54 | commit: { sha: 'string', url: 'string' }, 55 | zipball_url: 'string', 56 | tarball_url: 'string', 57 | node_id: 'string', 58 | }, 59 | { 60 | name: 'v1.2.3', 61 | commit: { sha: 'string', url: 'string' }, 62 | zipball_url: 'string', 63 | tarball_url: 'string', 64 | node_id: 'string', 65 | }, 66 | ]; 67 | const mockListTags = jest 68 | .spyOn(github, 'listTags') 69 | .mockImplementation(async () => testTags); 70 | 71 | /* 72 | * When 73 | */ 74 | const validTags = await getValidTags(regex, false); 75 | 76 | /* 77 | * Then 78 | */ 79 | expect(mockListTags).toHaveBeenCalled(); 80 | expect(validTags).toHaveLength(1); 81 | }); 82 | 83 | it('returns sorted tags', async () => { 84 | /* 85 | * Given 86 | */ 87 | const testTags = [ 88 | { 89 | name: 'v1.2.4-prerelease.1', 90 | commit: { sha: 'string', url: 'string' }, 91 | zipball_url: 'string', 92 | tarball_url: 'string', 93 | node_id: 'string', 94 | }, 95 | { 96 | name: 'v1.2.4-prerelease.2', 97 | commit: { sha: 'string', url: 'string' }, 98 | zipball_url: 'string', 99 | tarball_url: 'string', 100 | node_id: 'string', 101 | }, 102 | { 103 | name: 'v1.2.4-prerelease.0', 104 | commit: { sha: 'string', url: 'string' }, 105 | zipball_url: 'string', 106 | tarball_url: 'string', 107 | node_id: 'string', 108 | }, 109 | { 110 | name: 'v1.2.3', 111 | commit: { sha: 'string', url: 'string' }, 112 | zipball_url: 'string', 113 | tarball_url: 'string', 114 | node_id: 'string', 115 | }, 116 | ]; 117 | const mockListTags = jest 118 | .spyOn(github, 'listTags') 119 | .mockImplementation(async () => testTags); 120 | 121 | /* 122 | * When 123 | */ 124 | const validTags = await getValidTags(regex, false); 125 | 126 | /* 127 | * Then 128 | */ 129 | expect(mockListTags).toHaveBeenCalled(); 130 | expect(validTags[0]).toEqual({ 131 | name: 'v1.2.4-prerelease.2', 132 | commit: { sha: 'string', url: 'string' }, 133 | zipball_url: 'string', 134 | tarball_url: 'string', 135 | node_id: 'string', 136 | }); 137 | }); 138 | 139 | it('returns only prefixed tags', async () => { 140 | /* 141 | * Given 142 | */ 143 | const testTags = [ 144 | { 145 | name: 'app2/5.0.0', 146 | commit: { sha: 'string', url: 'string' }, 147 | zipball_url: 'string', 148 | tarball_url: 'string', 149 | node_id: 'string', 150 | }, 151 | { 152 | name: '7.0.0', 153 | commit: { sha: 'string', url: 'string' }, 154 | zipball_url: 'string', 155 | tarball_url: 'string', 156 | node_id: 'string', 157 | }, 158 | { 159 | name: 'app1/3.0.0', 160 | commit: { sha: 'string', url: 'string' }, 161 | zipball_url: 'string', 162 | tarball_url: 'string', 163 | node_id: 'string', 164 | }, 165 | ]; 166 | const mockListTags = jest 167 | .spyOn(github, 'listTags') 168 | .mockImplementation(async () => testTags); 169 | /* 170 | * When 171 | */ 172 | const validTags = await getValidTags(/^app1\//, false); 173 | /* 174 | * Then 175 | */ 176 | expect(mockListTags).toHaveBeenCalled(); 177 | expect(validTags).toHaveLength(1); 178 | expect(validTags[0]).toEqual({ 179 | name: 'app1/3.0.0', 180 | commit: { sha: 'string', url: 'string' }, 181 | zipball_url: 'string', 182 | tarball_url: 'string', 183 | node_id: 'string', 184 | }); 185 | }); 186 | 187 | describe('custom release types', () => { 188 | it('maps custom release types', () => { 189 | /* 190 | * Given 191 | */ 192 | const customReleasesString = 193 | 'james:preminor,bond:premajor,007:major:Breaking Changes,feat:minor'; 194 | 195 | /* 196 | * When 197 | */ 198 | const mappedReleases = utils.mapCustomReleaseRules(customReleasesString); 199 | 200 | /* 201 | * Then 202 | */ 203 | expect(mappedReleases).toEqual([ 204 | { type: 'james', release: 'preminor' }, 205 | { type: 'bond', release: 'premajor' }, 206 | { type: '007', release: 'major', section: 'Breaking Changes' }, 207 | { 208 | type: 'feat', 209 | release: 'minor', 210 | section: defaultChangelogRules['feat'].section, 211 | }, 212 | ]); 213 | }); 214 | 215 | it('filters out invalid custom release types', () => { 216 | /* 217 | * Given 218 | */ 219 | const customReleasesString = 'james:pre-release,bond:premajor'; 220 | 221 | /* 222 | * When 223 | */ 224 | const mappedReleases = utils.mapCustomReleaseRules(customReleasesString); 225 | 226 | /* 227 | * Then 228 | */ 229 | expect(mappedReleases).toEqual([{ type: 'bond', release: 'premajor' }]); 230 | }); 231 | }); 232 | 233 | describe('method: mergeWithDefaultChangelogRules', () => { 234 | it('combines non-existing type rules with default rules', () => { 235 | /** 236 | * Given 237 | */ 238 | const newRule = { 239 | type: 'james', 240 | release: 'major', 241 | section: '007 Changes', 242 | }; 243 | 244 | /** 245 | * When 246 | */ 247 | const result = utils.mergeWithDefaultChangelogRules([newRule]); 248 | 249 | /** 250 | * Then 251 | */ 252 | expect(result).toEqual([ 253 | ...Object.values(defaultChangelogRules), 254 | newRule, 255 | ]); 256 | }); 257 | 258 | it('overwrites existing default type rules with provided rules', () => { 259 | /** 260 | * Given 261 | */ 262 | const newRule = { 263 | type: 'feat', 264 | release: 'minor', 265 | section: '007 Changes', 266 | }; 267 | 268 | /** 269 | * When 270 | */ 271 | const result = utils.mergeWithDefaultChangelogRules([newRule]); 272 | const overWrittenRule = result.find((rule) => rule.type === 'feat'); 273 | 274 | /** 275 | * Then 276 | */ 277 | expect(overWrittenRule?.section).toBe(newRule.section); 278 | }); 279 | 280 | it('returns only the rules having changelog section', () => { 281 | /** 282 | * Given 283 | */ 284 | const mappedReleaseRules = [ 285 | { type: 'james', release: 'major', section: '007 Changes' }, 286 | { type: 'bond', release: 'minor', section: undefined }, 287 | ]; 288 | 289 | /** 290 | * When 291 | */ 292 | const result = utils.mergeWithDefaultChangelogRules(mappedReleaseRules); 293 | 294 | /** 295 | * Then 296 | */ 297 | expect(result).toContainEqual(mappedReleaseRules[0]); 298 | expect(result).not.toContainEqual(mappedReleaseRules[1]); 299 | }); 300 | }); 301 | }); 302 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", /* Concatenate and emit output to single file. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": ["node_modules", "**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /types/semantic.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '@semantic-release/commit-analyzer' { 4 | export function analyzeCommits( 5 | config: { 6 | preset?: string; 7 | config?: string; 8 | parserOpts?: any; 9 | releaseRules?: 10 | | string 11 | | { 12 | type: string; 13 | release: string; 14 | scope?: string; 15 | }[]; 16 | presetConfig?: string; 17 | }, 18 | args: { 19 | commits: { message: string; hash: string | null }[]; 20 | logger: { log: (args: any) => void }; 21 | } 22 | ): Promise; 23 | } 24 | 25 | declare module '@semantic-release/release-notes-generator' { 26 | export function generateNotes( 27 | config: { 28 | preset?: string; 29 | config?: string; 30 | parserOpts?: any; 31 | writerOpts?: any; 32 | releaseRules?: 33 | | string 34 | | { 35 | type: string; 36 | release: string; 37 | scope?: string; 38 | }[]; 39 | presetConfig?: any; // Depends on used preset 40 | }, 41 | args: { 42 | commits: { message: string; hash: string | null }[]; 43 | logger: { log: (args: any) => void }; 44 | options: { 45 | repositoryUrl: string; 46 | }; 47 | lastRelease: { gitTag: string }; 48 | nextRelease: { gitTag: string; version: string }; 49 | } 50 | ): Promise; 51 | } 52 | --------------------------------------------------------------------------------