Compare commits

...

61 commits

Author SHA1 Message Date
Paul Hatcherian
b1025b26f2
Merge pull request #178 from PaulHatch/conventional-commits
feat!: Change default version patterns to follow Conventional Commits
2025-10-15 06:47:03 -04:00
Paul Hatcherian
6b919cc3ae
docs: Minor updates to guide 2025-10-15 06:46:25 -04:00
Paul Hatcherian
7b71828c01
docs: Update example for monorepo in guide
#182
2025-09-06 12:18:09 +01:00
Paul Hatcherian
bdf7908364 feat!: Change default version patterns to follow Conventional Commits 2025-07-19 07:50:19 +02:00
Paul Hatcherian
305899e7da Add changelog 2025-07-10 08:21:26 +02:00
Paul Hatcherian
c4f3793c16
Merge pull request #163 from jeremybower/update-versions-to-v5.4
Update examples to v5.4.0
2024-07-19 00:38:58 +02:00
Jeremy Bower
7adc5c502c
Update examples to v5.4.0
Update contributing.md
Update readme.md
2024-07-17 17:34:11 -04:00
Paul Hatcherian
c423ebb784
Update readme.md 2024-04-17 09:37:02 +03:00
Paul Hatcherian
a8f8f59fd7 Update node/package versions (MINOR) 2024-01-31 06:52:08 -06:00
Paul Hatcherian
8c0b779a80 Fix example config 2024-01-28 07:52:38 -06:00
Paul Hatcherian
4df56d00ce Update guide 2024-01-27 23:07:44 -06:00
Paul Hatcherian
e528d273e7 Update guide + readme 2023-10-30 21:11:34 -06:00
Paul Hatcherian
23baf5d553
Update readme.md with latest version 2023-10-24 18:27:54 -06:00
Paul Hatcherian
8d3552d384 Can't default to GITHUB_REF_NAME during testing 2023-09-30 09:27:44 -04:00
Paul Hatcherian
f53462a96e Update Jest config 2023-09-30 07:28:24 -04:00
Paul Hatcherian
fce0e75dfd Update dist 2023-09-30 05:52:00 -04:00
Paul Hatcherian
ec20cad99a Ignore non-version branches, reset diagnostics between tests 2023-09-30 04:51:49 -04:00
Paul Hatcherian
cfbfddabdd Fix for bump each commit not respecting prerelease mode 2023-09-27 20:36:33 -04:00
Paul Hatcherian
61963e734d Add new branch versioning (MINOR) 2023-09-27 20:17:26 -04:00
Paul Hatcherian
d3c0da227f Update readme 2023-09-13 22:13:14 -04:00
Paul Hatcherian
0995adf892 Update contributing guide, document diagnostics mode 2023-09-13 22:07:45 -04:00
Paul Hatcherian
270924986e Update "no tags" warning to indicate if tags were found. 2023-08-26 16:21:25 -04:00
Paul Hatcherian
5f6f89c4e0 Fix for diagnostic output 2023-08-24 20:35:13 -04:00
Paul Hatcherian
4fafb1f5a0 Enable debug mode 2023-08-24 20:23:23 -04:00
Paul Hatcherian
ba1fbef849 Include output in action 2023-08-24 19:01:19 -04:00
Paul Hatcherian
d93d2fb887 Add debug/replay mode (MINOR) 2023-08-20 21:33:26 -04:00
Paul Hatcherian
4f07cfb9e0 Update readme 2023-08-13 20:46:03 -04:00
Paul Hatcherian
976ff820fc Fix incorrect expected result 2023-08-09 09:02:09 -04:00
Paul Hatcherian
ce15f9a933
Merge pull request #116 from PaulHatch/patch-pattern
Add "patch pattern" support for bump each commit versioning
2023-08-09 09:00:43 -04:00
Paul Hatcherian
5a995f7e27 Update guide 2023-08-09 08:59:50 -04:00
Paul Hatcherian
cc7cc19f01 Add "patch pattern" support for bump each commit versioning 2023-08-09 08:55:19 -04:00
Paul Hatcherian
d439666925
Merge pull request #114 from PaulHatch/add-output
Add 'is_tagged' to output definition
2023-07-05 20:52:59 +02:00
Paul Hatcherian
59b55a49a0 Add 'is_tagged' to output definition 2023-07-05 20:43:06 +02:00
Paul Hatcherian
d3ce4be042
Merge pull request #104 from PaulHatch/101-previous_version-output-results-in-first-versiontag-and-not-previous-versiontag-when-commit-is-already-tagged
Fix for previous tag logic
2023-04-07 18:05:44 -10:00
Paul Hatcherian
9a5d07b45d Remove incorrect "reverse" on previous tags 2023-04-07 17:59:35 -10:00
Paul Hatcherian
f9d3daa396 Increase test timeout for Windows 2023-03-13 15:59:42 -10:00
Paul Hatcherian
6a1b048e03
Merge pull request #94 from Kantis/master
Adding is_tagged to outputs
2023-03-13 15:22:17 -10:00
Emil Kantis
37aa192825
Fixing tests in an environment where GPG signatures are globally enabled 2023-03-13 19:36:40 +01:00
Emil Kantis
e27fda7711
Adding is_tagged to outputs
Sets a flag when the current commit that the action is being run on has a tag matching the version format
2023-03-13 19:36:36 +01:00
Paul Hatcherian
9e89a29a4a
Merge pull request #90 from syanukov/fix-doc
fix(doc): fix "version" Output name
2023-01-26 07:28:27 -06:00
Stanislau Yanukou
e34c211152 fix(doc): fix "version" Output name 2023-01-26 09:38:17 +01:00
Paul Hatcherian
1fb98ec223
Merge pull request #88 from PaulHatch/pre-release-mode
Add "pre-release" mode
2023-01-15 11:34:55 -06:00
Paul Hatcherian
ddf8faf6a4 Add "pre-release" mode (MINOR) 2023-01-15 11:25:01 -06:00
Paul Hatcherian
61243c9221
Merge pull request #87 from PaulHatch/pre-release-tagged-commits-fix
Fix for pre-release tags on current commit
2023-01-10 17:16:32 -06:00
Paul Hatcherian
8b3b8f89c6 Fix for pre-release tags on current commit 2023-01-10 17:11:54 -06:00
Paul Hatcherian
346a6f2b12 Update dependencies 2023-01-08 09:14:33 -06:00
Paul Hatcherian
a951df0155 Fix input parameter name 2023-01-07 12:13:47 -06:00
Paul Hatcherian
6e136d69ea
Update readme.md 2022-12-31 07:12:58 -06:00
Paul Hatcherian
ea50fff3e4 Fix output mapping 2022-12-31 07:06:35 -06:00
Paul Hatcherian
d160cb59d5
Update readme.md 2022-12-30 21:04:27 -06:00
Paul Hatcherian
999339635f Escpae prefix and namespace when creating regex 2022-12-27 21:34:15 -06:00
Paul Hatcherian
0b58042494 Fix for regex filter using unescaped '.' 2022-12-26 23:49:27 -06:00
Paul Hatcherian
2fa33887a3
Merge pull request #77 from bwestover/patch-1
Update Docs with new input name
2022-12-23 12:54:46 -06:00
Paul Hatcherian
5447a3c144 Add syntax highlighting 2022-12-23 12:50:42 -06:00
Paul Hatcherian
124679ea79 Always use master as branch name in tests 2022-12-23 12:44:02 -06:00
Paul Hatcherian
87f3035988 Add contributing.md 2022-12-23 12:42:32 -06:00
Brett Westover
c7f5a74370
Update Docs with new input name 2022-12-23 10:14:01 -08:00
Paul Hatcherian
5003ad4622 Add test for namespaces containing a slash 2022-12-22 21:21:21 -06:00
Paul Hatcherian
4142d3bfe6 Documentation updates 2022-12-22 21:18:16 -06:00
Paul Hatcherian
74fca7fef9
Bump version references to 5.0.0 2022-12-21 22:41:39 -06:00
Paul Hatcherian
5a79aff975 Do not limit the number of tags returned when finding previous version 2022-12-20 07:02:54 -06:00
44 changed files with 4870 additions and 7064 deletions

View file

@ -27,3 +27,9 @@ jobs:
- name: Run Action
uses: ./
id: run
with:
debug: true
- name: Print Diagnostic Output
run: echo $DEBUG_OUTPUT
env:
DEBUG_OUTPUT: ${{ steps.run.outputs.debug_output }}

281
CHANGELOG.md Normal file
View file

@ -0,0 +1,281 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.4.0] - 2024-01-31
### Changed
- Updated to Node.js v20 runtime
- Updated all dependencies to latest versions
## [5.3.0] - 2023-09-30
### Added
- **Branch-based versioning mode** (`version_from_branch` input) - Major/minor versions can now be derived from branch names (e.g., `release/v1`, `release/1.2`). Only considers tags matching the branch version, useful for maintaining multiple release lines
- Enhanced diagnostics documentation in contributing guide
- Improved warning messages to clarify when no tags are found vs when tags exist but don't match criteria
### Fixed
- `GITHUB_REF_NAME` environment variable no longer causes failures during testing
- `bump_each_commit` now properly respects `enable_prerelease_mode` setting
- Non-version branches are properly ignored when using branch-based versioning
### Changed
- Updated Jest configuration for better test isolation
- Rebuilt distribution files with latest changes
### Deprecated
- `use_branches` input is deprecated and will be removed in v6.0.0 - use `version_from_branch` instead
## [5.2.1] - 2023-08-24
### Fixed
- Diagnostic mode output was not being properly included in the action's output, preventing debugging
## [5.2.0] - 2023-08-20
### Added
- **Debug/diagnostic mode** (`debug` input) - Captures and outputs diagnostic information for troubleshooting version calculations. Useful when the source repository isn't available for direct inspection
## [5.1.0] - 2023-08-09
### Added
- **Patch pattern filtering** (`bump_each_commit_patch_pattern` input) - When using `bump_each_commit`, patch version only increments if commit matches specified pattern. Supports JavaScript regex syntax with flags (e.g., `/fix\(.*\)/i`)
- **Pre-release mode** (`enable_prerelease_mode` input) - Prevents automatic major version bumps for 0.x.x versions. When enabled, "major" changes become "minor" and "minor" become "patch", preventing premature 1.0.0 releases
- `is_tagged` output - Boolean indicating if the current commit already has a version tag
- Previous version commit information outputs (`previous_commit`, `previous_version`) for better version tracking
### Fixed
- Corrected tag ordering when determining previous version (was using reverse order incorrectly)
- Increased test timeout for Windows environments to prevent CI failures
- Fixed test failures in environments with global GPG signing enabled
- Documentation typo: "version" output name was incorrectly documented
## [5.0.3] - 2023-01-10
### Fixed
- Pre-release tags on current commit were not being handled correctly when determining version increments
- Fixed incorrect parameter name mapping that was causing action failures
### Changed
- Updated dependencies to latest versions
## [5.0.2] - 2022-12-31
### Fixed
- Build output mapping was incorrect, causing the action to fail when generating outputs
## [5.0.1] - 2022-12-27
### Fixed
- Tag prefix and namespace values are now properly escaped when constructing regex patterns, preventing regex errors with special characters
- Fixed unescaped dots in regex patterns that could cause incorrect matching
- Test suite now consistently uses 'master' as branch name to avoid CI failures
### Added
- Syntax highlighting for code examples in documentation
- Contributing.md guide for developers
- Test coverage for namespaces containing forward slashes
### Changed
- Documentation updated to reflect correct input parameter names
- Version calculation no longer limits the number of tags retrieved, ensuring accurate version determination in repos with many tags
## [5.0.0] - 2022-12-20 - Major Rewrite
### Added
- **Complete TypeScript rewrite** - Action rewritten from JavaScript to TypeScript with modular architecture
- **Author tracking** - New `authors` output lists all commit authors since last release, formatted as CSV (JSON option available via `user_format_type`)
- **Commit body searching** - `search_commit_body` input allows searching commit message bodies for version patterns, not just the subject line
- **Branch support** - Can now use branch names instead of tags for versioning with `use_branches` input
- **Improved outputs** - Additional metadata including `version_type`, commit hashes, and more detailed version information
- **Namespace support without tags** - Namespaces now work even when no existing tags match the namespace
### Fixed
- Pre-release tags (alpha, beta, rc) are now properly excluded from version calculations unless explicitly included
- Fixed issue where current commit's tag wasn't properly considered when calculating previous version
- Tag ordering now uses git's version sort instead of author date, providing more accurate version ordering
### Changed
- Architecture completely redesigned with providers, resolvers, classifiers, and formatters for better extensibility
- Short tag support has been completely removed (was deprecated in v4)
- Updated to actions/core@1.10.0 and modernized all dependencies
- Node.js 16 compatibility
## [4.0.3] - 2021-10-29
### Changed
- Version output now properly uses the user-supplied version format template combined with namespace
- Updated dependencies and improved test coverage
- Documentation clarifications for better user understanding
## [4.0.2] - 2021-04-22
### Fixed
- Tag prefixes can now contain forward slashes (e.g., `releases/v`), enabling more flexible tagging schemes
## [4.0.1] - 2021-02-25
### Fixed
- Fixed regex pattern for matching full version tags when `short_tags` is disabled
## [4.0.0] - 2021-02-08
### Changed
- **Breaking**: Branch parameter now defaults to `HEAD` instead of requiring explicit branch name
- Branch names no longer include `origin/` prefix, simplifying branch-based versioning
- Reintroduced support for using `HEAD` as branch parameter (was removed in v3)
### Deprecated
- `branch` input is now deprecated in favor of automatic HEAD detection
## [3.3.1] - 2021-01-28
### Added
- `version_tag` output now includes namespace value, making it easier to identify versioned releases in multi-project repositories
### Changed
- Improved documentation clarity for namespace feature
- Enhanced readme formatting and examples
## [3.3.0] - 2021-01-23
### Added
- **Regular expression support** for `major_pattern` and `minor_pattern` - Wrap patterns in `/` to use regex (e.g., `/breaking:\s/i`)
### Fixed
- Fixed logic that prevented version tags from being properly matched when calculating increments
## [3.2.1] - 2021-01-16
### Fixed
- Tagged commits now properly preserve their increment value instead of resetting to 0
- SVG diagrams now have proper background color for better visibility
### Changed
- Updated dependencies
- Documentation improvements
## [3.2.0] - 2020-12-20
### Added
- **`bump_each_commit` mode** - Every commit creates a new patch version, useful for continuous deployment scenarios
- **`short_tags` toggle** - When set to `false`, only full semantic version tags (e.g., v1.2.3) are considered, ignoring short tags (e.g., v1)
### Changed
- Improved documentation with visual diagrams
- Removed deprecated parameters from documentation
- Enhanced readme clarity with better examples
## [3.1.2] - 2020-10-07
### Fixed
- **Full Windows support** - Fixed line ending issues and command execution on Windows
- Action now properly exits when current commit already has a version tag
- Current commit's tag is now used as the version when applicable
### Added
- Complete Windows support in test suite with OS-specific temp directories
- Windows runner added to CI pipeline alongside Linux
### Changed
- Commands now run silently to reduce log noise
- Improved error handling for command execution failures
- Added warning about actions/checkout@v2 shallow clone behavior that can affect version detection
## [3.1.1] - 2020-09-05
### Fixed
- Change detection now works correctly when no previous tags exist in the repository
## [3.1.0] - 2020-09-05
### Added
- **`version_tag` output** - Returns the complete version tag including prefix and namespace
### Changed
- Improved logging for change detection to help with debugging
- Command execution failures are now logged as info rather than errors (they're handled gracefully)
- Updated package dependencies
## [3.0.0] - 2020-09-02 - Multi-Project Support
### Added
- **Namespace support** (`namespace` input) - Enables multiple projects/components in same repo with isolated versioning
- **Improved mono-repo support** - Each namespace maintains its own version sequence
### Changed
- **Breaking**: `change_path` input now filters which paths trigger version changes rather than just detecting changes
- Removed verbose action output for cleaner logs
- Modernized codebase and dependencies for GitHub Actions runner compatibility
### Removed
- Deprecated action inputs from v2
## [2.1.1] - 2020-02-07
### Fixed
- Release link generation now uses correct branch name format
## [2.1.0] - 2020-01-25
### Added
- **Path-based change detection** (`change_path` input) - Specify paths to monitor for changes, useful for mono-repos where not all changes should trigger version bumps
### Changed
- Release link now uses branch name from action input rather than GitHub environment variable
- Release link is now output to action logs for visibility
## [2.0.0] - 2019-12-24
### Changed
- **Breaking**: Now uses `git describe` for more reliable tag detection instead of `git log`
- Added warning when repository has no tags, helping users understand why versioning starts at 0.0.0
## [1.0.1] - 2019-12-11
### Fixed
- Empty tag prefixes are now supported (useful for repos that use plain version numbers without 'v' prefix)
### Added
- Documentation for `version_format` input parameter
## [1.0.0] - 2019-12-11 - Initial Release
### Added
- Automatic semantic versioning based on git commit history
- Version bumping through commit message markers: `(MAJOR)` and `(MINOR)`
- Customizable version output format via `version_format` input
- Support for both short (v1) and full (v1.0.0) version tags
- Increment counter for commits since last version tag
- No manual version maintenance required - fully automated from git history
[5.4.0]: https://github.com/PaulHatch/semantic-version/compare/v5.3.0...v5.4.0
[5.3.0]: https://github.com/PaulHatch/semantic-version/compare/v5.2.1...v5.3.0
[5.2.1]: https://github.com/PaulHatch/semantic-version/compare/v5.2.0...v5.2.1
[5.2.0]: https://github.com/PaulHatch/semantic-version/compare/v5.1.0...v5.2.0
[5.1.0]: https://github.com/PaulHatch/semantic-version/compare/v5.0.3...v5.1.0
[5.0.3]: https://github.com/PaulHatch/semantic-version/compare/v5.0.2...v5.0.3
[5.0.2]: https://github.com/PaulHatch/semantic-version/compare/v5.0.1...v5.0.2
[5.0.1]: https://github.com/PaulHatch/semantic-version/compare/v5.0.0...v5.0.1
[5.0.0]: https://github.com/PaulHatch/semantic-version/compare/v4.0.3...v5.0.0
[4.0.3]: https://github.com/PaulHatch/semantic-version/compare/v4.0.2...v4.0.3
[4.0.2]: https://github.com/PaulHatch/semantic-version/compare/v4.0.1...v4.0.2
[4.0.1]: https://github.com/PaulHatch/semantic-version/compare/v4...v4.0.1
[4.0.0]: https://github.com/PaulHatch/semantic-version/compare/v3.3.1...v4
[3.3.1]: https://github.com/PaulHatch/semantic-version/compare/v3.3...v3.3.1
[3.3.0]: https://github.com/PaulHatch/semantic-version/compare/v3.2.1...v3.3
[3.2.1]: https://github.com/PaulHatch/semantic-version/compare/v3.2...v3.2.1
[3.2.0]: https://github.com/PaulHatch/semantic-version/compare/v3.1.2...v3.2
[3.1.2]: https://github.com/PaulHatch/semantic-version/compare/v3.1.1...v3.1.2
[3.1.1]: https://github.com/PaulHatch/semantic-version/compare/v3.1...v3.1.1
[3.1.0]: https://github.com/PaulHatch/semantic-version/compare/v3...v3.1
[3.0.0]: https://github.com/PaulHatch/semantic-version/compare/v2.1.1...v3
[2.1.1]: https://github.com/PaulHatch/semantic-version/compare/v2.1...v2.1.1
[2.1.0]: https://github.com/PaulHatch/semantic-version/compare/v2...v2.1
[2.0.0]: https://github.com/PaulHatch/semantic-version/compare/v1.0.1...v2
[1.0.1]: https://github.com/PaulHatch/semantic-version/compare/v1...v1.0.1
[1.0.0]: https://github.com/PaulHatch/semantic-version/releases/tag/v1

View file

@ -13,13 +13,17 @@ inputs:
required: false
default: "v"
use_branches:
description: "Use branches instead of tags"
description: "(Deprecated) Use branches instead of tags"
required: false
default: "false"
version_from_branch:
description: If true, the branch will be used to select the maximum version
required: false
default: "false"
major_pattern:
description: "A string which, if present in a git commit, indicates that a change represents a major (breaking) change. Wrap with '/' to match using a regular expression."
required: true
default: "(MAJOR)"
default: "/!:|BREAKING CHANGE:/"
major_regexp_flags:
description: "A string which indicates the flags used by the `major_pattern` regular expression. Supported flags: idgs"
required: false
@ -27,7 +31,7 @@ inputs:
minor_pattern:
description: "A string which, if present in a git commit, indicates that a change represents a minor (feature) change. Wrap with '/' to match using a regular expression."
required: true
default: "(MINOR)"
default: "/feat:/"
minor_regexp_flags:
description: "A string which indicates the flags used by the `minor_pattern` regular expression. Supported flags: idgs"
required: false
@ -54,6 +58,18 @@ inputs:
description: "The output method used to generate list of users, 'csv' or 'json'. Default is 'csv'."
required: true
default: "csv"
enable_prerelease_mode:
description: "Prevents pre-v1.0.0 version from automatically incrementing the major version. If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged."
required: true
default: "false"
bump_each_commit_patch_pattern:
description: "If bump_each_commit is also set to true, setting this value will cause the version to increment only if the pattern specified is matched."
required: true
default: ""
debug:
description: "If enabled, diagnostic information will be added to the action output"
required: true
default: "false"
outputs:
major:
description: "Current major number"
@ -71,8 +87,19 @@ outputs:
description: "The version tag"
changed:
description: "Indicates whether there was a change since the last version if change_path was specified. If no change_path was specified this value will always be true since the entire repo is considered."
is_tagged:
description: "Indicates that the commit had a tag that matched the 'versionTag' format"
authors:
description: "List of users contributing commits to this version"
previous_commit:
description: "Hash of the previous commit"
previous_version:
description: "Indicates the previous version"
current_commit:
description: "The current commit hash"
debug_output:
description: "Diagnostic information, if debug is enabled"
runs:
using: "node16"
using: "node20"
main: "dist/index.js"

105
contributing.md Normal file
View file

@ -0,0 +1,105 @@
# Contributing
Fixes and enhancements are welcome, but if you are planning to do a lot of work, it is a good idea to raise an issue first to discuss it. Generally enhancements should follow the goals of the project described in the main readme:
- Allow the version to be injected into the build
- Derive the version only from the git repository itself
- Do not require the version to be maintained by hand
- Resolve the version deterministically for a given commit
- Provide an easy mechanism for incrementing major and minor versions by developers
## Getting Help
If you have found or believe you have found a bug please open a ticket. If you are having trouble using the action and need help please use the discussions page.
Since nearly all questions are related to a specific repository it can be difficult to diagnose issues from a description alone. There are a few ways to provide additional information that can help diagnose the problem.
### Creating Diagnostic Information
There is a debug option which produces diagnostic information. This information can be used to troubleshoot and even to rerun the action without access to the original repository, for example with a debugger attached. To enable this option set the `debug` input to `true` and then use the `debug_output` output to access the information. The following configuration will print the debug output to the console:
```
- name: Version
uses: paulhatch/semantic-version@v5.4.0
id: version
with:
tag_prefix: ""
version_format: "${major}.${minor}.${patch}.${increment}"
debug: true
- name: Print Diagnostic Output
run: echo "$DEBUG_OUTPUT"
env:
DEBUG_OUTPUT: ${{ steps.version.outputs.debug_output }}
```
Please review the information before posting it to avoid disclosing any sensitive information. In particular the output may contain names and email addresses of the committers, as well as commit messages for recent commits.
### Providing a Test Case
If you are planning to open a ticket or post to discussions, it is extremely helpful if you can provide a test case that demonstrates the problem. This project includes a test helper than makes it very easy to create new tests with just a few lines of code.
To get started simply:
- Ensure you have the latest version of NodeJS installed (https://nodejs.org)
- Clone the repository, `git clone https://github.com/PaulHatch/semantic-version.git` or `git@github.com:PaulHatch/semantic-version.git`
- Run `npm install` in the root of the repository to install the dependencies
- Run `npm run test` to run the tests
The src/main.test.ts file contains integration tests that validate all the features of this action, to add a new test case simply add a new function to the bottom of this file.
```typescript
test('Name of test goes here', async () => {
// This method creates a test repository in your temp directory, the repo
// object returned provides methods to interact with the repository
const repo = createTestRepo({
// Specify any config options you want to set, the options available can be found
// in the src/ActionConfig.ts file
tagPrefix: ''
});
// the make commit method creates a commit with the specified message, an empty file will be
// automatically created for the commit
repo.makeCommit('Initial Commit');
// an optional second parameter can be used to specify the path of the file to commit,
// which will be created if it does not exist already
repo.makeCommit('Initial Commit', 'subdir');
// the exec method runs an arbitrary command in the repository
repo.exec('git tag 0.0.1')
// the runAction method runs the action and returns the result
const result = await repo.runAction();
// finally, the use whatever assertion you want to validate the result
expect(result.formattedVersion).toBe('0.0.1+0');
}, 15000);
```
This uses the [Jest](https://jestjs.io/) testing framework. Really it is generally not necessary to be very familiar with Jest, just copy the pattern of the existing tests and you should be fine.
Once you have a failing test case that demostrates the problem, you can just past it into the ticket, there is no need to create a repository unless you want to.
### Providing an Example Repository
If you are not able to provide a test case, it is still very helpful to provide an example repository that demonstrates the problem. Many times people are experiencing problems with a configuration of a private repository. This is quite difficult to debug without access to the repository, if you are able to provide a public repository that demonstrates the problem it will make it much easier to debug and can eliminate a great deal of back and forth.
## Forking the Project
If want to do something that does not fit with the project goals, particually if you want to include information from an outside system, it is probably best to fork the project and create your own action. One of the goals of this project starting in 5.0.0 is to be easy to fork and customize, and to that end the action has been broken into individual providers than can be replaced. All providers have been implemented using async calls specifically to support calls to external systems.
The steps of this action are:
- Get the current commit
- Get the last release
- Get commits between the last release and the current commit
- Classify the release
Additionally a few formatter provide modular behavior to these step:
- A tag formmater is used to parse and format the version number
- A version formatter is used to format output version string
- A user formatter is used to format the user information in the output (JSON and CSV are provided in the default implementation)
Each one includes at least one default, but can be replaced by a custom provider by implementing the appropriate interface and updating the `ConfigurationProvider` to return your action instead. This should allow you to continue to merge in changes from the main project as needed with minimal conflict.

535
dist/index.js vendored
View file

@ -42,7 +42,12 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.cmd = void 0;
// Using require instead of import to support integration testing
const exec = __importStar(__nccwpck_require__(1514));
const DebugManager_1 = __nccwpck_require__(1823);
const cmd = (command, ...args) => __awaiter(void 0, void 0, void 0, function* () {
const debugManager = DebugManager_1.DebugManager.getInstance();
if (debugManager.isReplayMode()) {
return debugManager.replayCommand(command, args);
}
let output = '', errors = '';
const options = {
silent: true,
@ -53,15 +58,14 @@ const cmd = (command, ...args) => __awaiter(void 0, void 0, void 0, function* ()
silent: true
}
};
let caughtError = null;
try {
yield exec.exec(command, args, options);
}
catch (err) {
//core.info(`The command cd '${command} ${args.join(' ')}' failed: ${err}`);
}
if (errors !== '') {
//core.info(`stderr: ${errors}`);
caughtError = err;
}
debugManager.recordCommand(command, args, output, errors, caughtError);
return output;
});
exports.cmd = cmd;
@ -77,6 +81,7 @@ exports.cmd = cmd;
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ConfigurationProvider = void 0;
const CsvUserFormatter_1 = __nccwpck_require__(9105);
const BranchVersioningTagFormatter_1 = __nccwpck_require__(807);
const DefaultTagFormatter_1 = __nccwpck_require__(4808);
const DefaultVersionFormatter_1 = __nccwpck_require__(8524);
const JsonUserFormatter_1 = __nccwpck_require__(7892);
@ -85,9 +90,11 @@ const DefaultCurrentCommitResolver_1 = __nccwpck_require__(35);
const DefaultVersionClassifier_1 = __nccwpck_require__(5527);
const DefaultLastReleaseResolver_1 = __nccwpck_require__(8337);
const BumpAlwaysVersionClassifier_1 = __nccwpck_require__(6482);
const DebugManager_1 = __nccwpck_require__(1823);
class ConfigurationProvider {
constructor(config) {
this.config = config;
DebugManager_1.DebugManager.getInstance().initializeConfig(config);
}
GetCurrentCommitResolver() { return new DefaultCurrentCommitResolver_1.DefaultCurrentCommitResolver(this.config); }
GetLastReleaseResolver() { return new DefaultLastReleaseResolver_1.DefaultLastReleaseResolver(this.config); }
@ -99,7 +106,12 @@ class ConfigurationProvider {
return new DefaultVersionClassifier_1.DefaultVersionClassifier(this.config);
}
GetVersionFormatter() { return new DefaultVersionFormatter_1.DefaultVersionFormatter(this.config); }
GetTagFormatter() { return new DefaultTagFormatter_1.DefaultTagFormatter(this.config); }
GetTagFormatter(branchName) {
if (this.config.versionFromBranch) {
return new BranchVersioningTagFormatter_1.BranchVersioningTagFormatter(this.config, branchName);
}
return new DefaultTagFormatter_1.DefaultTagFormatter(this.config);
}
GetUserFormatter() {
switch (this.config.userFormatType) {
case 'json': return new JsonUserFormatter_1.JsonUserFormatter(this.config);
@ -112,6 +124,114 @@ class ConfigurationProvider {
exports.ConfigurationProvider = ConfigurationProvider;
/***/ }),
/***/ 1823:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.DebugManager = void 0;
/** Utility class for managing debug mode and diagnostic information */
class DebugManager {
constructor() {
this.debugEnabled = false;
this.replayMode = false;
this.diagnosticInfo = null;
}
/** Returns the singleton instance of the DebugManager */
static getInstance() {
if (!DebugManager.instance) {
DebugManager.instance = new DebugManager();
}
return DebugManager.instance;
}
/** Clears the singleton instance of the DebugManager (used for testing) */
static clearState() {
DebugManager.instance = new DebugManager();
}
/** Returns true if debug mode is enabled */
isDebugEnabled() {
return this.debugEnabled;
}
/** Returns true if replay mode is enabled */
isReplayMode() {
return this.replayMode;
}
initializeConfig(config) {
if (config.debug) {
this.setDebugEnabled(true);
}
else if (config.replay.length > 0) {
this.replayFromDiagnostics(config.replay);
}
}
/** Enables or disables debug mode, also clears any existing diagnostics info */
setDebugEnabled(enableDebug = true) {
this.debugEnabled = enableDebug;
this.replayMode = false;
this.diagnosticInfo = new DiagnosticInfo();
}
;
/** Enables replay mode and loads the diagnostic information from the specified string */
replayFromDiagnostics(diagnostics) {
this.debugEnabled = false;
this.replayMode = true;
this.diagnosticInfo = JSON.parse(diagnostics);
}
/** Returns a JSON string containing the diagnostic information for this run */
getDebugOutput(emptyRepo = false) {
return this.isDebugEnabled() ? JSON.stringify(this.diagnosticInfo) : '';
}
/** Records a command and its output for diagnostic purposes */
recordCommand(command, args, output, stderr, error) {
var _a;
if (this.isDebugEnabled()) {
(_a = this.diagnosticInfo) === null || _a === void 0 ? void 0 : _a.recordCommand(command, args, output, stderr, error);
}
}
/** Replays the specified command and returns the output */
replayCommand(command, args) {
if (this.diagnosticInfo === null) {
throw new Error('No diagnostic information available for replay');
}
const commandResult = this.diagnosticInfo.commands.find(c => c.command === command && JSON.stringify(c.args) === JSON.stringify(args));
if (!commandResult) {
throw new Error(`No result found in diagnostic for command "${command}"`);
}
if (commandResult.error) {
throw commandResult.error;
}
if (commandResult.stderr) {
console.error(commandResult.stderr);
}
return commandResult.output;
}
}
exports.DebugManager = DebugManager;
/** Represents a CLI command result */
class CommandResult {
constructor(command, args, output, stderr, error) {
this.command = command;
this.args = args;
this.output = output;
this.stderr = stderr;
this.error = error;
}
}
/** Represents the result of the commands executed for a run */
class DiagnosticInfo {
constructor() {
this.commands = [];
this.empty = false;
}
recordCommand(command, args, output, stderr, error) {
this.commands.push(new CommandResult(command, args, output, stderr, error));
}
}
/***/ }),
/***/ 2010:
@ -133,12 +253,14 @@ class VersionResult {
* @param formattedVersion - The formatted semantic version
* @param versionTag - The string to be used as a Git tag
* @param changed - True if the version was changed, otherwise false
* @param isTagged - True if the commit had a tag that matched the `versionTag` format
* @param authors - Authors formatted according to the format mode (e.g. JSON, CSV, YAML, etc.)
* @param currentCommit - The current commit hash
* @param previousCommit - The previous commit hash
* @param previousVersion - the previous version
* @param previousVersion - The previous version
* @param debugOutput - Diagnostic information, if debug is enabled
*/
constructor(major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, authors, currentCommit, previousCommit, previousVersion) {
constructor(major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, isTagged, authors, currentCommit, previousCommit, previousVersion, debugOutput) {
this.major = major;
this.minor = minor;
this.patch = patch;
@ -147,10 +269,12 @@ class VersionResult {
this.formattedVersion = formattedVersion;
this.versionTag = versionTag;
this.changed = changed;
this.isTagged = isTagged;
this.authors = authors;
this.currentCommit = currentCommit;
this.previousCommit = previousCommit;
this.previousVersion = previousVersion;
this.debugOutput = debugOutput;
}
}
exports.VersionResult = VersionResult;
@ -178,6 +302,7 @@ const VersionResult_1 = __nccwpck_require__(2010);
const VersionType_1 = __nccwpck_require__(895);
const UserInfo_1 = __nccwpck_require__(5907);
const VersionInformation_1 = __nccwpck_require__(5686);
const DebugManager_1 = __nccwpck_require__(1823);
function runAction(configurationProvider) {
return __awaiter(this, void 0, void 0, function* () {
const currentCommitResolver = configurationProvider.GetCurrentCommitResolver();
@ -185,20 +310,22 @@ function runAction(configurationProvider) {
const commitsProvider = configurationProvider.GetCommitsProvider();
const versionClassifier = configurationProvider.GetVersionClassifier();
const versionFormatter = configurationProvider.GetVersionFormatter();
const tagFormmater = configurationProvider.GetTagFormatter();
const tagFormatter = configurationProvider.GetTagFormatter(yield currentCommitResolver.ResolveBranchNameAsync());
const userFormatter = configurationProvider.GetUserFormatter();
const debugManager = DebugManager_1.DebugManager.getInstance();
if (yield currentCommitResolver.IsEmptyRepoAsync()) {
const versionInfo = new VersionInformation_1.VersionInformation(0, 0, 0, 0, VersionType_1.VersionType.None, [], false);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormmater.Format(versionInfo), versionInfo.changed, userFormatter.Format('author', []), '', '', '0.0.0');
const versionInfo = new VersionInformation_1.VersionInformation(0, 0, 0, 0, VersionType_1.VersionType.None, [], false, false);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormatter.Format(versionInfo), versionInfo.changed, versionInfo.isTagged, userFormatter.Format('author', []), '', '', tagFormatter.Parse(tagFormatter.Format(versionInfo)).join('.'), debugManager.getDebugOutput(true));
}
const currentCommit = yield currentCommitResolver.ResolveAsync();
const lastRelease = yield lastReleaseResolver.ResolveAsync(currentCommit, tagFormmater);
const lastRelease = yield lastReleaseResolver.ResolveAsync(currentCommit, tagFormatter);
const commitSet = yield commitsProvider.GetCommitsAsync(lastRelease.hash, currentCommit);
const classification = yield versionClassifier.ClassifyAsync(lastRelease, commitSet);
const { isTagged } = lastRelease;
const { major, minor, patch, increment, type, changed } = classification;
// At this point all necessary data has been pulled from the database, create
// version information to be used by the formatters
let versionInfo = new VersionInformation_1.VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed);
let versionInfo = new VersionInformation_1.VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed, isTagged);
// Group all the authors together, count the number of commits per author
const allAuthors = versionInfo.commits
.reduce((acc, commit) => {
@ -210,12 +337,107 @@ function runAction(configurationProvider) {
const authors = Object.values(allAuthors)
.map((u) => new UserInfo_1.UserInfo(u.n, u.e, u.c))
.sort((a, b) => b.commits - a.commits);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormmater.Format(versionInfo), versionInfo.changed, userFormatter.Format('author', authors), currentCommit, lastRelease.hash, `${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormatter.Format(versionInfo), versionInfo.changed, versionInfo.isTagged, userFormatter.Format('author', authors), currentCommit, lastRelease.hash, `${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`, debugManager.getDebugOutput());
});
}
exports.runAction = runAction;
/***/ }),
/***/ 807:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.BranchVersioningTagFormatter = void 0;
const DefaultTagFormatter_1 = __nccwpck_require__(4808);
/** Default tag formatter which allows a prefix to be specified */
class BranchVersioningTagFormatter extends DefaultTagFormatter_1.DefaultTagFormatter {
getRegex(pattern) {
if (/^\/.+\/[i]*$/.test(pattern)) {
const regexEnd = pattern.lastIndexOf('/');
const parsedFlags = pattern.slice(pattern.lastIndexOf('/') + 1);
return new RegExp(pattern.slice(1, regexEnd), parsedFlags);
}
return new RegExp(pattern);
}
constructor(config, branchName) {
super(config);
const pattern = config.versionFromBranch === true ?
new RegExp("[0-9]+.[0-9]+$|[0-9]+$") :
this.getRegex(config.versionFromBranch);
const result = pattern.exec(branchName);
if (result === null) {
this.major = NaN;
this.onVersionBranch = false;
return;
}
let branchVersion;
switch (result === null || result === void 0 ? void 0 : result.length) {
case 1:
branchVersion = result[0];
break;
case 2:
branchVersion = result[1];
break;
default:
throw new Error(`Unable to parse version from branch named '${branchName}' using pattern '${pattern}'`);
}
this.onVersionBranch = true;
const versionValues = branchVersion.split('.');
if (versionValues.length > 2) {
throw new Error(`The version string '${branchVersion}' parsed from branch '${branchName}' is invalid. It must be in the format 'major.minor' or 'major'`);
}
this.major = parseInt(versionValues[0]);
if (isNaN(this.major)) {
throw new Error(`The major version '${versionValues[0]}' parsed from branch '${branchName}' is invalid. It must be a number.`);
}
if (versionValues.length > 1) {
this.minor = parseInt(versionValues[1]);
if (isNaN(this.minor)) {
throw new Error(`The minor version '${versionValues[1]}' parsed from branch '${branchName}' is invalid. It must be a number.`);
}
}
}
GetPattern() {
let pattern = super.GetPattern();
if (!this.onVersionBranch) {
return pattern;
}
if (this.minor === undefined) {
return pattern.replace('*[0-9].*[0-9].*[0-9]', `${this.major}.*[0-9].*[0-9]`);
}
return pattern.replace('*[0-9].*[0-9].*[0-9]', `${this.major}.${this.minor}.*[0-9]`);
}
IsValid(tag) {
if (!this.onVersionBranch) {
return super.IsValid(tag);
}
if (!super.IsValid(tag)) {
return false;
}
const parsed = super.Parse(tag);
if (parsed[0] !== this.major) {
return false;
}
if (this.minor !== undefined && parsed[1] !== this.minor) {
return false;
}
return true;
}
Parse(tag) {
if (!this.onVersionBranch) {
return super.Parse(tag);
}
const parsed = super.Parse(tag);
return [this.major, this.minor || parsed[1], parsed[2]];
}
}
exports.BranchVersioningTagFormatter = BranchVersioningTagFormatter;
/***/ }),
/***/ 9105:
@ -266,6 +488,9 @@ class DefaultTagFormatter {
return `${this.tagPrefix}*[0-9].*[0-9].*[0-9]`;
}
Parse(tag) {
if (tag === '') {
return [0, 0, 0];
}
let tagParts = tag
.replace(this.tagPrefix, '<--!PREFIX!-->')
.replace(this.namespace, '<--!NAMESPACE!-->')
@ -287,10 +512,14 @@ class DefaultTagFormatter {
}
;
IsValid(tag) {
const regexEscape = (literal) => literal.replace(/\W/g, '\\$&');
const tagPrefix = regexEscape(this.tagPrefix);
const namespaceSeperator = regexEscape(this.namespaceSeperator);
const namespace = regexEscape(this.namespace);
if (!!this.namespace) {
return new RegExp(`^${this.tagPrefix}[0-9]+.[0-9]+.[0-9]+${this.namespaceSeperator}${this.namespace}$`).test(tag);
return new RegExp(`^${tagPrefix}[0-9]+\.[0-9]+\.[0-9]+${namespaceSeperator}${namespace}$`).test(tag);
}
return new RegExp(`^${this.tagPrefix}[0-9]+.[0-9]+.[0-9]+$`).test(tag);
return new RegExp(`^${tagPrefix}[0-9]+\.[0-9]+\.[0-9]+$`).test(tag);
}
}
exports.DefaultTagFormatter = DefaultTagFormatter;
@ -387,7 +616,7 @@ const ConfigurationProvider_1 = __nccwpck_require__(2614);
const core = __importStar(__nccwpck_require__(2186));
const VersionType_1 = __nccwpck_require__(895);
function setOutput(versionResult) {
const { major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, authors, currentCommit, previousCommit, previousVersion } = versionResult;
const { major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, isTagged, authors, currentCommit, previousCommit, previousVersion, debugOutput } = versionResult;
const repository = process.env.GITHUB_REPOSITORY;
if (!changed) {
core.info('No changes detected for this commit');
@ -403,18 +632,39 @@ function setOutput(versionResult) {
core.setOutput("increment", increment.toString());
core.setOutput("version_type", VersionType_1.VersionType[versionType].toLowerCase());
core.setOutput("changed", changed.toString());
core.setOutput("is_tagged", isTagged.toString());
core.setOutput("version_tag", versionTag);
core.setOutput("authors", authors);
core.setOutput("lastVersion", authors);
core.setOutput("previous_commit", previousCommit);
core.setOutput("previous_version", previousVersion);
core.setOutput("current_commit", currentCommit);
core.setOutput("debug_output", debugOutput);
}
function run() {
return __awaiter(this, void 0, void 0, function* () {
function toBool(value) {
if (!value || value.toLowerCase() === 'false') {
return false;
}
else if (value.toLowerCase() === 'true') {
return true;
}
return false;
}
function toStringOrBool(value) {
if (!value || value === 'false') {
return false;
}
if (value === 'true') {
return true;
}
return value;
}
const config = {
branch: core.getInput('branch'),
tagPrefix: core.getInput('tag_prefix'),
useBranches: core.getInput('use_branches') === 'true',
useBranches: toBool(core.getInput('use_branches')),
versionFromBranch: toStringOrBool(core.getInput('version_from_branch')),
majorPattern: core.getInput('major_pattern'),
minorPattern: core.getInput('minor_pattern'),
majorFlags: core.getInput('major_regexp_flags'),
@ -422,10 +672,17 @@ function run() {
versionFormat: core.getInput('version_format'),
changePath: core.getInput('change_path'),
namespace: core.getInput('namespace'),
bumpEachCommit: core.getInput('bump_each_commit') === 'true',
searchCommitBody: core.getInput('search_commit_body') === 'true',
userFormatType: core.getInput('user_format_type')
bumpEachCommit: toBool(core.getInput('bump_each_commit')),
searchCommitBody: toBool(core.getInput('search_commit_body')),
userFormatType: core.getInput('user_format_type'),
enablePrereleaseMode: toBool(core.getInput('enable_prerelease_mode')),
bumpEachCommitPatchPattern: core.getInput('bump_each_commit_patch_pattern'),
debug: toBool(core.getInput('debug')),
replay: ''
};
if (config.useBranches) {
core.warning(`The 'use_branches' input option is deprecated, please see the documentation for more information on how to use branches`);
}
if (config.versionFormat === '' && core.getInput('format') !== '') {
core.warning(`The 'format' input is deprecated, use 'versionFormat' instead`);
config.versionFormat = core.getInput('format');
@ -466,7 +723,10 @@ const VersionType_1 = __nccwpck_require__(895);
class BumpAlwaysVersionClassifier extends DefaultVersionClassifier_1.DefaultVersionClassifier {
constructor(config) {
super(config);
// Placeholder for consistency
this.enablePrereleaseMode = config.enablePrereleaseMode;
this.patchPattern = !config.bumpEachCommitPatchPattern ?
_ => true :
this.parsePattern(config.bumpEachCommitPatchPattern, "", config.searchCommitBody);
}
ClassifyAsync(lastRelease, commitSet) {
return __awaiter(this, void 0, void 0, function* () {
@ -475,27 +735,64 @@ class BumpAlwaysVersionClassifier extends DefaultVersionClassifier_1.DefaultVers
}
let { major, minor, patch } = lastRelease;
let type = VersionType_1.VersionType.None;
let increment = 0;
if (commitSet.commits.length === 0) {
return new VersionClassification_1.VersionClassification(type, 0, false, major, minor, patch);
}
for (let commit of commitSet.commits.reverse()) {
if (this.majorPattern(commit)) {
major += 1;
minor = 0;
patch = 0;
type = VersionType_1.VersionType.Major;
}
else if (this.minorPattern(commit)) {
minor += 1;
patch = 0;
type = VersionType_1.VersionType.Minor;
}
else {
patch += 1;
else if (this.patchPattern(commit) ||
(major === 0 && minor === 0 && patch === 0 && commitSet.commits.length > 0)) {
type = VersionType_1.VersionType.Patch;
}
else {
type = VersionType_1.VersionType.None;
}
return new VersionClassification_1.VersionClassification(type, 0, true, major, minor, patch);
if (this.enablePrereleaseMode && major === 0) {
switch (type) {
case VersionType_1.VersionType.Major:
case VersionType_1.VersionType.Minor:
minor += 1;
patch = 0;
increment = 0;
break;
case VersionType_1.VersionType.Patch:
patch += 1;
increment = 0;
break;
default:
increment++;
break;
}
}
else {
switch (type) {
case VersionType_1.VersionType.Major:
major += 1;
minor = 0;
patch = 0;
increment = 0;
break;
case VersionType_1.VersionType.Minor:
minor += 1;
patch = 0;
break;
case VersionType_1.VersionType.Patch:
patch += 1;
increment = 0;
break;
default:
increment++;
break;
}
}
}
return new VersionClassification_1.VersionClassification(type, increment, true, major, minor, patch);
});
}
}
@ -685,6 +982,12 @@ class DefaultCurrentCommitResolver {
return lastCommitAll === '';
});
}
ResolveBranchNameAsync() {
return __awaiter(this, void 0, void 0, function* () {
const branchName = this.branch == 'HEAD' ? yield (0, CommandRunner_1.cmd)('git', 'rev-parse', '--abbrev-ref', 'HEAD') : this.branch;
return branchName.trim();
});
}
}
exports.DefaultCurrentCommitResolver = DefaultCurrentCommitResolver;
@ -742,25 +1045,27 @@ class DefaultLastReleaseResolver {
return __awaiter(this, void 0, void 0, function* () {
const releasePattern = tagFormatter.GetPattern();
let currentTag = (yield (0, CommandRunner_1.cmd)(`git tag --points-at ${current} ${releasePattern}`)).trim();
currentTag = tagFormatter.IsValid(currentTag) ? currentTag : '';
const isTagged = currentTag !== '';
const [currentMajor, currentMinor, currentPatch] = !!currentTag ? tagFormatter.Parse(currentTag) : [null, null, null];
let tagsCount = 0;
let tag = '';
try {
const refPrefixPattern = this.useBranches ? 'refs/heads/' : 'refs/tags/';
if (!!currentTag) {
// If we already have the current branch tagged, we are checking for the previous one
// so that we will have an accurate increment (assuming the new tag is the expected one)
const command = `git for-each-ref --count=2 --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
tag = yield (0, CommandRunner_1.cmd)(command);
tag = tag
.split('\n')
.reverse()
const command = `git for-each-ref --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
const tags = (yield (0, CommandRunner_1.cmd)(command)).split('\n');
tagsCount = tags.length;
tag = tags
.find(t => tagFormatter.IsValid(t) && t !== currentTag) || '';
}
else {
const command = `git for-each-ref --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
let tags = yield (0, CommandRunner_1.cmd)(command);
const tags = (yield (0, CommandRunner_1.cmd)(command)).split('\n');
tagsCount = tags.length;
tag = tags
.split('\n')
.find(t => tagFormatter.IsValid(t)) || '';
}
tag = tag.trim();
@ -773,15 +1078,21 @@ class DefaultLastReleaseResolver {
// Since there is no remote, we assume that there are no other tags to pull. In
// practice this isn't likely to happen, but it keeps the test output from being
// polluted with a bunch of warnings.
if (tagsCount > 0) {
core.warning(`None of the ${tagsCount} tags(s) found were valid version tags for the present configuration. If this is unexpected, check to ensure that the configuration is correct and matches the tag format you are using.`);
}
else {
core.warning('No tags are present for this repository. If this is unexpected, check to ensure that tags have been pulled from the remote.');
}
}
const [major, minor, patch] = tagFormatter.Parse('');
// no release tags yet, use the initial commit as the root
return new ReleaseInformation_1.ReleaseInformation(0, 0, 0, '', currentMajor, currentMinor, currentPatch);
return new ReleaseInformation_1.ReleaseInformation(major, minor, patch, '', currentMajor, currentMinor, currentPatch, isTagged);
}
// parse the version tag
const [major, minor, patch] = tagFormatter.Parse(tag);
const root = yield (0, CommandRunner_1.cmd)('git', `merge-base`, tag, current);
return new ReleaseInformation_1.ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch);
return new ReleaseInformation_1.ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch, isTagged);
});
}
}
@ -813,10 +1124,13 @@ class DefaultVersionClassifier {
const searchBody = config.searchCommitBody;
this.majorPattern = this.parsePattern(config.majorPattern, config.majorFlags, searchBody);
this.minorPattern = this.parsePattern(config.minorPattern, config.minorFlags, searchBody);
this.enablePrereleaseMode = config.enablePrereleaseMode;
}
parsePattern(pattern, flags, searchBody) {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
var regex = new RegExp(pattern.slice(1, -1), flags);
if (/^\/.+\/[i]*$/.test(pattern)) {
const regexEnd = pattern.lastIndexOf('/');
const parsedFlags = pattern.slice(pattern.lastIndexOf('/') + 1);
const regex = new RegExp(pattern.slice(1, regexEnd), parsedFlags || flags);
return searchBody ?
(commit) => regex.test(commit.subject) || regex.test(commit.body) :
(commit) => regex.test(commit.subject);
@ -829,6 +1143,19 @@ class DefaultVersionClassifier {
}
}
getNextVersion(current, type) {
if (this.enablePrereleaseMode && current.major === 0) {
switch (type) {
case VersionType_1.VersionType.Major:
return { major: current.major, minor: current.minor + 1, patch: 0 };
case VersionType_1.VersionType.Minor:
case VersionType_1.VersionType.Patch:
return { major: current.major, minor: current.minor, patch: current.patch + 1 };
case VersionType_1.VersionType.None:
return { major: current.major, minor: current.minor, patch: current.patch };
default:
throw new Error(`Unknown change type: ${type}`);
}
}
switch (type) {
case VersionType_1.VersionType.Major:
return { major: current.major + 1, minor: 0, patch: 0 };
@ -876,8 +1203,8 @@ class DefaultVersionClassifier {
// - commit 3 was tagged v2.0.0 - v2.0.0+0
// - commit 4 - v2.0.1+0
const versionsMatch = lastRelease.currentMajor === major && lastRelease.currentMinor === minor && lastRelease.currentPatch === patch;
const currentIncremement = versionsMatch ? increment : 0;
return new VersionClassification_1.VersionClassification(VersionType_1.VersionType.None, currentIncremement, false, lastRelease.currentMajor, lastRelease.currentMinor, lastRelease.currentPatch);
const currentIncrement = versionsMatch ? increment : 0;
return new VersionClassification_1.VersionClassification(VersionType_1.VersionType.None, currentIncrement, false, lastRelease.currentMajor, lastRelease.currentMinor, lastRelease.currentPatch);
}
return new VersionClassification_1.VersionClassification(type, increment, changed, major, minor, patch);
});
@ -906,8 +1233,9 @@ class ReleaseInformation {
* @param currentMajor - the major version number from the current commit
* @param currentMinor - the minor version number from the current commit
* @param currentPatch - the patch version number from the current commit
* @param isTagged - whether the current commit is tagged with a version
*/
constructor(major, minor, patch, hash, currentMajor, currentMinor, currentPatch) {
constructor(major, minor, patch, hash, currentMajor, currentMinor, currentPatch, isTagged) {
this.major = major;
this.minor = minor;
this.patch = patch;
@ -915,6 +1243,7 @@ class ReleaseInformation {
this.currentMajor = currentMajor;
this.currentMinor = currentMinor;
this.currentPatch = currentPatch;
this.isTagged = isTagged;
}
}
exports.ReleaseInformation = ReleaseInformation;
@ -1001,8 +1330,9 @@ class VersionInformation {
* @param type - The type of change the current range represents
* @param commits - The list of commits for this version
* @param changed - True if the version has changed, false otherwise
* @param isTagged - True if the current commit is a version-tagged commit
*/
constructor(major, minor, patch, increment, type, commits, changed) {
constructor(major, minor, patch, increment, type, commits, changed, isTagged) {
this.major = major;
this.minor = minor;
this.patch = patch;
@ -1010,6 +1340,7 @@ class VersionInformation {
this.type = type;
this.commits = commits;
this.changed = changed;
this.isTagged = isTagged;
}
}
exports.VersionInformation = VersionInformation;
@ -1035,7 +1366,7 @@ var VersionType;
VersionType["Patch"] = "Patch";
/** Indicates no change--generally this means that the current commit is already tagged with a version */
VersionType["None"] = "None";
})(VersionType = exports.VersionType || (exports.VersionType = {}));
})(VersionType || (exports.VersionType = VersionType = {}));
/***/ }),
@ -1597,7 +1928,7 @@ class OidcClient {
.catch(error => {
throw new Error(`Failed to get ID Token. \n
Error Code : ${error.statusCode}\n
Error Message: ${error.result.message}`);
Error Message: ${error.message}`);
});
const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value;
if (!id_token) {
@ -2985,6 +3316,19 @@ class HttpClientResponse {
}));
});
}
readBodyBuffer() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
const chunks = [];
this.message.on('data', (chunk) => {
chunks.push(chunk);
});
this.message.on('end', () => {
resolve(Buffer.concat(chunks));
});
}));
});
}
}
exports.HttpClientResponse = HttpClientResponse;
function isHttps(requestUrl) {
@ -3489,8 +3833,14 @@ function getProxyUrl(reqUrl) {
}
})();
if (proxyVar) {
try {
return new URL(proxyVar);
}
catch (_a) {
if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://'))
return new URL(`http://${proxyVar}`);
}
}
else {
return undefined;
}
@ -3500,6 +3850,10 @@ function checkBypass(reqUrl) {
if (!reqUrl.hostname) {
return false;
}
const reqHost = reqUrl.hostname;
if (isLoopbackAddress(reqHost)) {
return true;
}
const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || '';
if (!noProxy) {
return false;
@ -3525,13 +3879,24 @@ function checkBypass(reqUrl) {
.split(',')
.map(x => x.trim().toUpperCase())
.filter(x => x)) {
if (upperReqHosts.some(x => x === upperNoProxyItem)) {
if (upperNoProxyItem === '*' ||
upperReqHosts.some(x => x === upperNoProxyItem ||
x.endsWith(`.${upperNoProxyItem}`) ||
(upperNoProxyItem.startsWith('.') &&
x.endsWith(`${upperNoProxyItem}`)))) {
return true;
}
}
return false;
}
exports.checkBypass = checkBypass;
function isLoopbackAddress(host) {
const hostLower = host.toLowerCase();
return (hostLower === 'localhost' ||
hostLower.startsWith('127.') ||
hostLower.startsWith('[::1]') ||
hostLower.startsWith('[0:0:0:0:0:0:0:1]'));
}
//# sourceMappingURL=proxy.js.map
/***/ }),
@ -3571,11 +3936,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
};
var _a;
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rename = exports.readlink = exports.readdir = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0;
exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.READONLY = exports.UV_FS_O_EXLOCK = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rm = exports.rename = exports.readlink = exports.readdir = exports.open = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0;
const fs = __importStar(__nccwpck_require__(7147));
const path = __importStar(__nccwpck_require__(1017));
_a = fs.promises, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink;
_a = fs.promises
// export const {open} = 'fs'
, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.open = _a.open, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rm = _a.rm, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink;
// export const {open} = 'fs'
exports.IS_WINDOWS = process.platform === 'win32';
// See https://github.com/nodejs/node/blob/d0153aee367422d0858105abec186da4dff0a0c5/deps/uv/include/uv/win.h#L691
exports.UV_FS_O_EXLOCK = 0x10000000;
exports.READONLY = fs.constants.O_RDONLY;
function exists(fsPath) {
return __awaiter(this, void 0, void 0, function* () {
try {
@ -3756,12 +4127,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.findInPath = exports.which = exports.mkdirP = exports.rmRF = exports.mv = exports.cp = void 0;
const assert_1 = __nccwpck_require__(9491);
const childProcess = __importStar(__nccwpck_require__(2081));
const path = __importStar(__nccwpck_require__(1017));
const util_1 = __nccwpck_require__(3837);
const ioUtil = __importStar(__nccwpck_require__(1962));
const exec = util_1.promisify(childProcess.exec);
const execFile = util_1.promisify(childProcess.execFile);
/**
* Copies a file or folder.
* Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js
@ -3842,61 +4209,23 @@ exports.mv = mv;
function rmRF(inputPath) {
return __awaiter(this, void 0, void 0, function* () {
if (ioUtil.IS_WINDOWS) {
// Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another
// program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del.
// Check for invalid characters
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
if (/[*"<>|]/.test(inputPath)) {
throw new Error('File path must not contain `*`, `"`, `<`, `>` or `|` on Windows');
}
}
try {
const cmdPath = ioUtil.getCmdPath();
if (yield ioUtil.isDirectory(inputPath, true)) {
yield exec(`${cmdPath} /s /c "rd /s /q "%inputPath%""`, {
env: { inputPath }
// note if path does not exist, error is silent
yield ioUtil.rm(inputPath, {
force: true,
maxRetries: 3,
recursive: true,
retryDelay: 300
});
}
else {
yield exec(`${cmdPath} /s /c "del /f /a "%inputPath%""`, {
env: { inputPath }
});
}
}
catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT')
throw err;
}
// Shelling out fails to remove a symlink folder with missing source, this unlink catches that
try {
yield ioUtil.unlink(inputPath);
}
catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT')
throw err;
}
}
else {
let isDir = false;
try {
isDir = yield ioUtil.isDirectory(inputPath);
}
catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT')
throw err;
return;
}
if (isDir) {
yield execFile(`rm`, [`-rf`, `${inputPath}`]);
}
else {
yield ioUtil.unlink(inputPath);
}
throw new Error(`File was unable to be removed ${err}`);
}
});
}
@ -4546,7 +4875,7 @@ exports["default"] = _default;
/***/ }),
/***/ 807:
/***/ 8292:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
@ -4664,7 +4993,7 @@ Object.defineProperty(exports, "__esModule", ({
}));
exports["default"] = void 0;
var _rng = _interopRequireDefault(__nccwpck_require__(807));
var _rng = _interopRequireDefault(__nccwpck_require__(8292));
var _stringify = _interopRequireDefault(__nccwpck_require__(8950));
@ -4886,7 +5215,7 @@ Object.defineProperty(exports, "__esModule", ({
}));
exports["default"] = void 0;
var _rng = _interopRequireDefault(__nccwpck_require__(807));
var _rng = _interopRequireDefault(__nccwpck_require__(8292));
var _stringify = _interopRequireDefault(__nccwpck_require__(8950));

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

106
guide.md Normal file
View file

@ -0,0 +1,106 @@
# Configuration Guide
## Choosing a Release Strategy
This section is designed to help you choose a release strategy for your project and help you configure GitHub Workflow to use that strategy. It is organized starting from the most simple with each strategy supporting more complex needs, allowing you to start at the top and continue until you find the simplest strategy that meets your needs.
Note that in the examples given `latest` is used, but you will likely want to pin your version to a specific version.
### Increment Every Release
If your project has no gating requirements and you want to release every time a commit is pushed to the default branch, you can use the _Increment Every Release_ strategy. This may be appropriate for documentation projects, very small projects, or in cases where "shipping" a broken version isn't a big deal. The key limitation of this strategy is that once you push a commit, the version is going to increments no matter what. If you push a version and your build or automated tests fail, you'll have a version that is broken and you'll have to increment the version again to fix it.
```yaml
- uses: paulhatch/semantic-version@latest
with:
bump_each_commit: true
```
### Increment from Commit Message
Very similar to the strategy above, using the _Increment from Commit Message_ means that you are making the decision to increment the version at the time you commit the code, however by using the `bump_each_commit_patch_pattern` parameter introduced in v5.1.0, you can prevent the version from incrementing for a commit unless it matches one of the patters (major, minor, or patch).
Compared to the _Increment Every Release_ strategy, this strategy allows you to make a decision about whether or not to increment the version for a particular commit, allowing you to add commits to a repo that do not increment the version. Again, you make the decision to increment the version at the time you commit the code, so this strategy may not be appropriate for some project types.
On the other hand, if you have a fast deployment strategy, such as "every commit goes to prod" and don't mind versions being created for failed builds, this may be the right choice.
```yaml
- uses: paulhatch/semantic-version@latest
with:
bump_each_commit: true
bump_each_commit_patch_pattern: "(PATCH)"
```
### Tag Versioning
This strategy is the most common and is the best option for many projects. It allows you to make the decision to release a version after the build has run, which is essentially the primary motivation and main purpose for this action.
The only real limitation of this strategy is that it does not allow for multiple versions to receive ongoing updates, which may be necessary for certain types of projects which are distributed and receive ongoing maintenance of older versions. This is in contrast to projects that are developed only for a single deployment and are not distributed.
Tags should generally not be created automatically as part of the build, which can cause strange behavior unless you've taken care to prevent race conditions. Creating tags automatically also largely negates the purpose of this strategy.
_This is the default behavior, so no special options are required._
```yaml
- uses: paulhatch/semantic-version@latest
```
### Branch + Tag Versioning
So far all the options considered have assumed that a single, ever incrementing version is being released, and that once a new major or minor version is tagged no further updates are made for previous versions. This is appropriate for many projects such as web applications and most libraries, however if you need to support on-going update to multiple major or major+minor versions, using only the approaches above can lead to problems if you are merging updates into multiple branches, as any tags may be picked up and cause the version of an older branch to unexpectedly jump.
To accomplish this, we can enable the `version_from_branch`, which will cause the major and optionally the minor version to be taken from the current branch name rather than the tag, and to filter out tags that do not begin with the same version number(s). The `version_from_branch` input can be either a boolean or a regex string to be used to identify the version from the branch name. By default this will be `[0-9]+.[0-9]+$|[0-9]+$` e.g. match the final number or pair of numbers separated by a `.`. This default is probably appropriate for the majority of cases as it will match any prefix, for example branches named:
- `release/v1`
- `release/v1.2`
- `v1`
- `v1.2`
- `1`
- `1.2`
Note that when using this strategy you should always tag at the same time as the branch is created to ensure that the increment value is correct.
```yaml
- uses: paulhatch/semantic-version@latest
with:
version_from_branch: true
```
Alternately, you can override the branch pattern.
```yaml
- uses: paulhatch/semantic-version@latest
with:
version_from_branch: "/v([0-9]+.[0-9]+$|[0-9]+)$/"
```
## Namespace Services / "Monorepo" Support
If your project contains multiple services which you wish to version independently, you can use the `namespace` and `change_path` inputs to provide a version for a specific service which increments only when a file in the specified path is changed. (Or, if you are only build on push/pull requests you can just use the GitHub Action's [`paths`/`paths-ignore`](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onpushpull_requestpull_request_targetpathspaths-ignore) feature to block the trigger itself and run the workflow only when files in a specific path are changed. In contrast this method will also work on other triggers like `workflow_dispatch`.)
```yaml
- id: version
uses: paulhatch/semantic-version@latest
with:
change_path: "src/my-service"
namespace: my-service
- name: Cancel if Unchanged
if: ${{ ! fromJSON(steps.version.outputs.changed) }}
run: |
gh run cancel ${{ github.run_id }}
gh run watch ${{ github.run_id }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
## Additional Configuration
| Value | Description |
| --- | --- |
| `tag_prefix` | The prefix to use for the tag. Defaults to `v`, generally you will use either `v` or an empty string. Note that the tag format is distinct from the version. Tags used for versioning must always follow the pattern `{tag_prefix}{major}.{minor}.{patch}` with and optional `-{namespace}` suffix. |
| `major_pattern` and `minor_pattern` | These strings are used to determine the type of version to create. If any commit message since matches the `major_pattern` the major version will be incremented, if it matches the `minor_pattern` the minor version will be incremented. If neither pattern matches, the patch version will be incremented. These can be specified either as strings or as regular expression by wrapping the expression in `/`. The defaults follow [Conventional Commits](https://www.conventionalcommits.org/): `/!:|BREAKING CHANGE:/` for major and `/feat:/` for minor. |
| `version_format` | A value such as `${major}.${minor}.${patch}-prerelease${increment}` that will be used to format the version value of the output, **formatting this value is the only effect of this input parameter!** It is not used for parsing or any other purpose. It is a convenient alternative to formatting the output in a subsequent step. |
| `user_format_type` | Indicates the format of the `authors` output. Can be `json` or `yaml`. |
| `enable_prerelease_mode` | If true, major changes to versions starting with 0 will result in a minor change, preventing ths initial version `1.0.0`` from being created automatically by someone checking in a commit with the major pattern. |

View file

@ -8,14 +8,16 @@ class ActionConfig {
this.branch = "HEAD";
/** The prefix to use to identify tags */
this.tagPrefix = "v";
/** Use branches instead of tags */
/** (Deprecated) Use branches instead of tags */
this.useBranches = false;
/** If true, the branch will be used to select the maximum version. */
this.versionFromBranch = false;
/** A string which, if present in a git commit, indicates that a change represents a major (breaking) change. Wrap with '/' to match using a regular expression. */
this.majorPattern = "(MAJOR)";
this.majorPattern = "/!:|BREAKING CHANGE:/";
/** A string which indicates the flags used by the `majorPattern` regular expression. */
this.majorFlags = "";
/** A string which, if present in a git commit, indicates that a change represents a minor (feature) change. Wrap with '/' to match using a regular expression. */
this.minorPattern = "(MINOR)";
this.minorPattern = "/feat:/";
/** A string which indicates the flags used by the `minorPattern` regular expression. */
this.minorFlags = "";
/** Pattern to use when formatting output version */
@ -30,6 +32,14 @@ class ActionConfig {
this.searchCommitBody = false;
/** The output method used to generate list of users, 'csv' or 'json'. Default is 'csv'. */
this.userFormatType = "csv";
/** Prevents pre-v1.0.0 version from automatically incrementing the major version. If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the versionType output is unchanged. */
this.enablePrereleaseMode = false;
/** If bump_each_commit is also set to true, setting this value will cause the version to increment only if the pattern specified is matched. */
this.bumpEachCommitPatchPattern = "";
/** If enabled, diagnostic information will be added to the action output. */
this.debug = false;
/** Diagnostics to replay */
this.replay = '';
}
}
exports.ActionConfig = ActionConfig;

View file

@ -35,7 +35,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.cmd = void 0;
// Using require instead of import to support integration testing
const exec = __importStar(require("@actions/exec"));
const DebugManager_1 = require("./DebugManager");
const cmd = (command, ...args) => __awaiter(void 0, void 0, void 0, function* () {
const debugManager = DebugManager_1.DebugManager.getInstance();
if (debugManager.isReplayMode()) {
return debugManager.replayCommand(command, args);
}
let output = '', errors = '';
const options = {
silent: true,
@ -46,15 +51,14 @@ const cmd = (command, ...args) => __awaiter(void 0, void 0, void 0, function* ()
silent: true
}
};
let caughtError = null;
try {
yield exec.exec(command, args, options);
}
catch (err) {
//core.info(`The command cd '${command} ${args.join(' ')}' failed: ${err}`);
}
if (errors !== '') {
//core.info(`stderr: ${errors}`);
caughtError = err;
}
debugManager.recordCommand(command, args, output, errors, caughtError);
return output;
});
exports.cmd = cmd;

View file

@ -2,6 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigurationProvider = void 0;
const CsvUserFormatter_1 = require("./formatting/CsvUserFormatter");
const BranchVersioningTagFormatter_1 = require("./formatting/BranchVersioningTagFormatter");
const DefaultTagFormatter_1 = require("./formatting/DefaultTagFormatter");
const DefaultVersionFormatter_1 = require("./formatting/DefaultVersionFormatter");
const JsonUserFormatter_1 = require("./formatting/JsonUserFormatter");
@ -10,9 +11,11 @@ const DefaultCurrentCommitResolver_1 = require("./providers/DefaultCurrentCommit
const DefaultVersionClassifier_1 = require("./providers/DefaultVersionClassifier");
const DefaultLastReleaseResolver_1 = require("./providers/DefaultLastReleaseResolver");
const BumpAlwaysVersionClassifier_1 = require("./providers/BumpAlwaysVersionClassifier");
const DebugManager_1 = require("./DebugManager");
class ConfigurationProvider {
constructor(config) {
this.config = config;
DebugManager_1.DebugManager.getInstance().initializeConfig(config);
}
GetCurrentCommitResolver() { return new DefaultCurrentCommitResolver_1.DefaultCurrentCommitResolver(this.config); }
GetLastReleaseResolver() { return new DefaultLastReleaseResolver_1.DefaultLastReleaseResolver(this.config); }
@ -24,7 +27,12 @@ class ConfigurationProvider {
return new DefaultVersionClassifier_1.DefaultVersionClassifier(this.config);
}
GetVersionFormatter() { return new DefaultVersionFormatter_1.DefaultVersionFormatter(this.config); }
GetTagFormatter() { return new DefaultTagFormatter_1.DefaultTagFormatter(this.config); }
GetTagFormatter(branchName) {
if (this.config.versionFromBranch) {
return new BranchVersioningTagFormatter_1.BranchVersioningTagFormatter(this.config, branchName);
}
return new DefaultTagFormatter_1.DefaultTagFormatter(this.config);
}
GetUserFormatter() {
switch (this.config.userFormatType) {
case 'json': return new JsonUserFormatter_1.JsonUserFormatter(this.config);

100
lib/DebugManager.js Normal file
View file

@ -0,0 +1,100 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DebugManager = void 0;
/** Utility class for managing debug mode and diagnostic information */
class DebugManager {
constructor() {
this.debugEnabled = false;
this.replayMode = false;
this.diagnosticInfo = null;
}
/** Returns the singleton instance of the DebugManager */
static getInstance() {
if (!DebugManager.instance) {
DebugManager.instance = new DebugManager();
}
return DebugManager.instance;
}
/** Clears the singleton instance of the DebugManager (used for testing) */
static clearState() {
DebugManager.instance = new DebugManager();
}
/** Returns true if debug mode is enabled */
isDebugEnabled() {
return this.debugEnabled;
}
/** Returns true if replay mode is enabled */
isReplayMode() {
return this.replayMode;
}
initializeConfig(config) {
if (config.debug) {
this.setDebugEnabled(true);
}
else if (config.replay.length > 0) {
this.replayFromDiagnostics(config.replay);
}
}
/** Enables or disables debug mode, also clears any existing diagnostics info */
setDebugEnabled(enableDebug = true) {
this.debugEnabled = enableDebug;
this.replayMode = false;
this.diagnosticInfo = new DiagnosticInfo();
}
;
/** Enables replay mode and loads the diagnostic information from the specified string */
replayFromDiagnostics(diagnostics) {
this.debugEnabled = false;
this.replayMode = true;
this.diagnosticInfo = JSON.parse(diagnostics);
}
/** Returns a JSON string containing the diagnostic information for this run */
getDebugOutput(emptyRepo = false) {
return this.isDebugEnabled() ? JSON.stringify(this.diagnosticInfo) : '';
}
/** Records a command and its output for diagnostic purposes */
recordCommand(command, args, output, stderr, error) {
var _a;
if (this.isDebugEnabled()) {
(_a = this.diagnosticInfo) === null || _a === void 0 ? void 0 : _a.recordCommand(command, args, output, stderr, error);
}
}
/** Replays the specified command and returns the output */
replayCommand(command, args) {
if (this.diagnosticInfo === null) {
throw new Error('No diagnostic information available for replay');
}
const commandResult = this.diagnosticInfo.commands.find(c => c.command === command && JSON.stringify(c.args) === JSON.stringify(args));
if (!commandResult) {
throw new Error(`No result found in diagnostic for command "${command}"`);
}
if (commandResult.error) {
throw commandResult.error;
}
if (commandResult.stderr) {
console.error(commandResult.stderr);
}
return commandResult.output;
}
}
exports.DebugManager = DebugManager;
/** Represents a CLI command result */
class CommandResult {
constructor(command, args, output, stderr, error) {
this.command = command;
this.args = args;
this.output = output;
this.stderr = stderr;
this.error = error;
}
}
/** Represents the result of the commands executed for a run */
class DiagnosticInfo {
constructor() {
this.commands = [];
this.empty = false;
}
recordCommand(command, args, output, stderr, error) {
this.commands.push(new CommandResult(command, args, output, stderr, error));
}
}

View file

@ -13,12 +13,14 @@ class VersionResult {
* @param formattedVersion - The formatted semantic version
* @param versionTag - The string to be used as a Git tag
* @param changed - True if the version was changed, otherwise false
* @param isTagged - True if the commit had a tag that matched the `versionTag` format
* @param authors - Authors formatted according to the format mode (e.g. JSON, CSV, YAML, etc.)
* @param currentCommit - The current commit hash
* @param previousCommit - The previous commit hash
* @param previousVersion - the previous version
* @param previousVersion - The previous version
* @param debugOutput - Diagnostic information, if debug is enabled
*/
constructor(major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, authors, currentCommit, previousCommit, previousVersion) {
constructor(major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, isTagged, authors, currentCommit, previousCommit, previousVersion, debugOutput) {
this.major = major;
this.minor = minor;
this.patch = patch;
@ -27,10 +29,12 @@ class VersionResult {
this.formattedVersion = formattedVersion;
this.versionTag = versionTag;
this.changed = changed;
this.isTagged = isTagged;
this.authors = authors;
this.currentCommit = currentCommit;
this.previousCommit = previousCommit;
this.previousVersion = previousVersion;
this.debugOutput = debugOutput;
}
}
exports.VersionResult = VersionResult;

View file

@ -14,6 +14,7 @@ const VersionResult_1 = require("./VersionResult");
const VersionType_1 = require("./providers/VersionType");
const UserInfo_1 = require("./providers/UserInfo");
const VersionInformation_1 = require("./providers/VersionInformation");
const DebugManager_1 = require("./DebugManager");
function runAction(configurationProvider) {
return __awaiter(this, void 0, void 0, function* () {
const currentCommitResolver = configurationProvider.GetCurrentCommitResolver();
@ -21,20 +22,22 @@ function runAction(configurationProvider) {
const commitsProvider = configurationProvider.GetCommitsProvider();
const versionClassifier = configurationProvider.GetVersionClassifier();
const versionFormatter = configurationProvider.GetVersionFormatter();
const tagFormmater = configurationProvider.GetTagFormatter();
const tagFormatter = configurationProvider.GetTagFormatter(yield currentCommitResolver.ResolveBranchNameAsync());
const userFormatter = configurationProvider.GetUserFormatter();
const debugManager = DebugManager_1.DebugManager.getInstance();
if (yield currentCommitResolver.IsEmptyRepoAsync()) {
const versionInfo = new VersionInformation_1.VersionInformation(0, 0, 0, 0, VersionType_1.VersionType.None, [], false);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormmater.Format(versionInfo), versionInfo.changed, userFormatter.Format('author', []), '', '', '0.0.0');
const versionInfo = new VersionInformation_1.VersionInformation(0, 0, 0, 0, VersionType_1.VersionType.None, [], false, false);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormatter.Format(versionInfo), versionInfo.changed, versionInfo.isTagged, userFormatter.Format('author', []), '', '', tagFormatter.Parse(tagFormatter.Format(versionInfo)).join('.'), debugManager.getDebugOutput(true));
}
const currentCommit = yield currentCommitResolver.ResolveAsync();
const lastRelease = yield lastReleaseResolver.ResolveAsync(currentCommit, tagFormmater);
const lastRelease = yield lastReleaseResolver.ResolveAsync(currentCommit, tagFormatter);
const commitSet = yield commitsProvider.GetCommitsAsync(lastRelease.hash, currentCommit);
const classification = yield versionClassifier.ClassifyAsync(lastRelease, commitSet);
const { isTagged } = lastRelease;
const { major, minor, patch, increment, type, changed } = classification;
// At this point all necessary data has been pulled from the database, create
// version information to be used by the formatters
let versionInfo = new VersionInformation_1.VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed);
let versionInfo = new VersionInformation_1.VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed, isTagged);
// Group all the authors together, count the number of commits per author
const allAuthors = versionInfo.commits
.reduce((acc, commit) => {
@ -46,7 +49,7 @@ function runAction(configurationProvider) {
const authors = Object.values(allAuthors)
.map((u) => new UserInfo_1.UserInfo(u.n, u.e, u.c))
.sort((a, b) => b.commits - a.commits);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormmater.Format(versionInfo), versionInfo.changed, userFormatter.Format('author', authors), currentCommit, lastRelease.hash, `${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionInfo.type, versionFormatter.Format(versionInfo), tagFormatter.Format(versionInfo), versionInfo.changed, versionInfo.isTagged, userFormatter.Format('author', authors), currentCommit, lastRelease.hash, `${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`, debugManager.getDebugOutput());
});
}
exports.runAction = runAction;

View file

@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BranchVersioningTagFormatter = void 0;
const DefaultTagFormatter_1 = require("./DefaultTagFormatter");
/** Default tag formatter which allows a prefix to be specified */
class BranchVersioningTagFormatter extends DefaultTagFormatter_1.DefaultTagFormatter {
getRegex(pattern) {
if (/^\/.+\/[i]*$/.test(pattern)) {
const regexEnd = pattern.lastIndexOf('/');
const parsedFlags = pattern.slice(pattern.lastIndexOf('/') + 1);
return new RegExp(pattern.slice(1, regexEnd), parsedFlags);
}
return new RegExp(pattern);
}
constructor(config, branchName) {
super(config);
const pattern = config.versionFromBranch === true ?
new RegExp("[0-9]+.[0-9]+$|[0-9]+$") :
this.getRegex(config.versionFromBranch);
const result = pattern.exec(branchName);
if (result === null) {
this.major = NaN;
this.onVersionBranch = false;
return;
}
let branchVersion;
switch (result === null || result === void 0 ? void 0 : result.length) {
case 1:
branchVersion = result[0];
break;
case 2:
branchVersion = result[1];
break;
default:
throw new Error(`Unable to parse version from branch named '${branchName}' using pattern '${pattern}'`);
}
this.onVersionBranch = true;
const versionValues = branchVersion.split('.');
if (versionValues.length > 2) {
throw new Error(`The version string '${branchVersion}' parsed from branch '${branchName}' is invalid. It must be in the format 'major.minor' or 'major'`);
}
this.major = parseInt(versionValues[0]);
if (isNaN(this.major)) {
throw new Error(`The major version '${versionValues[0]}' parsed from branch '${branchName}' is invalid. It must be a number.`);
}
if (versionValues.length > 1) {
this.minor = parseInt(versionValues[1]);
if (isNaN(this.minor)) {
throw new Error(`The minor version '${versionValues[1]}' parsed from branch '${branchName}' is invalid. It must be a number.`);
}
}
}
GetPattern() {
let pattern = super.GetPattern();
if (!this.onVersionBranch) {
return pattern;
}
if (this.minor === undefined) {
return pattern.replace('*[0-9].*[0-9].*[0-9]', `${this.major}.*[0-9].*[0-9]`);
}
return pattern.replace('*[0-9].*[0-9].*[0-9]', `${this.major}.${this.minor}.*[0-9]`);
}
IsValid(tag) {
if (!this.onVersionBranch) {
return super.IsValid(tag);
}
if (!super.IsValid(tag)) {
return false;
}
const parsed = super.Parse(tag);
if (parsed[0] !== this.major) {
return false;
}
if (this.minor !== undefined && parsed[1] !== this.minor) {
return false;
}
return true;
}
Parse(tag) {
if (!this.onVersionBranch) {
return super.Parse(tag);
}
const parsed = super.Parse(tag);
return [this.major, this.minor || parsed[1], parsed[2]];
}
}
exports.BranchVersioningTagFormatter = BranchVersioningTagFormatter;

View file

@ -22,6 +22,9 @@ class DefaultTagFormatter {
return `${this.tagPrefix}*[0-9].*[0-9].*[0-9]`;
}
Parse(tag) {
if (tag === '') {
return [0, 0, 0];
}
let tagParts = tag
.replace(this.tagPrefix, '<--!PREFIX!-->')
.replace(this.namespace, '<--!NAMESPACE!-->')
@ -43,10 +46,14 @@ class DefaultTagFormatter {
}
;
IsValid(tag) {
const regexEscape = (literal) => literal.replace(/\W/g, '\\$&');
const tagPrefix = regexEscape(this.tagPrefix);
const namespaceSeperator = regexEscape(this.namespaceSeperator);
const namespace = regexEscape(this.namespace);
if (!!this.namespace) {
return new RegExp(`^${this.tagPrefix}[0-9]+.[0-9]+.[0-9]+${this.namespaceSeperator}${this.namespace}$`).test(tag);
return new RegExp(`^${tagPrefix}[0-9]+\.[0-9]+\.[0-9]+${namespaceSeperator}${namespace}$`).test(tag);
}
return new RegExp(`^${this.tagPrefix}[0-9]+.[0-9]+.[0-9]+$`).test(tag);
return new RegExp(`^${tagPrefix}[0-9]+\.[0-9]+\.[0-9]+$`).test(tag);
}
}
exports.DefaultTagFormatter = DefaultTagFormatter;

View file

@ -38,7 +38,7 @@ const ConfigurationProvider_1 = require("./ConfigurationProvider");
const core = __importStar(require("@actions/core"));
const VersionType_1 = require("./providers/VersionType");
function setOutput(versionResult) {
const { major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, authors, currentCommit, previousCommit, previousVersion } = versionResult;
const { major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, isTagged, authors, currentCommit, previousCommit, previousVersion, debugOutput } = versionResult;
const repository = process.env.GITHUB_REPOSITORY;
if (!changed) {
core.info('No changes detected for this commit');
@ -54,18 +54,39 @@ function setOutput(versionResult) {
core.setOutput("increment", increment.toString());
core.setOutput("version_type", VersionType_1.VersionType[versionType].toLowerCase());
core.setOutput("changed", changed.toString());
core.setOutput("is_tagged", isTagged.toString());
core.setOutput("version_tag", versionTag);
core.setOutput("authors", authors);
core.setOutput("lastVersion", authors);
core.setOutput("previous_commit", previousCommit);
core.setOutput("previous_version", previousVersion);
core.setOutput("current_commit", currentCommit);
core.setOutput("debug_output", debugOutput);
}
function run() {
return __awaiter(this, void 0, void 0, function* () {
function toBool(value) {
if (!value || value.toLowerCase() === 'false') {
return false;
}
else if (value.toLowerCase() === 'true') {
return true;
}
return false;
}
function toStringOrBool(value) {
if (!value || value === 'false') {
return false;
}
if (value === 'true') {
return true;
}
return value;
}
const config = {
branch: core.getInput('branch'),
tagPrefix: core.getInput('tag_prefix'),
useBranches: core.getInput('use_branches') === 'true',
useBranches: toBool(core.getInput('use_branches')),
versionFromBranch: toStringOrBool(core.getInput('version_from_branch')),
majorPattern: core.getInput('major_pattern'),
minorPattern: core.getInput('minor_pattern'),
majorFlags: core.getInput('major_regexp_flags'),
@ -73,10 +94,17 @@ function run() {
versionFormat: core.getInput('version_format'),
changePath: core.getInput('change_path'),
namespace: core.getInput('namespace'),
bumpEachCommit: core.getInput('bump_each_commit') === 'true',
searchCommitBody: core.getInput('search_commit_body') === 'true',
userFormatType: core.getInput('user_format_type')
bumpEachCommit: toBool(core.getInput('bump_each_commit')),
searchCommitBody: toBool(core.getInput('search_commit_body')),
userFormatType: core.getInput('user_format_type'),
enablePrereleaseMode: toBool(core.getInput('enable_prerelease_mode')),
bumpEachCommitPatchPattern: core.getInput('bump_each_commit_patch_pattern'),
debug: toBool(core.getInput('debug')),
replay: ''
};
if (config.useBranches) {
core.warning(`The 'use_branches' input option is deprecated, please see the documentation for more information on how to use branches`);
}
if (config.versionFormat === '' && core.getInput('format') !== '') {
core.warning(`The 'format' input is deprecated, use 'versionFormat' instead`);
config.versionFormat = core.getInput('format');

View file

@ -16,7 +16,10 @@ const VersionType_1 = require("./VersionType");
class BumpAlwaysVersionClassifier extends DefaultVersionClassifier_1.DefaultVersionClassifier {
constructor(config) {
super(config);
// Placeholder for consistency
this.enablePrereleaseMode = config.enablePrereleaseMode;
this.patchPattern = !config.bumpEachCommitPatchPattern ?
_ => true :
this.parsePattern(config.bumpEachCommitPatchPattern, "", config.searchCommitBody);
}
ClassifyAsync(lastRelease, commitSet) {
return __awaiter(this, void 0, void 0, function* () {
@ -25,27 +28,64 @@ class BumpAlwaysVersionClassifier extends DefaultVersionClassifier_1.DefaultVers
}
let { major, minor, patch } = lastRelease;
let type = VersionType_1.VersionType.None;
let increment = 0;
if (commitSet.commits.length === 0) {
return new VersionClassification_1.VersionClassification(type, 0, false, major, minor, patch);
}
for (let commit of commitSet.commits.reverse()) {
if (this.majorPattern(commit)) {
major += 1;
minor = 0;
patch = 0;
type = VersionType_1.VersionType.Major;
}
else if (this.minorPattern(commit)) {
minor += 1;
patch = 0;
type = VersionType_1.VersionType.Minor;
}
else {
patch += 1;
else if (this.patchPattern(commit) ||
(major === 0 && minor === 0 && patch === 0 && commitSet.commits.length > 0)) {
type = VersionType_1.VersionType.Patch;
}
else {
type = VersionType_1.VersionType.None;
}
return new VersionClassification_1.VersionClassification(type, 0, true, major, minor, patch);
if (this.enablePrereleaseMode && major === 0) {
switch (type) {
case VersionType_1.VersionType.Major:
case VersionType_1.VersionType.Minor:
minor += 1;
patch = 0;
increment = 0;
break;
case VersionType_1.VersionType.Patch:
patch += 1;
increment = 0;
break;
default:
increment++;
break;
}
}
else {
switch (type) {
case VersionType_1.VersionType.Major:
major += 1;
minor = 0;
patch = 0;
increment = 0;
break;
case VersionType_1.VersionType.Minor:
minor += 1;
patch = 0;
break;
case VersionType_1.VersionType.Patch:
patch += 1;
increment = 0;
break;
default:
increment++;
break;
}
}
}
return new VersionClassification_1.VersionClassification(type, increment, true, major, minor, patch);
});
}
}

View file

@ -29,5 +29,11 @@ class DefaultCurrentCommitResolver {
return lastCommitAll === '';
});
}
ResolveBranchNameAsync() {
return __awaiter(this, void 0, void 0, function* () {
const branchName = this.branch == 'HEAD' ? yield (0, CommandRunner_1.cmd)('git', 'rev-parse', '--abbrev-ref', 'HEAD') : this.branch;
return branchName.trim();
});
}
}
exports.DefaultCurrentCommitResolver = DefaultCurrentCommitResolver;

View file

@ -45,25 +45,27 @@ class DefaultLastReleaseResolver {
return __awaiter(this, void 0, void 0, function* () {
const releasePattern = tagFormatter.GetPattern();
let currentTag = (yield (0, CommandRunner_1.cmd)(`git tag --points-at ${current} ${releasePattern}`)).trim();
currentTag = tagFormatter.IsValid(currentTag) ? currentTag : '';
const isTagged = currentTag !== '';
const [currentMajor, currentMinor, currentPatch] = !!currentTag ? tagFormatter.Parse(currentTag) : [null, null, null];
let tagsCount = 0;
let tag = '';
try {
const refPrefixPattern = this.useBranches ? 'refs/heads/' : 'refs/tags/';
if (!!currentTag) {
// If we already have the current branch tagged, we are checking for the previous one
// so that we will have an accurate increment (assuming the new tag is the expected one)
const command = `git for-each-ref --count=2 --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
tag = yield (0, CommandRunner_1.cmd)(command);
tag = tag
.split('\n')
.reverse()
const command = `git for-each-ref --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
const tags = (yield (0, CommandRunner_1.cmd)(command)).split('\n');
tagsCount = tags.length;
tag = tags
.find(t => tagFormatter.IsValid(t) && t !== currentTag) || '';
}
else {
const command = `git for-each-ref --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
let tags = yield (0, CommandRunner_1.cmd)(command);
const tags = (yield (0, CommandRunner_1.cmd)(command)).split('\n');
tagsCount = tags.length;
tag = tags
.split('\n')
.find(t => tagFormatter.IsValid(t)) || '';
}
tag = tag.trim();
@ -76,15 +78,21 @@ class DefaultLastReleaseResolver {
// Since there is no remote, we assume that there are no other tags to pull. In
// practice this isn't likely to happen, but it keeps the test output from being
// polluted with a bunch of warnings.
if (tagsCount > 0) {
core.warning(`None of the ${tagsCount} tags(s) found were valid version tags for the present configuration. If this is unexpected, check to ensure that the configuration is correct and matches the tag format you are using.`);
}
else {
core.warning('No tags are present for this repository. If this is unexpected, check to ensure that tags have been pulled from the remote.');
}
}
const [major, minor, patch] = tagFormatter.Parse('');
// no release tags yet, use the initial commit as the root
return new ReleaseInformation_1.ReleaseInformation(0, 0, 0, '', currentMajor, currentMinor, currentPatch);
return new ReleaseInformation_1.ReleaseInformation(major, minor, patch, '', currentMajor, currentMinor, currentPatch, isTagged);
}
// parse the version tag
const [major, minor, patch] = tagFormatter.Parse(tag);
const root = yield (0, CommandRunner_1.cmd)('git', `merge-base`, tag, current);
return new ReleaseInformation_1.ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch);
return new ReleaseInformation_1.ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch, isTagged);
});
}
}

View file

@ -17,10 +17,13 @@ class DefaultVersionClassifier {
const searchBody = config.searchCommitBody;
this.majorPattern = this.parsePattern(config.majorPattern, config.majorFlags, searchBody);
this.minorPattern = this.parsePattern(config.minorPattern, config.minorFlags, searchBody);
this.enablePrereleaseMode = config.enablePrereleaseMode;
}
parsePattern(pattern, flags, searchBody) {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
var regex = new RegExp(pattern.slice(1, -1), flags);
if (/^\/.+\/[i]*$/.test(pattern)) {
const regexEnd = pattern.lastIndexOf('/');
const parsedFlags = pattern.slice(pattern.lastIndexOf('/') + 1);
const regex = new RegExp(pattern.slice(1, regexEnd), parsedFlags || flags);
return searchBody ?
(commit) => regex.test(commit.subject) || regex.test(commit.body) :
(commit) => regex.test(commit.subject);
@ -33,6 +36,19 @@ class DefaultVersionClassifier {
}
}
getNextVersion(current, type) {
if (this.enablePrereleaseMode && current.major === 0) {
switch (type) {
case VersionType_1.VersionType.Major:
return { major: current.major, minor: current.minor + 1, patch: 0 };
case VersionType_1.VersionType.Minor:
case VersionType_1.VersionType.Patch:
return { major: current.major, minor: current.minor, patch: current.patch + 1 };
case VersionType_1.VersionType.None:
return { major: current.major, minor: current.minor, patch: current.patch };
default:
throw new Error(`Unknown change type: ${type}`);
}
}
switch (type) {
case VersionType_1.VersionType.Major:
return { major: current.major + 1, minor: 0, patch: 0 };
@ -80,8 +96,8 @@ class DefaultVersionClassifier {
// - commit 3 was tagged v2.0.0 - v2.0.0+0
// - commit 4 - v2.0.1+0
const versionsMatch = lastRelease.currentMajor === major && lastRelease.currentMinor === minor && lastRelease.currentPatch === patch;
const currentIncremement = versionsMatch ? increment : 0;
return new VersionClassification_1.VersionClassification(VersionType_1.VersionType.None, currentIncremement, false, lastRelease.currentMajor, lastRelease.currentMinor, lastRelease.currentPatch);
const currentIncrement = versionsMatch ? increment : 0;
return new VersionClassification_1.VersionClassification(VersionType_1.VersionType.None, currentIncrement, false, lastRelease.currentMajor, lastRelease.currentMinor, lastRelease.currentPatch);
}
return new VersionClassification_1.VersionClassification(type, increment, changed, major, minor, patch);
});

View file

@ -12,8 +12,9 @@ class ReleaseInformation {
* @param currentMajor - the major version number from the current commit
* @param currentMinor - the minor version number from the current commit
* @param currentPatch - the patch version number from the current commit
* @param isTagged - whether the current commit is tagged with a version
*/
constructor(major, minor, patch, hash, currentMajor, currentMinor, currentPatch) {
constructor(major, minor, patch, hash, currentMajor, currentMinor, currentPatch, isTagged) {
this.major = major;
this.minor = minor;
this.patch = patch;
@ -21,6 +22,7 @@ class ReleaseInformation {
this.currentMajor = currentMajor;
this.currentMinor = currentMinor;
this.currentPatch = currentPatch;
this.isTagged = isTagged;
}
}
exports.ReleaseInformation = ReleaseInformation;

View file

@ -15,8 +15,9 @@ class VersionInformation {
* @param type - The type of change the current range represents
* @param commits - The list of commits for this version
* @param changed - True if the version has changed, false otherwise
* @param isTagged - True if the current commit is a version-tagged commit
*/
constructor(major, minor, patch, increment, type, commits, changed) {
constructor(major, minor, patch, increment, type, commits, changed, isTagged) {
this.major = major;
this.minor = minor;
this.patch = patch;
@ -24,6 +25,7 @@ class VersionInformation {
this.type = type;
this.commits = commits;
this.changed = changed;
this.isTagged = isTagged;
}
}
exports.VersionInformation = VersionInformation;

View file

@ -12,4 +12,4 @@ var VersionType;
VersionType["Patch"] = "Patch";
/** Indicates no change--generally this means that the current commit is already tagged with a version */
VersionType["None"] = "None";
})(VersionType = exports.VersionType || (exports.VersionType = {}));
})(VersionType || (exports.VersionType = VersionType = {}));

9221
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@
"format-check": "prettier --check **/**.ts",
"lint": "eslint src/**/*.ts",
"package": "ncc build --source-map --license licenses.txt",
"test": "jest --config ./jest.config.js",
"test": "jest --runInBand --config ./jest.config.js",
"all": "npm run build && npm run format && npm run lint && npm run package && npm test"
},
"repository": {
@ -28,20 +28,20 @@
},
"homepage": "https://github.com/paulhatch/semantic-version#readme",
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1"
},
"devDependencies": {
"@types/node": "^18.11.9",
"@typescript-eslint/parser": "^5.17.0",
"@vercel/ncc": "^0.34.0",
"eslint": "^8.12.0",
"eslint-plugin-github": "^4.3.6",
"eslint-plugin-jest": "^27.1.3",
"jest": "^29.2.2",
"@types/node": "^20.11.13",
"@typescript-eslint/parser": "^6.20.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.56.0",
"eslint-plugin-github": "^4.10.1",
"eslint-plugin-jest": "^27.6.3",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
"prettier": "^2.6.1",
"ts-jest": "^29.0.3",
"typescript": "^4.6.3"
"prettier": "^3.2.4",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
}
}

View file

@ -1,12 +1,6 @@
![Build](https://github.com/PaulHatch/semantic-version/workflows/Build/badge.svg)
> *Version 5-alpha now available!*
> Version 5 adds support for several new features and allows easier extension/customization.
> ##### Breaking Changes
> - Versions now use the version number (`--sort=-v:*refname`) rather than date to determine which tag is the latest.
> - "Short tag" support removed
>
> See the [release notes](https://github.com/PaulHatch/semantic-version/releases/tag/v5.0.0-alpha) for more information.
See the [configuration guide](guide.md) for help getting started, selecting a versioning strategy and example configurations, or [contributing.md](contributing.md) for information on how to get help or contribute to this project.
# Git-Based Semantic Versioning
@ -19,8 +13,9 @@ automatically while publishing version that only increment by one value per
release. To accomplish this, the next version number is calculated along with
a commit increment indicating the number of commits for this version. The
commit messages are inspected to determine the type of version change the next
version represents. Including the term `(MAJOR)` or `(MINOR)` in the commit
message alters the type of change the next version will represent.
version represents. By default, this action follows [Conventional Commits](https://www.conventionalcommits.org/)
patterns: commits with `feat:` trigger minor version bumps, and commits with a `!` suffix
(e.g., `feat!:`, `fix!:`) or containing `BREAKING CHANGE:` trigger major version bumps.
# Background
@ -45,7 +40,7 @@ To solve this problem, this action calculates the next _implied_ version based o
the most recently tagged version and the commit messages. An additional value called
the "increment" tracks the count of commits since the last version change, allowing
a label to be created to mark pre-release versions. The version produced by this
action is always the_implied version (unless `bump_each_commit` is set to `true`).
action is always the implied version (unless `bump_each_commit` is set to `true`).
Subsequently tagging a commit that is chosen as the implied version is what bumps
the version for future commits.
@ -56,7 +51,8 @@ _Unless the current commit is already tagged, the version produced by this actio
## Major and Minor Versions
The commit messages for the span of commits from the last tag are checked for the
presence of the designated terms (`(MAJOR)` or `(MINOR)` by default), if a term
presence of version bump patterns. By default, `feat:` triggers a minor version bump,
while `!:` (e.g., `feat!:`, `fix!:`) or `BREAKING CHANGE:` triggers a major version bump. If a pattern
is encountered that commit is treated as the start of a major or minor version
instead of the default patch level. As with normal commits the implied version
will only increment by one value since the last tag regardless of how many major
@ -79,21 +75,21 @@ it will be given the new version if the build were to be retriggered, for exampl
<!-- start usage -->
```yaml
- uses: paulhatch/semantic-version@v4.0.2
- uses: paulhatch/semantic-version@v5.4.0
with:
# The prefix to use to identify tags
tag_prefix: "v"
# A string which, if present in a git commit, indicates that a change represents a
# major (breaking) change, supports regular expressions wrapped with '/'
major_pattern: "(MAJOR)"
major_pattern: "/!:|BREAKING CHANGE:/"
# A string which indicates the flags used by the `major_pattern` regular expression. Supported flags: idgs
major_regexp_flags: ""
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
minor_pattern: "(MINOR)"
minor_pattern: "/feat:/"
# A string which indicates the flags used by the `minor_pattern` regular expression. Supported flags: idgs
minor_regexp_flags: ""
# A string to determine the format of the version output
format: "${major}.${minor}.${patch}-prerelease${increment}"
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
# Optional path to check for changes. If any changes are detected in the path the
# 'changed' output will true. Enter multiple paths separated by spaces.
change_path: "src/my-service"
@ -101,10 +97,19 @@ it will be given the new version if the build were to be retriggered, for exampl
namespace: my-service
# If this is set to true, *every* commit will be treated as a new version.
bump_each_commit: false
# If bump_each_commit is also set to true, setting this value will cause the version to increment only if the pattern specified is matched.
bump_each_commit_patch_pattern: ""
# If true, the body of commits will also be searched for major/minor patterns to determine the version type.
search_commit_body: false
# The output method used to generate list of users, 'csv' or 'json'.
user_format_type: "csv"
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
enable_prerelease_mode: true
# If enabled, diagnostic information will be added to the action output.
debug: false
# If true, the branch will be used to select the maximum version.
version_from_branch: false
```
## Outputs
@ -115,7 +120,12 @@ it will be given the new version if the build were to be retriggered, for exampl
- *version* is a formatted version string created using the format input. This is a convenience value to provide a preformatted representation of the data generated by this action.
- *version_tag* is a string identifier that would be used to tag the current commit as the "released" version. Typically this would only be used to generate a Git tag name.
- *changed* indicates whether there was a change since the last version if change_path was specified. If no `change_path` was specified this value will always be true since the entire repo is considered. (It is possible to create a commit with no changes, but the Git cli rejects this by default and this case is not considered here)
- *is_tagged* indicates whether the current commit has a tag matching `tag_prefix`
- *authors* is a list of authors that have committed to this version, formatted as either csv or json.
- *current_commit* is the current commit hash.
- *previous_commit* is the previous commit hash.
- *previous_version* is the previous version.
- *debug_output* will show diagnostic information, if debug is enabled
There are two types of "version" string, one is the semantic version output that can be used to identify a build and can include prerelease data and metadata specific to the commit such as `v2.0.1-pre001+cf6e75` (you would produce this string yourself using the version information from this action plus whatever metadata you wanted to add), the other is the tag version string, which identifies a specific commit as being a specific version.
@ -148,12 +158,12 @@ like `v1.2.3+0-db` could be configured like this:
```yaml
- name: Application Version
id: version
uses: paulhatch/semantic-version@v4.0.2
uses: paulhatch/semantic-version@v5.4.0
with:
change_path: "src/service"
- name: Database Version
id: db-version
uses: paulhatch/semantic-version@v4.0.2
uses: paulhatch/semantic-version@v5.4.0
with:
major_pattern: "(MAJOR-DB)"
minor_pattern: "(MINOR-DB)"
@ -168,7 +178,7 @@ This history is required to determine the version correctly. To include the hist
and tags, specify the fetch-depth parameter in your checkout action declaration. Specify
zero to pull the full history and tags.
```
```yaml
- name: Checkout
uses: actions/checkout@v2
with:

View file

@ -4,14 +4,16 @@ export class ActionConfig {
public branch: string = "HEAD";
/** The prefix to use to identify tags */
public tagPrefix: string = "v";
/** Use branches instead of tags */
/** (Deprecated) Use branches instead of tags */
public useBranches: boolean = false;
/** If true, the branch will be used to select the maximum version. */
public versionFromBranch: string | boolean = false;
/** A string which, if present in a git commit, indicates that a change represents a major (breaking) change. Wrap with '/' to match using a regular expression. */
public majorPattern: string = "(MAJOR)";
public majorPattern: string = "/!:|BREAKING CHANGE:/";
/** A string which indicates the flags used by the `majorPattern` regular expression. */
public majorFlags: string = "";
/** A string which, if present in a git commit, indicates that a change represents a minor (feature) change. Wrap with '/' to match using a regular expression. */
public minorPattern: string = "(MINOR)";
public minorPattern: string = "/feat:/";
/** A string which indicates the flags used by the `minorPattern` regular expression. */
public minorFlags: string = "";
/** Pattern to use when formatting output version */
@ -26,4 +28,12 @@ export class ActionConfig {
public searchCommitBody: boolean = false;
/** The output method used to generate list of users, 'csv' or 'json'. Default is 'csv'. */
public userFormatType: string = "csv";
/** Prevents pre-v1.0.0 version from automatically incrementing the major version. If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the versionType output is unchanged. */
public enablePrereleaseMode: boolean = false;
/** If bump_each_commit is also set to true, setting this value will cause the version to increment only if the pattern specified is matched. */
public bumpEachCommitPatchPattern: string = "";
/** If enabled, diagnostic information will be added to the action output. */
public debug: boolean = false;
/** Diagnostics to replay */
public replay: string = '';
}

View file

@ -1,8 +1,15 @@
// Using require instead of import to support integration testing
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import { DebugManager } from './DebugManager';
export const cmd = async (command: string, ...args: any): Promise<string> => {
const debugManager = DebugManager.getInstance();
if (debugManager.isReplayMode()) {
return debugManager.replayCommand(command, args);
}
let output = '', errors = '';
const options = {
silent: true,
@ -14,15 +21,14 @@ export const cmd = async (command: string, ...args: any): Promise<string> => {
}
};
let caughtError: any = null;
try {
await exec.exec(command, args, options);
} catch (err) {
//core.info(`The command cd '${command} ${args.join(' ')}' failed: ${err}`);
caughtError = err;
}
if (errors !== '') {
//core.info(`stderr: ${errors}`);
}
debugManager.recordCommand(command, args, output, errors, caughtError);
return output;
};

View file

@ -1,4 +1,5 @@
import { CsvUserFormatter } from './formatting/CsvUserFormatter'
import { BranchVersioningTagFormatter } from './formatting/BranchVersioningTagFormatter'
import { DefaultTagFormatter } from './formatting/DefaultTagFormatter'
import { DefaultVersionFormatter } from './formatting/DefaultVersionFormatter'
import { JsonUserFormatter } from './formatting/JsonUserFormatter'
@ -15,11 +16,13 @@ import { DefaultLastReleaseResolver } from './providers/DefaultLastReleaseResolv
import { VersionClassifier } from './providers/VersionClassifier'
import { BumpAlwaysVersionClassifier } from './providers/BumpAlwaysVersionClassifier'
import { ActionConfig } from './ActionConfig';
import { DebugManager } from './DebugManager';
export class ConfigurationProvider {
constructor(config: ActionConfig) {
this.config = config;
DebugManager.getInstance().initializeConfig(config);
}
private config: ActionConfig;
@ -39,7 +42,12 @@ export class ConfigurationProvider {
public GetVersionFormatter(): VersionFormatter { return new DefaultVersionFormatter(this.config); }
public GetTagFormatter(): TagFormatter { return new DefaultTagFormatter(this.config); }
public GetTagFormatter(branchName: string): TagFormatter {
if (this.config.versionFromBranch) {
return new BranchVersioningTagFormatter(this.config, branchName);
}
return new DefaultTagFormatter(this.config);
}
public GetUserFormatter(): UserFormatter {
switch (this.config.userFormatType) {

115
src/DebugManager.ts Normal file
View file

@ -0,0 +1,115 @@
import { ActionConfig } from "./ActionConfig";
/** Utility class for managing debug mode and diagnostic information */
export class DebugManager {
private constructor() { }
private static instance: DebugManager;
/** Returns the singleton instance of the DebugManager */
public static getInstance(): DebugManager {
if (!DebugManager.instance) {
DebugManager.instance = new DebugManager();
}
return DebugManager.instance;
}
/** Clears the singleton instance of the DebugManager (used for testing) */
public static clearState() {
DebugManager.instance = new DebugManager();
}
private debugEnabled: boolean = false;
private replayMode: boolean = false;
private diagnosticInfo: DiagnosticInfo | null = null;
/** Returns true if debug mode is enabled */
public isDebugEnabled(): boolean {
return this.debugEnabled;
}
/** Returns true if replay mode is enabled */
public isReplayMode(): boolean {
return this.replayMode;
}
initializeConfig(config: ActionConfig) {
if (config.debug) {
this.setDebugEnabled(true);
} else if (config.replay.length > 0) {
this.replayFromDiagnostics(config.replay);
}
}
/** Enables or disables debug mode, also clears any existing diagnostics info */
public setDebugEnabled(enableDebug: boolean = true): void {
this.debugEnabled = enableDebug;
this.replayMode = false;
this.diagnosticInfo = new DiagnosticInfo();
};
/** Enables replay mode and loads the diagnostic information from the specified string */
public replayFromDiagnostics(diagnostics: string): void {
this.debugEnabled = false
this.replayMode = true;
this.diagnosticInfo = JSON.parse(diagnostics);
}
/** Returns a JSON string containing the diagnostic information for this run */
public getDebugOutput(emptyRepo: boolean = false): string {
return this.isDebugEnabled() ? JSON.stringify(this.diagnosticInfo) : '';
}
/** Records a command and its output for diagnostic purposes */
public recordCommand(command: string, args: any[], output: string, stderr: string, error: any): void {
if (this.isDebugEnabled()) {
this.diagnosticInfo?.recordCommand(command, args, output, stderr, error);
}
}
/** Replays the specified command and returns the output */
public replayCommand(command: string, args: any[]): string {
if (this.diagnosticInfo === null) {
throw new Error('No diagnostic information available for replay');
}
const commandResult = this.diagnosticInfo.commands.find(c => c.command === command && JSON.stringify(c.args) === JSON.stringify(args));
if (!commandResult) {
throw new Error(`No result found in diagnostic for command "${command}"`);
}
if (commandResult.error) {
throw commandResult.error;
}
if (commandResult.stderr) {
console.error(commandResult.stderr);
}
return commandResult.output;
}
}
/** Represents a CLI command result */
class CommandResult {
public command: string;
public args: any[];
public output: string;
public stderr: string;
public error: any;
public constructor(command: string, args: any[], output: string, stderr: string, error: any) {
this.command = command;
this.args = args;
this.output = output;
this.stderr = stderr;
this.error = error;
}
}
/** Represents the result of the commands executed for a run */
class DiagnosticInfo {
public commands: CommandResult[] = [];
public empty: boolean = false;
public recordCommand(command: string, args: any[], output: string, stderr: string, error: any): void {
this.commands.push(new CommandResult(command, args, output, stderr, error));
}
}

View file

@ -13,10 +13,12 @@ export class VersionResult {
* @param formattedVersion - The formatted semantic version
* @param versionTag - The string to be used as a Git tag
* @param changed - True if the version was changed, otherwise false
* @param isTagged - True if the commit had a tag that matched the `versionTag` format
* @param authors - Authors formatted according to the format mode (e.g. JSON, CSV, YAML, etc.)
* @param currentCommit - The current commit hash
* @param previousCommit - The previous commit hash
* @param previousVersion - the previous version
* @param previousVersion - The previous version
* @param debugOutput - Diagnostic information, if debug is enabled
*/
constructor(
public major: number,
@ -27,8 +29,10 @@ export class VersionResult {
public formattedVersion: string,
public versionTag: string,
public changed: boolean,
public isTagged: boolean,
public authors: string,
public currentCommit: string,
public previousCommit: string,
public previousVersion: string) { }
public previousVersion: string,
public debugOutput: string) { }
}

View file

@ -3,6 +3,7 @@ import { VersionResult } from './VersionResult';
import { VersionType } from './providers/VersionType';
import { UserInfo } from './providers/UserInfo';
import { VersionInformation } from './providers/VersionInformation';
import { DebugManager } from './DebugManager';
export async function runAction(configurationProvider: ConfigurationProvider): Promise<VersionResult> {
@ -11,11 +12,14 @@ export async function runAction(configurationProvider: ConfigurationProvider): P
const commitsProvider = configurationProvider.GetCommitsProvider();
const versionClassifier = configurationProvider.GetVersionClassifier();
const versionFormatter = configurationProvider.GetVersionFormatter();
const tagFormmater = configurationProvider.GetTagFormatter();
const tagFormatter = configurationProvider.GetTagFormatter(await currentCommitResolver.ResolveBranchNameAsync());
const userFormatter = configurationProvider.GetUserFormatter();
const debugManager = DebugManager.getInstance();
if (await currentCommitResolver.IsEmptyRepoAsync()) {
const versionInfo = new VersionInformation(0, 0, 0, 0, VersionType.None, [], false);
const versionInfo = new VersionInformation(0, 0, 0, 0, VersionType.None, [], false, false);
return new VersionResult(
versionInfo.major,
versionInfo.minor,
@ -23,25 +27,28 @@ export async function runAction(configurationProvider: ConfigurationProvider): P
versionInfo.increment,
versionInfo.type,
versionFormatter.Format(versionInfo),
tagFormmater.Format(versionInfo),
tagFormatter.Format(versionInfo),
versionInfo.changed,
versionInfo.isTagged,
userFormatter.Format('author', []),
'',
'',
'0.0.0'
tagFormatter.Parse(tagFormatter.Format(versionInfo)).join('.'),
debugManager.getDebugOutput(true)
);
}
const currentCommit = await currentCommitResolver.ResolveAsync();
const lastRelease = await lastReleaseResolver.ResolveAsync(currentCommit, tagFormmater);
const lastRelease = await lastReleaseResolver.ResolveAsync(currentCommit, tagFormatter);
const commitSet = await commitsProvider.GetCommitsAsync(lastRelease.hash, currentCommit);
const classification = await versionClassifier.ClassifyAsync(lastRelease, commitSet);
const { isTagged } = lastRelease;
const { major, minor, patch, increment, type, changed } = classification;
// At this point all necessary data has been pulled from the database, create
// version information to be used by the formatters
let versionInfo = new VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed);
let versionInfo = new VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed, isTagged);
// Group all the authors together, count the number of commits per author
const allAuthors = versionInfo.commits
@ -63,11 +70,13 @@ export async function runAction(configurationProvider: ConfigurationProvider): P
versionInfo.increment,
versionInfo.type,
versionFormatter.Format(versionInfo),
tagFormmater.Format(versionInfo),
tagFormatter.Format(versionInfo),
versionInfo.changed,
versionInfo.isTagged,
userFormatter.Format('author', authors),
currentCommit,
lastRelease.hash,
`${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`
`${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`,
debugManager.getDebugOutput()
);
}

View file

@ -0,0 +1,104 @@
import { ActionConfig } from '../ActionConfig';
import { DefaultTagFormatter } from './DefaultTagFormatter';
/** Default tag formatter which allows a prefix to be specified */
export class BranchVersioningTagFormatter extends DefaultTagFormatter {
private onVersionBranch: boolean;
private major: number;
private minor?: number;
private getRegex(pattern: string) {
if (/^\/.+\/[i]*$/.test(pattern)) {
const regexEnd = pattern.lastIndexOf('/');
const parsedFlags = pattern.slice(pattern.lastIndexOf('/') + 1);
return new RegExp(pattern.slice(1, regexEnd), parsedFlags);
}
return new RegExp(pattern);
}
constructor(config: ActionConfig, branchName: string) {
super(config);
const pattern = config.versionFromBranch === true ?
new RegExp("[0-9]+.[0-9]+$|[0-9]+$") :
this.getRegex(config.versionFromBranch as string);
const result = pattern.exec(branchName);
if (result === null) {
this.major = NaN;
this.onVersionBranch = false;
return;
}
let branchVersion: string;
switch (result?.length) {
case 1:
branchVersion = result[0];
break;
case 2:
branchVersion = result[1];
break;
default:
throw new Error(`Unable to parse version from branch named '${branchName}' using pattern '${pattern}'`);
}
this.onVersionBranch = true;
const versionValues = branchVersion.split('.');
if (versionValues.length > 2) {
throw new Error(`The version string '${branchVersion}' parsed from branch '${branchName}' is invalid. It must be in the format 'major.minor' or 'major'`);
}
this.major = parseInt(versionValues[0]);
if (isNaN(this.major)) {
throw new Error(`The major version '${versionValues[0]}' parsed from branch '${branchName}' is invalid. It must be a number.`);
}
if (versionValues.length > 1) {
this.minor = parseInt(versionValues[1]);
if (isNaN(this.minor)) {
throw new Error(`The minor version '${versionValues[1]}' parsed from branch '${branchName}' is invalid. It must be a number.`);
}
}
}
public override GetPattern(): string {
let pattern = super.GetPattern();
if (!this.onVersionBranch) {
return pattern;
}
if(this.minor === undefined) {
return pattern.replace('*[0-9].*[0-9].*[0-9]', `${this.major}.*[0-9].*[0-9]`);
}
return pattern.replace('*[0-9].*[0-9].*[0-9]', `${this.major}.${this.minor}.*[0-9]`);
}
override IsValid(tag: string): boolean {
if (!this.onVersionBranch) {
return super.IsValid(tag);
}
if (!super.IsValid(tag)) {
return false;
}
const parsed = super.Parse(tag);
if (parsed[0] !== this.major) {
return false;
}
if (this.minor !== undefined && parsed[1] !== this.minor) {
return false;
}
return true;
}
override Parse(tag: string): [major: number, minor: number, patch: number] {
if (!this.onVersionBranch) {
return super.Parse(tag);
}
const parsed = super.Parse(tag);
return [this.major, this.minor || parsed[1], parsed[2]];
}
}

View file

@ -36,6 +36,10 @@ export class DefaultTagFormatter implements TagFormatter {
public Parse(tag: string): [major: number, minor: number, patch: number] {
if(tag === '') {
return [0, 0, 0];
}
let tagParts = tag
.replace(this.tagPrefix, '<--!PREFIX!-->')
.replace(this.namespace, '<--!NAMESPACE!-->')
@ -62,11 +66,15 @@ export class DefaultTagFormatter implements TagFormatter {
};
public IsValid(tag: string): boolean {
const regexEscape = (literal: string) => literal.replace(/\W/g, '\\$&');
const tagPrefix = regexEscape(this.tagPrefix);
const namespaceSeperator = regexEscape(this.namespaceSeperator);
const namespace = regexEscape(this.namespace);
if (!!this.namespace) {
return new RegExp(`^${this.tagPrefix}[0-9]+.[0-9]+.[0-9]+${this.namespaceSeperator}${this.namespace}$`).test(tag);
return new RegExp(`^${tagPrefix}[0-9]+\.[0-9]+\.[0-9]+${namespaceSeperator}${namespace}$`).test(tag);
}
return new RegExp(`^${this.tagPrefix}[0-9]+.[0-9]+.[0-9]+$`).test(tag);
return new RegExp(`^${tagPrefix}[0-9]+\.[0-9]+\.[0-9]+$`).test(tag);
}
}

View file

@ -6,14 +6,16 @@ import { expect, test } from '@jest/globals'
import { runAction } from '../src/action';
import { ConfigurationProvider } from './ConfigurationProvider';
import { ActionConfig } from './ActionConfig';
import { DebugManager } from './DebugManager';
const windows = process.platform === "win32";
const timeout = 30000;
// Creates a randomly named git repository and returns a function to execute commands in it
const createTestRepo = (repoDefaultConfig?: Partial<ActionConfig>) => {
const repoDirectory = path.join(os.tmpdir(), `test${Math.random().toString(36).substring(2, 15)}`);
cp.execSync(`mkdir ${repoDirectory}`);
cp.execSync(`git init ${repoDirectory}`);
cp.execSync(`git init --initial-branch=master ${repoDirectory}`);
const run = (command: string) => {
return execute(repoDirectory, command);
@ -22,6 +24,7 @@ const createTestRepo = (repoDefaultConfig?: Partial<ActionConfig>) => {
// Configure up git user
run(`git config user.name "Test User"`);
run(`git config user.email "test@example.com"`);
run(`git config commit.gpgsign false`);
let i = 1;
@ -35,7 +38,11 @@ const createTestRepo = (repoDefaultConfig?: Partial<ActionConfig>) => {
run(`git add --all`);
run(`git commit -m "${msg}"`);
},
merge: (branch: string) => {
run(`git merge ${branch}`);
},
runAction: async (inputs?: Partial<ActionConfig>) => {
DebugManager.clearState();
let config = new ActionConfig();
config = { ...config, ...{ versionFormat: "${major}.${minor}.${patch}+${increment}" }, ...repoDefaultConfig, ...inputs };
process.chdir(repoDirectory);
@ -60,7 +67,7 @@ test('Empty repository version is correct', async () => {
var result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.0+0');
}, 15000);
}, timeout);
test('Repository with commits shows increment', async () => {
const repo = createTestRepo(); // 0.0.0+0
@ -70,7 +77,7 @@ test('Repository with commits shows increment', async () => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.1+1');
}, 15000);
}, timeout);
test('Repository show commit for checked out commit', async () => {
const repo = createTestRepo(); // 0.0.0+0
@ -83,7 +90,7 @@ test('Repository show commit for checked out commit', async () => {
repo.exec(`git checkout HEAD~1`); // 0.0.1+1
result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.1+0');
}, 15000);
}, timeout);
test('Tagging does not break version', async () => {
const repo = createTestRepo(); // 0.0.0+0
@ -95,7 +102,7 @@ test('Tagging does not break version', async () => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.1+2');
}, 15000);
}, timeout);
test('Tagging does not break version from previous tag', async () => {
@ -108,51 +115,51 @@ test('Tagging does not break version from previous tag', async () => {
repo.exec('git tag v0.0.2')
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.2+1');
}, 15000);
}, timeout);
test('Minor update bumps minor version and resets increment', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MINOR)'); // 0.1.0+0
repo.makeCommit('feat: Second Commit'); // 0.1.0+0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.1.0+0');
}, 15000);
}, timeout);
test('Major update bumps major version and resets increment', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
repo.makeCommit('feat!: Second Commit'); // 1.0.0+0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0+0');
}, 15000);
}, timeout);
test('Multiple major commits are idempotent', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
repo.makeCommit('Third Commit (MAJOR)'); // 1.0.0+1
repo.makeCommit('feat!: Second Commit'); // 1.0.0+0
repo.makeCommit('fix!: Third Commit'); // 1.0.0+1
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0+1');
}, 15000);
}, timeout);
test('Minor commits after a major commit are ignored', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
repo.makeCommit('Third Commit (MINOR)'); // 1.0.0+1
repo.makeCommit('feat!: Second Commit'); // 1.0.0+0
repo.makeCommit('feat: Third Commit'); // 1.0.0+1
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0+1');
}, 15000);
}, timeout);
test('Tags start new version', async () => {
const repo = createTestRepo(); // 0.0.0+0
@ -165,7 +172,7 @@ test('Tags start new version', async () => {
expect(result.formattedVersion).toBe('0.0.2+0');
}, 15000);
}, timeout);
test('Version pulled from last release branch', async () => {
const repo = createTestRepo(); // 0.0.0+0
@ -179,33 +186,7 @@ test('Version pulled from last release branch', async () => {
expect(result.formattedVersion).toBe('5.6.8+0');
}, 15000);
/* Removed for now
test('Tags on branches are used', async () => {
// This test checks that tags are counted correctly even if they are not on
// the main branch:
// master o--o--o--o <- expecting v0.0.2
// \
// release o--o <- taged v0.0.1
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.makeCommit('Third Commit'); // 0.1.1+2
repo.exec('git checkout -b release/0.0.1')
repo.makeCommit('Fourth Commit'); // 0.1.1+3
repo.exec('git tag v0.0.1');
repo.exec('git checkout master');
repo.makeCommit('Fifth Commit'); // 0.0.2.0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.2+0');
});
*/
}, timeout);
test('Merged tags do not affect version', async () => {
@ -229,11 +210,11 @@ test('Merged tags do not affect version', async () => {
repo.exec('git checkout master');
repo.makeCommit('Fifth Commit'); // 0.0.2.0
repo.exec('git tag v0.0.2');
repo.exec('git merge release/0.0.1');
repo.merge('release/0.0.1');
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.3+1');
}, 15000);
}, timeout);
test('Format input is respected', async () => {
const repo = createTestRepo({ versionFormat: 'M${major}m${minor}p${patch}i${increment}' }); // M0m0p0i0
@ -244,7 +225,7 @@ test('Format input is respected', async () => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe('M1m2p4i0');
}, 15000);
}, timeout);
test('Version prefixes are not required/can be empty', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -255,7 +236,7 @@ test('Version prefixes are not required/can be empty', async () => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.2+0');
}, 15000);
}, timeout);
test('Tag order comes from commit order, not tag create order', async () => {
const repo = createTestRepo(); // 0.0.0+0
@ -273,7 +254,7 @@ test('Tag order comes from commit order, not tag create order', async () => {
expect(result.formattedVersion).toBe('2.0.1+0');
}, 15000);
}, timeout);
test('Change detection is true by default', async () => {
@ -285,7 +266,7 @@ test('Change detection is true by default', async () => {
const result = await repo.runAction();
expect(result.changed).toBe(true);
}, 15000);
}, timeout);
test('Changes to monitored path is true when change is in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -297,7 +278,7 @@ test('Changes to monitored path is true when change is in path', async () => {
const result = await repo.runAction({ changePath: "project1" });
expect(result.changed).toBe(true);
}, 15000);
}, timeout);
test('Changes to monitored path is false when changes are not in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -310,7 +291,7 @@ test('Changes to monitored path is false when changes are not in path', async ()
const result = await repo.runAction({ changePath: "project1" });
expect(result.changed).toBe(false);
}, 15000);
}, timeout);
test('Changes can be detected without tags', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -321,7 +302,7 @@ test('Changes can be detected without tags', async () => {
const result = await repo.runAction({ changePath: "project1" });
expect(result.changed).toBe(true);
}, 15000);
}, timeout);
test('Changes to multiple monitored path is true when change is in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -334,7 +315,7 @@ test('Changes to multiple monitored path is true when change is in path', async
const result = await repo.runAction({ changePath: "project1 project2" });
expect(result.changed).toBe(true);
}, 15000);
}, timeout);
test('Changes to multiple monitored path is false when change is not in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -348,7 +329,7 @@ test('Changes to multiple monitored path is false when change is not in path', a
const result = await repo.runAction({ changePath: "project1 project2" });
expect(result.changed).toBe(false);
}, 15000);
}, timeout);
test('Namespace is tracked separately', async () => {
@ -365,7 +346,24 @@ test('Namespace is tracked separately', async () => {
expect(result.formattedVersion).toBe('0.0.2+1');
expect(subprojectResult.formattedVersion).toBe('0.1.1+0');
}, 15000);
}, timeout);
test('Version Namespace is tracked separately', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit('Second Commit'); // 0.0.2
repo.exec('git tag subproject0.1.0');
repo.makeCommit('Third Commit'); // 0.0.2 / 0.1.1
const result = await repo.runAction();
const subprojectResult = await repo.runAction({ tagPrefix: "subproject" });
expect(result.formattedVersion).toBe('0.0.2+1');
expect(subprojectResult.formattedVersion).toBe('0.1.1+0');
}, timeout);
test('Namespace allows dashes', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -381,7 +379,7 @@ test('Namespace allows dashes', async () => {
expect(result.formattedVersion).toBe('0.0.2+1');
expect(subprojectResult.formattedVersion).toBe('0.1.1+0');
}, 15000);
}, timeout);
test('Commits outside of path are not counted', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -393,7 +391,7 @@ test('Commits outside of path are not counted', async () => {
const result = await repo.runAction({ changePath: "project1" });
expect(result.formattedVersion).toBe('0.0.0+0');
}, 15000);
}, timeout);
test('Commits inside path are counted', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -409,7 +407,7 @@ test('Commits inside path are counted', async () => {
const result = await repo.runAction({ changePath: "project1" });
expect(result.formattedVersion).toBe('0.0.1+2');
}, 15000);
}, timeout);
test('Current tag is used', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
@ -422,7 +420,7 @@ test('Current tag is used', async () => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe('7.6.5+0');
}, 15000);
}, timeout);
test('Bump each commit works', async () => {
@ -435,15 +433,15 @@ test('Bump each commit works', async () => {
expect((await repo.runAction()).formattedVersion).toBe('0.0.2+0');
repo.makeCommit('Third Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.3+0');
repo.makeCommit('Fourth Commit (MINOR)');
repo.makeCommit('feat: Fourth Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.1.0+0');
repo.makeCommit('Fifth Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.1.1+0');
repo.makeCommit('Sixth Commit (MAJOR)');
repo.makeCommit('feat!: Sixth Commit');
expect((await repo.runAction()).formattedVersion).toBe('1.0.0+0');
repo.makeCommit('Seventh Commit');
expect((await repo.runAction()).formattedVersion).toBe('1.0.1+0');
}, 15000);
}, timeout);
test('Bump each commit picks up tags', async () => {
const repo = createTestRepo({ tagPrefix: '', bumpEachCommit: true }); // 0.0.0
@ -458,7 +456,7 @@ test('Bump each commit picks up tags', async () => {
expect((await repo.runAction()).formattedVersion).toBe('3.0.0+0');
repo.makeCommit('Fourth Commit');
expect((await repo.runAction()).formattedVersion).toBe('3.0.1+0');
}, 15000);
}, timeout);
test('Increment not affected by matching tag', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.1
@ -467,7 +465,7 @@ test('Increment not affected by matching tag', async () => {
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.exec('git tag 0.0.1');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1+1');
}, 15000);
}, timeout);
test('Regular expressions can be used as major tag', async () => {
const repo = createTestRepo({ tagPrefix: '', majorPattern: '/S[a-z]+Value/' }); // 0.0.1
@ -475,7 +473,7 @@ test('Regular expressions can be used as major tag', async () => {
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 1.0.0+0
expect((await repo.runAction()).formattedVersion).toBe('1.0.0+0');
}, 15000);
}, timeout);
test('Regular expressions can be used as minor tag', async () => {
const repo = createTestRepo({ tagPrefix: '', minorPattern: '/S[a-z]+Value/' }); // 0.0.1
@ -483,7 +481,7 @@ test('Regular expressions can be used as minor tag', async () => {
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 0.0.1+1
expect((await repo.runAction()).formattedVersion).toBe('0.1.0+0');
}, 15000);
}, timeout);
test('Regular expressions and flags can be used as major tag', async () => {
const repo = createTestRepo({ tagPrefix: '', majorPattern: '/s[a-z]+value/', majorFlags: 'i' }); // 0.0.1
@ -491,7 +489,7 @@ test('Regular expressions and flags can be used as major tag', async () => {
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 1.0.0+0
expect((await repo.runAction()).formattedVersion).toBe('1.0.0+0');
}, 15000);
}, timeout);
test('Regular expressions and flags can be used as minor tag', async () => {
const repo = createTestRepo({ tagPrefix: '', minorPattern: '/s[a-z]+value/', minorFlags: 'i' }); // 0.0.1
@ -499,7 +497,7 @@ test('Regular expressions and flags can be used as minor tag', async () => {
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 0.0.1+1
expect((await repo.runAction()).formattedVersion).toBe('0.1.0+0');
}, 15000);
}, timeout);
test('Tag prefix can include forward slash', async () => {
const repo = createTestRepo({ tagPrefix: 'version/' }); // 0.0.0
@ -509,7 +507,7 @@ test('Tag prefix can include forward slash', async () => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.2.3+0');
}, 15000);
}, timeout);
test('Tags immediately before merge are detected', async () => {
const repo = createTestRepo({ tagPrefix: 'v' }); // 0.0.0
@ -524,11 +522,11 @@ test('Tags immediately before merge are detected', async () => {
repo.exec('git tag v2.0.0');
repo.exec('git checkout master');
repo.makeCommit('Commit 5');
repo.exec('git merge feature/branch');
repo.merge('feature/branch');
const result = await repo.runAction();
expect(result.versionTag).toBe('v2.0.1');
}, 15000);
}, timeout);
test('Correct tag is detected on merged branches', async () => {
const repo = createTestRepo({ tagPrefix: 'v' }); // 0.0.0
@ -543,11 +541,11 @@ test('Correct tag is detected on merged branches', async () => {
repo.makeCommit('Commit 4');
repo.exec('git checkout master');
repo.makeCommit('Commit 5');
repo.exec('git merge feature/branch');
repo.merge('feature/branch');
const result = await repo.runAction();
expect(result.versionTag).toBe('v2.0.1');
}, 15000);
}, timeout);
test('Correct tag is detected on multiple branches', async () => {
const repo = createTestRepo({ tagPrefix: 'v' }); // 0.0.0
@ -558,18 +556,18 @@ test('Correct tag is detected on multiple branches', async () => {
repo.exec('git tag v1.0.0');
repo.makeCommit('Commit 2');
repo.exec('git checkout master');
repo.exec('git merge feature/branch1');
repo.merge('feature/branch1');
repo.exec('git checkout -b feature/branch2');
repo.makeCommit('Commit 3');
repo.exec('git tag v2.0.0');
repo.makeCommit('Commit 4');
repo.exec('git checkout master');
repo.makeCommit('Commit 5');
repo.exec('git merge feature/branch2');
repo.merge('feature/branch2');
const result = await repo.runAction();
expect(result.versionTag).toBe('v2.0.1');
}, 15000);
}, timeout);
test('Correct tag is detected when versions pass 10s place', async () => {
const repo = createTestRepo({ tagPrefix: 'v' }); // 0.0.0
@ -580,18 +578,18 @@ test('Correct tag is detected when versions pass 10s place', async () => {
repo.exec('git tag v10.15.0');
repo.makeCommit('Commit 2');
repo.exec('git checkout master');
repo.exec('git merge feature/branch1');
repo.merge('feature/branch1');
repo.exec('git checkout -b feature/branch2');
repo.makeCommit('Commit 3');
repo.exec('git tag v10.7.0');
repo.makeCommit('Commit 4');
repo.exec('git checkout master');
repo.makeCommit('Commit 5');
repo.exec('git merge feature/branch2');
repo.merge('feature/branch2');
const result = await repo.runAction();
expect(result.versionTag).toBe('v10.15.1');
}, 15000);
}, timeout);
test('Tags on unmerged branches are not considered', async () => {
const repo = createTestRepo({ tagPrefix: 'v' }); // 0.0.0
@ -609,7 +607,7 @@ test('Tags on unmerged branches are not considered', async () => {
const result = await repo.runAction();
expect(result.versionTag).toBe('v1.0.1');
}, 15000);
}, timeout);
test('Can use branches instead of tags', async () => {
const repo = createTestRepo({ tagPrefix: 'release/', useBranches: true }); // 0.0.0
@ -619,25 +617,41 @@ test('Can use branches instead of tags', async () => {
repo.exec('git checkout -b release/1.0.0');
repo.makeCommit('Commit 2');
repo.exec('git checkout master');
repo.exec('git merge release/1.0.0');
repo.merge('release/1.0.0');
repo.makeCommit('Commit 3');
const result = await repo.runAction();
expect(result.versionTag).toBe('release/1.0.1');
}, 15000);
}, timeout);
test('Correct previous version is returned', async () => {
const repo = createTestRepo();
repo.makeCommit('Initial Commit');
repo.exec('git tag v2.0.1')
repo.exec('git tag v2.0.1');
repo.makeCommit(`Second Commit`);
repo.makeCommit(`Third Commit`);
const result = await repo.runAction();
expect(result.formattedVersion).toBe('2.0.2+1');
expect(result.previousVersion).toBe('2.0.1');
}, 15000);
}, timeout);
test('Correct previous version is returned when multiple tags are present', async () => {
const repo = createTestRepo();
repo.makeCommit('Initial Commit');
repo.exec('git tag v1.0.1');
repo.makeCommit(`Second Commit`);
repo.exec('git tag v2.0.1');
repo.makeCommit(`Third Commit`);
repo.exec('git tag v3.0.1');
repo.makeCommit(`Fourth Commit`);
const result = await repo.runAction();
expect(result.formattedVersion).toBe('3.0.2+0');
expect(result.previousVersion).toBe('3.0.1');
}, timeout);
test('Correct previous version is returned when using branches', async () => {
const repo = createTestRepo({ tagPrefix: 'release/', useBranches: true });
@ -646,13 +660,13 @@ test('Correct previous version is returned when using branches', async () => {
repo.exec('git checkout -b release/2.0.1');
repo.makeCommit(`Second Commit`);
repo.exec('git checkout master');
repo.exec('git merge release/2.0.1');
repo.merge('release/2.0.1');
repo.makeCommit(`Third Commit`);
const result = await repo.runAction();
expect(result.previousVersion).toBe('2.0.1');
expect(result.formattedVersion).toBe('2.0.2+0');
}, 15000);
}, timeout);
test('Correct previous version is returned when directly tagged', async () => {
const repo = createTestRepo();
@ -666,24 +680,39 @@ test('Correct previous version is returned when directly tagged', async () => {
expect(result.previousVersion).toBe('2.0.1');
expect(result.formattedVersion).toBe('2.0.2+1');
}, 15000);
}, timeout);
test('Correct previous version is returned when directly tagged with multiple previous tags', async () => {
const repo = createTestRepo();
repo.makeCommit('Initial Commit');
repo.exec('git tag v2.0.1')
repo.makeCommit(`Second Commit`);
repo.exec('git tag v2.0.2')
repo.makeCommit(`Third Commit`);
repo.exec('git tag v2.0.3')
const result = await repo.runAction();
expect(result.previousVersion).toBe('2.0.2');
expect(result.formattedVersion).toBe('2.0.3+0');
}, timeout);
test('Prerelease suffixes are ignored', async () => {
const repo = createTestRepo();
repo.makeCommit('Initial Commit (MAJOR)');
repo.makeCommit('feat!: Initial Commit');
repo.makeCommit(`Second Commit`);
repo.exec('git tag v1.0.0-alpha.1')
repo.makeCommit(`Third Commit`);
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0+2');
}, 15000);
}, timeout);
test('Prerelease suffixes are ignored when namespaces are set', async () => {
const repo = createTestRepo({ namespace: 'test' });
repo.makeCommit('Initial Commit (MAJOR)');
repo.makeCommit('feat!: Initial Commit');
repo.exec('git tag v1.0.0-test')
repo.makeCommit(`Second Commit`);
repo.exec('git tag v1.0.1-test-alpha.1')
@ -691,4 +720,427 @@ test('Prerelease suffixes are ignored when namespaces are set', async () => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.1+1');
}, 15000);
}, timeout);
test('Namespace can contains a slash', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit('Second Commit'); // 0.0.2
repo.exec('git tag 0.1.0-sub/project');
repo.makeCommit('Third Commit'); // 0.0.2 / 0.1.1
const result = await repo.runAction();
const subprojectResult = await repo.runAction({ namespace: "sub/project" });
expect(result.formattedVersion).toBe('0.0.2+1');
expect(subprojectResult.formattedVersion).toBe('0.1.1+0');
}, timeout);
test('Namespace can contains a dot', async () => {
const repo = createTestRepo({ tagPrefix: '' });
repo.makeCommit('Initial Commit');
repo.exec('git tag 0.0.1');
repo.makeCommit('Second Commit');
repo.exec('git tag 0.1.0-sub.project');
repo.makeCommit('Third Commit');
repo.exec('git tag 0.2.0-sub/project');
repo.makeCommit('fourth Commit');
const result = await repo.runAction();
const subprojectResult = await repo.runAction({ namespace: "sub.project" });
expect(result.formattedVersion).toBe('0.0.2+2');
expect(subprojectResult.formattedVersion).toBe('0.1.1+1');
}, timeout);
test('Patch increments only once', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}" });
repo.makeCommit('Initial Commit');
repo.exec('git tag 0.0.1');
const firstResult = await repo.runAction();
repo.makeCommit('Second Commit');
const secondResult = await repo.runAction();
repo.makeCommit('Third Commit');
const thirdResult = await repo.runAction();
repo.makeCommit('fourth Commit');
const fourthResult = await repo.runAction();
expect(firstResult.formattedVersion).toBe('0.0.1');
expect(secondResult.formattedVersion).toBe('0.0.2');
expect(thirdResult.formattedVersion).toBe('0.0.2');
expect(fourthResult.formattedVersion).toBe('0.0.2');
}, timeout);
test('Patch increments each time when bump each commit is set', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}", bumpEachCommit: true });
repo.makeCommit('Initial Commit');
repo.exec('git tag 0.0.1');
const firstResult = await repo.runAction();
repo.makeCommit('Second Commit');
const secondResult = await repo.runAction();
repo.makeCommit('Third Commit');
const thirdResult = await repo.runAction();
repo.makeCommit('fourth Commit');
const fourthResult = await repo.runAction();
expect(firstResult.formattedVersion).toBe('0.0.1');
expect(secondResult.formattedVersion).toBe('0.0.2');
expect(thirdResult.formattedVersion).toBe('0.0.3');
expect(fourthResult.formattedVersion).toBe('0.0.4');
}, timeout);
test('Patch does not increment on bump each commit if a patch pattern is set', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}+${increment}", bumpEachCommit: true, bumpEachCommitPatchPattern: '(PATCH)' });
repo.makeCommit('Initial Commit');
repo.exec('git tag 0.0.1');
const firstResult = await repo.runAction();
repo.makeCommit('Second Commit');
const secondResult = await repo.runAction();
repo.makeCommit('Third Commit');
const thirdResult = await repo.runAction();
repo.makeCommit('fourth Commit');
const fourthResult = await repo.runAction();
expect(firstResult.formattedVersion).toBe('0.0.1+0');
expect(secondResult.formattedVersion).toBe('0.0.1+1');
expect(thirdResult.formattedVersion).toBe('0.0.1+2');
expect(fourthResult.formattedVersion).toBe('0.0.1+3');
}, timeout);
test('Patch pattern increment is correct on empty repo', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}+${increment}", bumpEachCommit: true, bumpEachCommitPatchPattern: '(PATCH)' });
const initialResult = await repo.runAction();
repo.makeCommit('Initial Commit');
const firstResult = await repo.runAction();
repo.makeCommit('Second Commit');
const secondResult = await repo.runAction();
repo.makeCommit('Third Commit');
const thirdResult = await repo.runAction();
repo.makeCommit('fourth Commit');
const fourthResult = await repo.runAction();
expect(initialResult.formattedVersion).toBe('0.0.0+0');
expect(firstResult.formattedVersion).toBe('0.0.1+0');
expect(secondResult.formattedVersion).toBe('0.0.1+1');
expect(thirdResult.formattedVersion).toBe('0.0.1+2');
expect(fourthResult.formattedVersion).toBe('0.0.1+3');
}, timeout);
test('Patch pattern increment is correct/matches non-bumped on empty repo', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}+${increment}", bumpEachCommit: true, bumpEachCommitPatchPattern: '(PATCH)' });
repo.makeCommit('Initial Commit');
const firstResult = await repo.runAction();
repo.makeCommit('Second Commit');
const secondResult = await repo.runAction();
repo.makeCommit('Third Commit');
const thirdResult = await repo.runAction();
repo.makeCommit('fourth Commit');
const fourthResult = await repo.runAction();
expect(firstResult.formattedVersion).toBe('0.0.1+0');
expect(secondResult.formattedVersion).toBe('0.0.1+1');
expect(thirdResult.formattedVersion).toBe('0.0.1+2');
expect(fourthResult.formattedVersion).toBe('0.0.1+3');
}, timeout);
test('Patch pattern applied when present using tag as initial version', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}+${increment}", bumpEachCommit: true, bumpEachCommitPatchPattern: '(PATCH)' });
repo.makeCommit('Initial Commit');
repo.exec('git tag 1.0.1');
const firstResult = await repo.runAction(); // 1.0.1+0
repo.makeCommit('Second Commit');
const secondResult = await repo.runAction(); // 1.0.1+1
repo.makeCommit('Third Commit (PATCH)');
const thirdResult = await repo.runAction(); // 1.0.2+0
repo.makeCommit('fourth Commit');
const fourthResult = await repo.runAction(); // 1.0.2+1
expect(firstResult.formattedVersion).toBe('1.0.1+0');
expect(secondResult.formattedVersion).toBe('1.0.1+1');
expect(thirdResult.formattedVersion).toBe('1.0.2+0');
expect(fourthResult.formattedVersion).toBe('1.0.2+1');
}, timeout);
test('Current commit is provided', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}" });
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
repo.makeCommit('fourth Commit');
repo.exec('git tag 0.0.1');
const result = await repo.runAction();
expect(result.currentCommit).toBeTruthy();
}, timeout);
test('Prerelease tags are ignored on current commit', async () => {
const repo = createTestRepo({
minorPattern: '/.*/'
});
let i = 0;
const validate = async (version: string, changed: boolean = true) => {
const result = await repo.runAction();
expect(result.formattedVersion).toBe(version);
expect(result.changed).toBe(changed);
}
await validate('0.0.0+0', false);
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+0');
repo.exec('git tag v0.0.0');
await validate('0.0.0+0', false);
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+0');
repo.exec('git tag v1.0.0-rc1');
await validate('0.1.0+0');
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+1');
repo.exec('git tag v1.0.0-rc2');
await validate('0.1.0+1');
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+2');
repo.exec('git tag v1.0.0-rc3');
await validate('0.1.0+2');
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+3');
repo.exec('git tag v1.0.0');
await validate('1.0.0+0', false);
repo.makeCommit(`Commit ${i++}`);
await validate('1.1.0+0');
repo.exec('git tag v1.1.0-rc2');
await validate('1.1.0+0');
repo.makeCommit(`Commit ${i++}`);
await validate('1.1.0+1');
repo.exec('git tag v1.1.0-rc4');
await validate('1.1.0+1');
repo.makeCommit(`Commit ${i++}`);
await validate('1.1.0+2');
repo.exec('git tag v1.1.0-rc8');
await validate('1.1.0+2');
repo.makeCommit(`Commit ${i++}`);
await validate('1.1.0+3');
repo.exec('git tag v1.1.0-rc9');
await validate('1.1.0+3');
repo.makeCommit(`Commit ${i++}`);
await validate('1.1.0+4');
}, timeout);
test('Pre-release mode does not update major version if major version is 0', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}", enablePrereleaseMode: true });
repo.makeCommit('Initial Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1');
repo.makeCommit('feat: Second Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1');
repo.makeCommit('feat!: Third Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.1.0');
repo.exec('git tag 0.1.0');
repo.makeCommit('feat!: Fourth Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.2.0');
}, timeout);
test('Pre-release mode updates major version if major version is not 0', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}", enablePrereleaseMode: true });
repo.makeCommit('Initial Commit');
repo.exec('git tag 1.0.0');
repo.makeCommit('Second Commit');
expect((await repo.runAction()).formattedVersion).toBe('1.0.1');
repo.makeCommit('feat: Third Commit');
expect((await repo.runAction()).formattedVersion).toBe('1.1.0');
repo.makeCommit('feat!: Fourth Commit');
expect((await repo.runAction()).formattedVersion).toBe('2.0.0');
repo.exec('git tag 2.0.0');
repo.makeCommit('feat!: Fifth Commit');
expect((await repo.runAction()).formattedVersion).toBe('3.0.0');
}, timeout);
test('Tagged commit is flagged as release', async () => {
const repo = createTestRepo({ tagPrefix: 'v', versionFormat: "${major}.${minor}.${patch}-prerelease.${increment}", enablePrereleaseMode: true });
repo.makeCommit('Initial Commit');
repo.exec('git tag v1.0.0');
var result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0-prerelease.0');
expect(result.isTagged).toBe(true);
repo.makeCommit('Second Commit');
result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.1-prerelease.0')
expect(result.isTagged).toBe(false);
repo.makeCommit('feat: Third Commit');
result = await repo.runAction();
expect(result.formattedVersion).toBe('1.1.0-prerelease.0');
expect(result.isTagged).toBe(false);
repo.makeCommit('feat: Fourth Commit');
repo.exec('git tag v1.1.0')
result = await repo.runAction();
expect(result.formattedVersion).toBe('1.1.0-prerelease.1');
expect(result.isTagged).toBe(true);
}, timeout);
test('Pre-release mode with bump each commit does not update major version if major version is 0', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}", enablePrereleaseMode: true, bumpEachCommit: true });
repo.makeCommit('Initial Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1');
repo.makeCommit('feat: Second Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.1.0');
repo.makeCommit('feat!: Third Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.2.0');
repo.exec('git tag 0.1.0');
repo.makeCommit('feat!: Fourth Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.2.0');
}, timeout);
test('Pre-release mode with bump each commit does not update major version if major version is 0, respecting patch pattern', async () => {
const repo = createTestRepo({
tagPrefix: '',
versionFormat: "${major}.${minor}.${patch}.${increment}",
enablePrereleaseMode: true,
bumpEachCommit: true,
bumpEachCommitPatchPattern: '(PATCH)',
});
repo.makeCommit('Initial Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1.0');
repo.makeCommit('Second Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1.1');
repo.makeCommit('Third Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1.2');
repo.makeCommit('feat!: Fourth Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.1.0.0');
}, timeout);
test('Highest tag is chosen when multiple tags are present', async () => {
const repo = createTestRepo();
repo.makeCommit('Initial Commit');
repo.exec('git tag v2.0.0');
repo.exec('git tag v1.0.0');
repo.exec('git tag v1.5.0');
var result = await repo.runAction();
expect(result.formattedVersion).toBe('2.0.0+0');
repo.exec('git tag v3.0.0');
repo.exec('git tag v2.1.0');
result = await repo.runAction();
expect(result.formattedVersion).toBe('3.0.0+0');
repo.makeCommit('Additional Commit');
result = await repo.runAction();
expect(result.formattedVersion).toBe('3.0.1+0');
}, timeout);
test('Debug records and replays expected data', async () => {
const repo = createTestRepo();
repo.makeCommit('Initial Commit');
repo.exec('git tag v3.0.0');
repo.makeCommit(`Second Commit`);
const result = await repo.runAction({ debug: true });
repo.makeCommit(`Breaking Commit`);
repo.exec('git tag v9.9.9');
var replayResult = await repo.runAction({ replay: result.debugOutput });
expect(replayResult.major).toBe(result.major);
expect(replayResult.minor).toBe(result.minor);
expect(replayResult.patch).toBe(result.patch);
expect(replayResult.increment).toBe(result.increment);
}, timeout);
test('Version branch using major version ignores other tags', async () => {
const repo = createTestRepo({ versionFromBranch: true });
repo.makeCommit('Initial Commit');
repo.makeCommit(`Second Commit`);
repo.exec("git checkout -b release/v3");
repo.exec('git tag v3.0.0');
repo.makeCommit(`Third Commit`);
repo.exec('git tag v4.0.0');
repo.makeCommit(`Fourth Commit`);
repo.makeCommit(`Fifth Commit`);
const result = await repo.runAction();
expect(result.formattedVersion).toBe('3.0.1+2');
}, timeout);
test('Versioning from branch always takes version from branch name even without tags', async () => {
const repo = createTestRepo({ versionFromBranch: true });
repo.makeCommit('Initial Commit');
repo.makeCommit(`Second Commit`);
repo.exec("git checkout -b release/v3.2");
repo.makeCommit(`Third Commit`);
repo.makeCommit(`Fourth Commit`);
repo.makeCommit(`Fifth Commit`);
const result = await repo.runAction();
expect(result.formattedVersion).toBe('3.2.1+4');
}, timeout);
test('Prerelease mode does not increment to 1.x.x', async () => {
const repo = createTestRepo({ tagPrefix: 'v', versionFormat: "${major}.${minor}.${patch}-prerelease.${increment}", enablePrereleaseMode: true });
repo.makeCommit('Initial Commit');
repo.exec('git tag v1.0.0');
var result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0-prerelease.0');
expect(result.isTagged).toBe(true);
repo.makeCommit('Second Commit');
result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.1-prerelease.0')
expect(result.isTagged).toBe(false);
repo.makeCommit('feat: Third Commit');
result = await repo.runAction();
expect(result.formattedVersion).toBe('1.1.0-prerelease.0');
expect(result.isTagged).toBe(false);
repo.makeCommit('feat: Fourth Commit');
repo.exec('git tag v1.1.0')
result = await repo.runAction();
expect(result.formattedVersion).toBe('1.1.0-prerelease.1');
expect(result.isTagged).toBe(true);
}, timeout);

View file

@ -6,7 +6,7 @@ import * as core from '@actions/core';
import { VersionType } from './providers/VersionType';
function setOutput(versionResult: VersionResult) {
const { major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, authors, currentCommit, previousCommit, previousVersion } = versionResult;
const { major, minor, patch, increment, versionType, formattedVersion, versionTag, changed, isTagged, authors, currentCommit, previousCommit, previousVersion, debugOutput } = versionResult;
const repository = process.env.GITHUB_REPOSITORY;
@ -26,19 +26,41 @@ function setOutput(versionResult: VersionResult) {
core.setOutput("increment", increment.toString());
core.setOutput("version_type", VersionType[versionType].toLowerCase());
core.setOutput("changed", changed.toString());
core.setOutput("is_tagged", isTagged.toString());
core.setOutput("version_tag", versionTag);
core.setOutput("authors", authors);
core.setOutput("lastVersion", authors);
core.setOutput("previous_commit", previousCommit);
core.setOutput("previous_version", previousVersion);
core.setOutput("current_commit", currentCommit);
core.setOutput("debug_output", debugOutput);
}
export async function run() {
function toBool(value: string): boolean {
if (!value || value.toLowerCase() === 'false') {
return false;
} else if (value.toLowerCase() === 'true') {
return true;
}
return false;
}
function toStringOrBool(value: string): string | boolean {
if (!value || value === 'false') {
return false;
}
if (value === 'true') {
return true;
}
return value;
}
const config: ActionConfig = {
branch: core.getInput('branch'),
tagPrefix: core.getInput('tag_prefix'),
useBranches: core.getInput('use_branches') === 'true',
useBranches: toBool(core.getInput('use_branches')),
versionFromBranch: toStringOrBool(core.getInput('version_from_branch')),
majorPattern: core.getInput('major_pattern'),
minorPattern: core.getInput('minor_pattern'),
majorFlags: core.getInput('major_regexp_flags'),
@ -46,11 +68,19 @@ export async function run() {
versionFormat: core.getInput('version_format'),
changePath: core.getInput('change_path'),
namespace: core.getInput('namespace'),
bumpEachCommit: core.getInput('bump_each_commit') === 'true',
searchCommitBody: core.getInput('search_commit_body') === 'true',
userFormatType: core.getInput('user_format_type')
bumpEachCommit: toBool(core.getInput('bump_each_commit')),
searchCommitBody: toBool(core.getInput('search_commit_body')),
userFormatType: core.getInput('user_format_type'),
enablePrereleaseMode: toBool(core.getInput('enable_prerelease_mode')),
bumpEachCommitPatchPattern: core.getInput('bump_each_commit_patch_pattern'),
debug: toBool(core.getInput('debug')),
replay: ''
};
if (config.useBranches) {
core.warning(`The 'use_branches' input option is deprecated, please see the documentation for more information on how to use branches`);
}
if (config.versionFormat === '' && core.getInput('format') !== '') {
core.warning(`The 'format' input is deprecated, use 'versionFormat' instead`);
config.versionFormat = core.getInput('format');

View file

@ -8,9 +8,16 @@ import { VersionType } from "./VersionType";
export class BumpAlwaysVersionClassifier extends DefaultVersionClassifier {
protected patchPattern: (commit: CommitInfo) => boolean;
protected enablePrereleaseMode: boolean;
constructor(config: ActionConfig) {
super(config);
// Placeholder for consistency
this.enablePrereleaseMode = config.enablePrereleaseMode;
this.patchPattern = !config.bumpEachCommitPatchPattern ?
_ => true :
this.parsePattern(config.bumpEachCommitPatchPattern, "", config.searchCommitBody);
}
public override async ClassifyAsync(lastRelease: ReleaseInformation, commitSet: CommitInfoSet): Promise<VersionClassification> {
@ -21,27 +28,66 @@ export class BumpAlwaysVersionClassifier extends DefaultVersionClassifier {
let { major, minor, patch } = lastRelease;
let type = VersionType.None;
let increment = 0;
if (commitSet.commits.length === 0) {
return new VersionClassification(type, 0, false, major, minor, patch);
}
for (let commit of commitSet.commits.reverse()) {
if (this.majorPattern(commit)) {
type = VersionType.Major;
} else if (this.minorPattern(commit)) {
type = VersionType.Minor;
} else if (this.patchPattern(commit) ||
(major === 0 && minor === 0 && patch === 0 && commitSet.commits.length > 0)) {
type = VersionType.Patch;
} else {
type = VersionType.None;
}
if (this.enablePrereleaseMode && major === 0) {
switch (type) {
case VersionType.Major:
case VersionType.Minor:
minor += 1;
patch = 0;
increment = 0;
break;
case VersionType.Patch:
patch += 1;
increment = 0;
break;
default:
increment++;
break;
}
} else {
switch (type) {
case VersionType.Major:
major += 1;
minor = 0;
patch = 0;
type = VersionType.Major;
} else if (this.minorPattern(commit)) {
increment = 0;
break;
case VersionType.Minor:
minor += 1;
patch = 0;
type = VersionType.Minor;
} else {
break;
case VersionType.Patch:
patch += 1;
type = VersionType.Patch;
increment = 0;
break;
default:
increment++;
break;
}
}
return new VersionClassification(type, 0, true, major, minor, patch);
}
return new VersionClassification(type, increment, true, major, minor, patch);
}
}

View file

@ -10,4 +10,10 @@ export interface CurrentCommitResolver {
* @returns True if the repository is empty
*/
IsEmptyRepoAsync(): Promise<boolean>;
/**
* Returns the current branch
* @returns The current branch
*/
ResolveBranchNameAsync(): Promise<string>;
}

View file

@ -22,4 +22,12 @@ export class DefaultCurrentCommitResolver implements CurrentCommitResolver {
let lastCommitAll = (await cmd('git', 'rev-list', '-n1', '--all')).trim();
return lastCommitAll === '';
}
public async ResolveBranchNameAsync(): Promise<string> {
const branchName =
this.branch == 'HEAD' ? await cmd('git', 'rev-parse', '--abbrev-ref', 'HEAD') : this.branch;
return branchName.trim();
}
}

View file

@ -21,26 +21,31 @@ export class DefaultLastReleaseResolver implements LastReleaseResolver {
let currentTag = (await cmd(
`git tag --points-at ${current} ${releasePattern}`
)).trim();
currentTag = tagFormatter.IsValid(currentTag) ? currentTag : '';
const isTagged = currentTag !== '';
const [currentMajor, currentMinor, currentPatch] = !!currentTag ? tagFormatter.Parse(currentTag) : [null, null, null];
let tagsCount = 0;
let tag = '';
try {
const refPrefixPattern = this.useBranches ? 'refs/heads/' : 'refs/tags/';
if (!!currentTag) {
// If we already have the current branch tagged, we are checking for the previous one
// so that we will have an accurate increment (assuming the new tag is the expected one)
const command = `git for-each-ref --count=2 --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
tag = await cmd(command);
tag = tag
.split('\n')
.reverse()
const command = `git for-each-ref --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
const tags = (await cmd(command)).split('\n')
tagsCount = tags.length;
tag = tags
.find(t => tagFormatter.IsValid(t) && t !== currentTag) || '';
} else {
const command = `git for-each-ref --sort=-v:*refname --format=%(refname:short) --merged=${current} ${refPrefixPattern}${releasePattern}`;
let tags = await cmd(command);
const tags = (await cmd(command)).split('\n')
tagsCount = tags.length;
tag = tags
.split('\n')
.find(t => tagFormatter.IsValid(t)) || '';
}
@ -57,16 +62,21 @@ export class DefaultLastReleaseResolver implements LastReleaseResolver {
// practice this isn't likely to happen, but it keeps the test output from being
// polluted with a bunch of warnings.
if (tagsCount > 0) {
core.warning(`None of the ${tagsCount} tags(s) found were valid version tags for the present configuration. If this is unexpected, check to ensure that the configuration is correct and matches the tag format you are using.`);
} else {
core.warning('No tags are present for this repository. If this is unexpected, check to ensure that tags have been pulled from the remote.');
}
}
const [major, minor, patch] = tagFormatter.Parse('');
// no release tags yet, use the initial commit as the root
return new ReleaseInformation(0, 0, 0, '', currentMajor, currentMinor, currentPatch);
return new ReleaseInformation(major, minor, patch, '', currentMajor, currentMinor, currentPatch, isTagged);
}
// parse the version tag
const [major, minor, patch] = tagFormatter.Parse(tag);
const root = await cmd('git', `merge-base`, tag, current);
return new ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch);
return new ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch, isTagged);
}
}

View file

@ -9,7 +9,7 @@ test('Regular expressions can be used as minor tag direct', async () => {
const classifier = new DefaultVersionClassifier({ ...new ActionConfig(), ...{ tagPrefix: '', minorPattern: '/S[a-z]+Value/' }});
const releaseInfo =new ReleaseInformation(0,0,1,"",null,null,null);
const releaseInfo =new ReleaseInformation(0,0,1,"",null,null,null,false);
const commitSet = new CommitInfoSet(false, [
new CommitInfo("", "Second Commit SomeValue", "", "","", new Date(), "", "", new Date(), []),
new CommitInfo("", "Initial Commit", "", "","", new Date(), "", "", new Date(), []),

View file

@ -10,16 +10,20 @@ export class DefaultVersionClassifier implements VersionClassifier {
protected majorPattern: (commit: CommitInfo) => boolean;
protected minorPattern: (commit: CommitInfo) => boolean;
protected enablePrereleaseMode: boolean;
constructor(config: ActionConfig) {
const searchBody = config.searchCommitBody;
this.majorPattern = this.parsePattern(config.majorPattern, config.majorFlags, searchBody);
this.minorPattern = this.parsePattern(config.minorPattern, config.minorFlags, searchBody);
this.enablePrereleaseMode = config.enablePrereleaseMode;
}
protected parsePattern(pattern: string, flags: string, searchBody: boolean): (pattern: CommitInfo) => boolean {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
var regex = new RegExp(pattern.slice(1, -1), flags);
if (/^\/.+\/[i]*$/.test(pattern)) {
const regexEnd = pattern.lastIndexOf('/');
const parsedFlags = pattern.slice(pattern.lastIndexOf('/') + 1);
const regex = new RegExp(pattern.slice(1, regexEnd), parsedFlags || flags);
return searchBody ?
(commit: CommitInfo) => regex.test(commit.subject) || regex.test(commit.body) :
(commit: CommitInfo) => regex.test(commit.subject);
@ -33,6 +37,20 @@ export class DefaultVersionClassifier implements VersionClassifier {
protected getNextVersion(current: ReleaseInformation, type: VersionType): ({ major: number, minor: number, patch: number }) {
if (this.enablePrereleaseMode && current.major === 0) {
switch (type) {
case VersionType.Major:
return { major: current.major, minor: current.minor + 1, patch: 0 };
case VersionType.Minor:
case VersionType.Patch:
return { major: current.major, minor: current.minor, patch: current.patch + 1 };
case VersionType.None:
return { major: current.major, minor: current.minor, patch: current.patch };
default:
throw new Error(`Unknown change type: ${type}`);
}
}
switch (type) {
case VersionType.Major:
return { major: current.major + 1, minor: 0, patch: 0 };
@ -89,8 +107,8 @@ export class DefaultVersionClassifier implements VersionClassifier {
// - commit 4 - v2.0.1+0
const versionsMatch = lastRelease.currentMajor === major && lastRelease.currentMinor === minor && lastRelease.currentPatch === patch;
const currentIncremement = versionsMatch ? increment : 0;
return new VersionClassification(VersionType.None, currentIncremement, false, <number>lastRelease.currentMajor, <number>lastRelease.currentMinor, <number>lastRelease.currentPatch);
const currentIncrement = versionsMatch ? increment : 0;
return new VersionClassification(VersionType.None, currentIncrement, false, <number>lastRelease.currentMajor, <number>lastRelease.currentMinor, <number>lastRelease.currentPatch);
}

View file

@ -9,6 +9,7 @@ export class ReleaseInformation {
* @param currentMajor - the major version number from the current commit
* @param currentMinor - the minor version number from the current commit
* @param currentPatch - the patch version number from the current commit
* @param isTagged - whether the current commit is tagged with a version
*/
constructor(
public major: number,
@ -17,5 +18,6 @@ export class ReleaseInformation {
public hash: string,
public currentMajor: number | null,
public currentMinor: number | null,
public currentPatch: number | null,) { }
public currentPatch: number | null,
public isTagged: boolean,) { }
}

View file

@ -15,6 +15,7 @@ export class VersionInformation {
* @param type - The type of change the current range represents
* @param commits - The list of commits for this version
* @param changed - True if the version has changed, false otherwise
* @param isTagged - True if the current commit is a version-tagged commit
*/
constructor(
public major: number,
@ -23,5 +24,6 @@ export class VersionInformation {
public increment: number,
public type: VersionType,
public commits: CommitInfo[],
public changed: boolean) { }
public changed: boolean,
public isTagged: boolean) { }
}