├── .config └── dotnet-tools.json ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── task-runner.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── Directory.Build.props ├── Directory.Packages.props ├── EasyBuild.ChangelogGen.sln ├── LICENSE.txt ├── README.md ├── build.bat ├── build.sh ├── build ├── Commands │ ├── Release.fs │ └── Test.fs ├── EasyBuild.fsproj ├── Main.fs ├── Workspace.fs └── packages.lock.json ├── global.json ├── src ├── Commands │ ├── Generate.fs │ └── Version.fs ├── ConfigLoader.fs ├── EasyBuild.ChangelogGen.fsproj ├── Generate │ ├── Changelog.fs │ ├── ReleaseContext.fs │ ├── Types.fs │ └── Verify.fs ├── Git.fs ├── Log.fs ├── Main.fs ├── Types.fs └── packages.lock.json └── tests ├── Changelog.fs ├── EasyBuild.ChangelogGen.Tests.fsproj ├── Git.fs ├── Main.fs ├── ReleaseContext.fs ├── Utils.fs ├── Verify.fs ├── VerifyTests ├── Changelog.breaking change are going into their own section if configured.verified.md ├── Changelog.breaking change stays in their original group if they don't have a dedicated group.verified.md ├── Changelog.commits are ordered by scope.verified.md ├── Changelog.compare link is not generated if no previous release sha is provided.verified.md ├── Changelog.include changelog additional data when present.verified.md ├── Changelog.only commit of type feat, perf and fix are included in the changelog.verified.md ├── Changelog.works for feat type commit.verified.md ├── Changelog.works for fix type commit.verified.md ├── Changelog.works if metadata was existing.verified.md └── Changelog.works if metadata was not existing.verified.md ├── Workspace.fs ├── fixtures ├── valid_changelog.md ├── valid_changelog_no_metadata.md └── valid_no_version.md └── packages.lock.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fantomas": { 6 | "version": "6.3.4", 7 | "commands": [ 8 | "fantomas" 9 | ] 10 | }, 11 | "husky": { 12 | "version": "0.6.4", 13 | "commands": [ 14 | "husky" 15 | ] 16 | }, 17 | "easybuild.commitlinter": { 18 | "version": "1.0.0", 19 | "commands": [ 20 | "commit-linter" 21 | ] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | 10 | [*.{fs,fsx}] 11 | max_line_length = 100 12 | fsharp_alternative_long_member_definitions = true 13 | fsharp_multi_line_lambda_closing_newline = true 14 | fsharp_multiline_bracket_style = aligned 15 | fsharp_keep_max_number_of_blank_lines = 1 16 | fsharp_align_function_signature_to_indentation = true 17 | fsharp_max_if_then_else_short_width = 0 18 | fsharp_record_multiline_formatter = number_of_items 19 | fsharp_array_or_list_multiline_formatter = number_of_items 20 | 21 | [*.yml] 22 | indent_size = 2 23 | indent_style = space 24 | 25 | # Verify settings 26 | [*.{received,verified}.{json,txt,xml}] 27 | charset = "utf-8-bom" 28 | end_of_line = lf 29 | indent_size = unset 30 | indent_style = unset 31 | insert_final_newline = false 32 | tab_width = unset 33 | trim_trailing_whitespace = false 34 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.verified.txt text eol=lf working-tree-encoding=UTF-8 2 | *.verified.xml text eol=lf working-tree-encoding=UTF-8 3 | *.verified.json text eol=lf working-tree-encoding=UTF-8 4 | 5 | * text=auto eol=lf 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ionide/ 2 | npm/ 3 | 4 | *.fs.js 5 | *.fs.js.map 6 | *.fable-temp.csproj 7 | 8 | obj/ 9 | bin/ 10 | .DS_Store 11 | node_modules/ 12 | demo/dist/ 13 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ## husky task runner examples ------------------- 5 | ## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' 6 | 7 | ## run all tasks 8 | #husky run 9 | 10 | ### run all tasks with group: 'group-name' 11 | #husky run --group group-name 12 | 13 | ## run task with name: 'task-name' 14 | #husky run --name task-name 15 | 16 | ## pass hook arguments to task 17 | #husky run --args "$1" "$2" 18 | 19 | ## or put your custom commands ------------------- 20 | #echo 'Husky.Net is awesome!' 21 | 22 | dotnet commit-linter "$1" 23 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ## husky task runner examples ------------------- 5 | ## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' 6 | 7 | ## run all tasks 8 | #husky run 9 | 10 | ### run all tasks with group: 'group-name' 11 | #husky run --group group-name 12 | 13 | ## run task with name: 'task-name' 14 | #husky run --name task-name 15 | 16 | ## pass hook arguments to task 17 | #husky run --args "$1" "$2" 18 | 19 | ## or put your custom commands ------------------- 20 | #echo 'Husky.Net is awesome!' 21 | 22 | dotnet husky run --name fantomas-format-staged-files 23 | -------------------------------------------------------------------------------- /.husky/task-runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "name": "fantomas-format-staged-files", 5 | "group": "pre-commit", 6 | "command": "dotnet", 7 | "args": [ 8 | "fantomas", 9 | "${staged}" 10 | ], 11 | "include": [ 12 | "**/*.fs", 13 | "**/*.fsx", 14 | "**/*.fsi" 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 8 | 9 | 10 | 11 | 12 | 13 | ## 4.1.0 - 2025-02-10 14 | 15 | ### 🚀 Features 16 | 17 | * Log JSON string when failing to parse it ([cb9536f](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/cb9536f440d28664b783069ac86ade199ea53885)) 18 | 19 | [View changes on Github](https://github.com/easybuild-org/EasyBuild.ChangelogGen/compare/bb89a8cc6780595338c6e88514ca0011f6b142af..94d7305e6f5d89e7b1331b967d4642a67c0d37fb) 20 | 21 | ## 4.0.0 - 2024-12-08 22 | 23 | ### 🏗️ Breaking changes 24 | 25 | * Return exit code `101` if no version bump was needed ([bb89a8c](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/bb89a8cc6780595338c6e88514ca0011f6b142af)) 26 | 27 | [View changes on Github](https://github.com/easybuild-org/EasyBuild.ChangelogGen/compare/5e0fde2b8169e81a247ed2b99d562fb1b0be95f2..bb89a8cc6780595338c6e88514ca0011f6b142af) 28 | 29 | ## 3.0.0 - 2024-12-02 30 | 31 | ### 🏗️ Breaking changes 32 | 33 | * Add support for `perf`, `revert` and `build` commit type to be on par with Angular convention (most common one) ([0732270](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/07322707bdd9d77d40d63edca22c80ac00933863)) 34 | 35 | ### 🚀 Features 36 | 37 | * `perf` commit are included in the generated Changelog and bump the minor version ([849f6f2](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/849f6f225fcf3676e30b8cc014d633885a50ba6f)) 38 | 39 | [View changes on Github](https://github.com/easybuild-org/EasyBuild.ChangelogGen/compare/a1bcdd9b87760bc746889681be33388b5e34c33e..5e0fde2b8169e81a247ed2b99d562fb1b0be95f2) 40 | 41 | ## 2.0.0 - 2024-11-23 42 | 43 | ### 🏗️ Breaking changes 44 | 45 | * `--dry-run` now output only the new version instead of the whole changelog ([a1bcdd9](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/a1bcdd9b87760bc746889681be33388b5e34c33e)) 46 | 47 | [View changes on Github](https://github.com/easybuild-org/EasyBuild.ChangelogGen/compare/d358dd739e2c0d9efa17491102a2fc80ef9494f1..a1bcdd9b87760bc746889681be33388b5e34c33e) 48 | 49 | ## 1.1.2 - 2024-11-23 50 | 51 | ### 🐞 Bug Fixes 52 | 53 | * Upgrade `EasyBuild.CommitParser` to support parsing footer with trailing lines ([d951fae](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/d951fae25d5563722ab063804cee2aa4e516f297)) 54 | * Upgrade `EasyBuild.CommitParser` to support parsing footer with trailing lines ([d358dd7](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/d358dd739e2c0d9efa17491102a2fc80ef9494f1)) 55 | 56 | [View changes on Github](https://github.com/easybuild-org/EasyBuild.ChangelogGen/compare/9a766589b3166ec918c25e7e5db3b947e8be0300..d358dd739e2c0d9efa17491102a2fc80ef9494f1) 57 | 58 | ## 1.1.1 - 2024-11-18 59 | 60 | ### 🐞 Bug Fixes 61 | 62 | * Link `PackageProjectUrl`, `RepositoryUrl` and `Packagelicense` to the correct repository ([9a76658](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/9a766589b3166ec918c25e7e5db3b947e8be0300)) 63 | 64 | [View changes on Github](https://github.com/easybuild-org/EasyBuild.ChangelogGen/compare/181edc555c6cd39c10efbe7ed73443e3078f45d2..9a766589b3166ec918c25e7e5db3b947e8be0300) 65 | 66 | ## 1.1.0 - 2024-11-18 67 | 68 | ### 🚀 Features 69 | 70 | * Add generation of `compare` link + date of release for the version + include `scope` if present ([181edc5](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/181edc555c6cd39c10efbe7ed73443e3078f45d2)) 71 | 72 | [View changes on Github](https://github.com/easybuild-org/EasyBuild.ChangelogGen/compare/62c8d027fa9664603a7a06562dd33de2d5fdd55b..181edc555c6cd39c10efbe7ed73443e3078f45d2) 73 | 74 | ## 1.0.0 75 | 76 | ### 🏗️ Breaking changes 77 | 78 | * Make the CLI return the version in stdout allowing other tools to capture the generate version easily ([0c69cda](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/0c69cdabc0c852f93b35f7712403a7f38b6fe1b4)) 79 | 80 | ### 🚀 Features 81 | 82 | * Initial implementation ([e797f3c](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/e797f3c08781a975a0dfc73776bdd0436ecc466f)) 83 | * Implements the pre-release logic ([2e34813](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/2e34813ff488940a3beb18fcd82f2581ba2d1d78)) 84 | * Make pre-release version follows the standard upgrade version (avoid edge cases) + use `Tag` footer for filtering project on monorepo ([3473311](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/3473311a89bcbe6d10dbd31c4748993a48c2b1d0)) 85 | 86 | This makes the tool compatible with Conventional Commit spec 87 | * Add automatic resolution for Github remote (user can still configure it using Config file) ([be8313e](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/be8313e66ae095bd3d90d095340e6e0f44526a49)) 88 | * Remove custom config file in favor of using CLI arguments to configure `--github-repo` + make the generator more opinionated by only allowing Breaking Change, Feat and Fix to be included in the changelog ([cf8c5a3](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/cf8c5a3620b279187441331cd0954ba5601a2e5e)) 89 | 90 | ### 🐞 Bug Fixes 91 | 92 | * Remove the 0.0.0 version hack as EasyBuild.PackageReleaseNotes.Tasks now supports Changelog without versions ([deff564](https://github.com/easybuild-org/EasyBuild.ChangelogGen/commit/deff564b16b08e2df6eced134475218fdece9ee7)) 93 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | true 6 | 7 | 8 | 9 | true 10 | 11 | 12 | true 13 | true 14 | 15 | 16 | https://github.com/easybuild-org/EasyBuild.ChangelogGen 17 | 18 | https://github.com/easybuild-org/EasyBuild.ChangelogGen/blob/master/LICENSE.txt 19 | https://github.com/easybuild-org/EasyBuild.ChangelogGen 20 | LICENSE.txt 21 | README.md 22 | true 23 | Maxime Mangel 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | all 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /EasyBuild.ChangelogGen.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "EasyBuild.ChangelogGen", "src\EasyBuild.ChangelogGen.fsproj", "{E63E880E-1879-435E-86DD-D99FF720F2D4}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "EasyBuild.ChangelogGen.Tests", "tests\EasyBuild.ChangelogGen.Tests.fsproj", "{70BE19B5-3E11-4CC2-9E3A-5D8792C092F7}" 9 | EndProject 10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "EasyBuild", "build\EasyBuild.fsproj", "{EF9785DD-EAEA-4C3E-9FE3-FAFCAD3D42E7}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {E63E880E-1879-435E-86DD-D99FF720F2D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {E63E880E-1879-435E-86DD-D99FF720F2D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {E63E880E-1879-435E-86DD-D99FF720F2D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {E63E880E-1879-435E-86DD-D99FF720F2D4}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {70BE19B5-3E11-4CC2-9E3A-5D8792C092F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {70BE19B5-3E11-4CC2-9E3A-5D8792C092F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {70BE19B5-3E11-4CC2-9E3A-5D8792C092F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {70BE19B5-3E11-4CC2-9E3A-5D8792C092F7}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {EF9785DD-EAEA-4C3E-9FE3-FAFCAD3D42E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {EF9785DD-EAEA-4C3E-9FE3-FAFCAD3D42E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {EF9785DD-EAEA-4C3E-9FE3-FAFCAD3D42E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {EF9785DD-EAEA-4C3E-9FE3-FAFCAD3D42E7}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [Mangel Maxime] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyBuild.ChangelogGen 2 | 3 | [![NuGet](https://img.shields.io/nuget/v/EasyBuild.ChangelogGen.svg)](https://www.nuget.org/packages/EasyBuild.ChangelogGen) 4 | 5 | [![Sponsors badge link](https://img.shields.io/badge/Sponsors_this_project-EA4AAA?style=for-the-badge)](https://mangelmaxime.github.io/sponsors/) 6 | 7 | Tool for generating changelog based on Git history based on [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). It is using [EasyBuild.CommitParser](https://github.com/easybuild-org/EasyBuild.CommitParser) to parse commit messages check their documentation for more information about configuration. 8 | 9 | ## Usage 10 | 11 | ```bash 12 | # Install the tool 13 | dotnet tool install EasyBuild.ChangelogGen 14 | 15 | # Run the tool 16 | dotnet changelog-gen 17 | ``` 18 | 19 | ### CLI manual 20 | 21 | ```txt 22 | USAGE: 23 | changelog-gen [changelog] [OPTIONS] [COMMAND] 24 | 25 | ARGUMENTS: 26 | [changelog] Path to the changelog file. Default is CHANGELOG.md 27 | 28 | OPTIONS: 29 | DEFAULT 30 | -h, --help Prints help information 31 | -v, --version Prints version information 32 | -c, --config Path to the configuration file 33 | --allow-dirty Allow to run in a dirty repository 34 | (having not commit changes in your 35 | reporitory) 36 | --allow-branch List of branches that are allowed to 37 | be used to generate the changelog. 38 | Default is 'main' 39 | --tag List of tags to include in the 40 | changelog 41 | --pre-release [PREFIX] beta Indicate that the generated version is 42 | a pre-release version. Optionally, you 43 | can provide a prefix for the beta 44 | version. Default is 'beta' 45 | --force-version Force the version to be used in the 46 | changelog 47 | --skip-invalid-commit Skip invalid commits instead of 48 | failing 49 | --dry-run Run the command without writing to the 50 | changelog file, output the result in 51 | STDOUT instead 52 | --github-repo GitHub repository name in format 53 | 'owner/repo' 54 | 55 | COMMANDS: 56 | version 57 | ``` 58 | 59 | ### How is the version calculated? 60 | 61 | ### Stable versions 62 | 63 | The version is calculated based on the commit messages since last released. 64 | 65 | Rules are the following: 66 | 67 | - A `breaking change` commit will bump the major version 68 | 69 | ```text 70 | * chore: release 1.2.10 71 | * feat!: first feature # => 2.0.0 72 | ``` 73 | 74 | - `feat` commits will bump the minor version 75 | 76 | ```text 77 | * chore: release 1.2.10 78 | * feat: first feature 79 | * feat: second feature # => 1.3.0 80 | ``` 81 | 82 | - `perf` commits will bump the minor version 83 | 84 | ```text 85 | * chore: release 1.2.10 86 | * perf: first performance improvement 87 | * perf: second performance improvement # => 1.3.0 88 | ``` 89 | 90 | - `fix` commits will bump the patch version 91 | 92 | ```text 93 | * chore: release 1.2.10 94 | * fix: first fix 95 | * fix: second fix # => 1.2.11 96 | ``` 97 | 98 | You can mix different types of commits, the highest version will be used (`breaking change` > `feat` or `perf` > `fix`). 99 | 100 | ```text 101 | * chore: release 1.2.10 102 | * feat: first feature 103 | * perf: first performance improvement 104 | * fix: first fix # => 1.3.0 105 | ``` 106 | 107 | ### Pre-release versions 108 | 109 | Passing `--pre-release [PREFIX]` will generate a pre-release version. 110 | 111 | Rules are the following: 112 | 113 | - If the previous version is **stable**, then we compute the standard version bump and start a new pre-release version. 114 | 115 | ```text 116 | * chore: release 1.2.10 117 | * feat: first feature 118 | * fix: first fix # => 1.3.0-beta.1 119 | ``` 120 | 121 | - If the previous version is a **pre-release**, with the same suffix, then we increment the pre-release version. 122 | 123 | ```text 124 | * chore: release 1.3.0-beta.10 125 | * feat: first feature 126 | * fix: first fix # => 1.3.0-beta.11 127 | ``` 128 | 129 | - If the previous version is a **pre-release**, with a different suffix, then we use the same base version and start a new pre-release version. 130 | 131 | ```text 132 | * chore: release 1.3.0-alpha.10 133 | * feat: first feature 134 | * fix: first fix # => 1.3.0-beta.1 135 | ``` 136 | 137 | **💡 Tips** 138 | 139 | EasyBuild.Changelog use the last version in the changelog file to compute the next version. 140 | 141 | For this reason, while working on a pre-release, it is advised to work in a separate branch from the main branch. This allows you to work on the pre-release while still being able to release new versions on the main branch. 142 | 143 | ```text 144 | * chore: release 1.2.10 145 | | \ 146 | | * feat!: remove `foo` API 147 | | * feat: add `bar` API # => 2.0.0.beta.1 148 | | * fix: fix `baz` API 149 | * fix: fix `qux` API 150 | * chore: release 1.2.11 151 | | * fix: fix `qux` API # => 2.0.0.beta.2 152 | | / 153 | * chore: release 2.0.0 # => 2.0.0 154 | ``` 155 | 156 | ### Moving out of pre-release 157 | 158 | If you want to move out of pre-release, you simply need to remove the `--pre-release` CLI options. 159 | 160 | Then the next version will be released using the base version of the previous pre-release. 161 | 162 | ```text 163 | * chore: release 1.3.0-beta.10 164 | * feat: first feature 165 | * fix: first fix # => 1.3.0 166 | ``` 167 | 168 | If you are not sure what will be calculated, you can use the `--dry-run` option to see the result without writing it to the changelog file. 169 | 170 | ### Overriding the version 171 | 172 | If the computed version is not what you want, you can use the `--force-version` option to override the version to any value you want. 173 | 174 | ```bash 175 | dotnet changelog-gen --force-version 2.0.0 176 | ``` 177 | 178 | ## Monorepo support 179 | 180 | EasyBuild.ChangelogGen supports monorepo. To do so, it use the `Tag` footer as specified in [EasyBuild.CommitParser](https://github.com/easybuild-org/EasyBuild.CommitParser). 181 | 182 | For example, if we have the following 3 commits: 183 | 184 | ```text 185 | ---------------------------------------------- 186 | feat: add interface support 187 | 188 | Tag: converter 189 | ---------------------------------------------- 190 | feat: add `export` support 191 | 192 | Tag: converter 193 | ---------------------------------------------- 194 | feat: add new CLI options 195 | 196 | Tag: cli 197 | ---------------------------------------------- 198 | ``` 199 | 200 | Then I can run `dotnet changelog-gen src/converter/CHANGELOG.md --tag converter` to generate the changelog using only the commits with the `converter` tag. 201 | 202 | ```bash 203 | dotnet changelog-gen src/converter/CHANGELOG.md --tag converter 204 | ``` 205 | 206 | ## Exit codes 207 | 208 | ### Standard exit codes 209 | 210 | - `0`: Success 211 | - `1`: Error 212 | 213 | ### Custom exit codes 214 | 215 | The following exit codes serves as a way to communicate with other tools. It is left to the user to decide if they want to treat this as an error or not. 216 | 217 | - `100`: Help was requested 218 | - `101`: No version bump needed 219 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | dotnet tool restore 4 | dotnet run --project build/EasyBuild.fsproj -- %* 5 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | dotnet tool restore 4 | dotnet run --project build/EasyBuild.fsproj -- $@ 5 | -------------------------------------------------------------------------------- /build/Commands/Release.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.Commands.Release 2 | 3 | open Spectre.Console.Cli 4 | open SimpleExec 5 | open EasyBuild.Workspace 6 | open System 7 | open System.IO 8 | open BlackFox.CommandLine 9 | open EasyBuild.Tools.DotNet 10 | open EasyBuild.Tools.Git 11 | open EasyBuild.Commands.Test 12 | 13 | type ReleaseSettings() = 14 | inherit CommandSettings() 15 | 16 | type ReleaseCommand() = 17 | inherit Command() 18 | interface ICommandLimiter 19 | 20 | override __.Execute(context, settings) = 21 | TestCommand().Execute(context, TestSettings()) |> ignore 22 | 23 | // Clean up the src/bin folder 24 | if Directory.Exists VirtualWorkspace.src.bin.``.`` then 25 | Directory.Delete(VirtualWorkspace.src.bin.``.``, true) 26 | 27 | let (struct (newVersion, _)) = 28 | Command.ReadAsync( 29 | "dotnet", 30 | CmdLine.empty 31 | |> CmdLine.appendRaw "run" 32 | |> CmdLine.appendPrefix "--project" Workspace.src.``EasyBuild.ChangelogGen.fsproj`` 33 | |> CmdLine.appendPrefix "--configuration" "Release" 34 | |> CmdLine.appendRaw "--" 35 | |> CmdLine.appendSeq context.Remaining.Raw 36 | |> CmdLine.toString, 37 | workingDirectory = Workspace.``.`` 38 | ) 39 | |> Async.AwaitTask 40 | |> Async.RunSynchronously 41 | 42 | let nupkgPath = DotNet.pack Workspace.src.``.`` 43 | 44 | DotNet.nugetPush nupkgPath 45 | 46 | Git.addAll () 47 | Git.commitRelease newVersion 48 | Git.push () 49 | 50 | 0 51 | -------------------------------------------------------------------------------- /build/Commands/Test.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.Commands.Test 2 | 3 | open Spectre.Console.Cli 4 | open SimpleExec 5 | open EasyBuild.Workspace 6 | open BlackFox.CommandLine 7 | 8 | type TestSettings() = 9 | inherit CommandSettings() 10 | 11 | [] 12 | member val IsWatch = false with get, set 13 | 14 | type TestCommand() = 15 | inherit Command() 16 | interface ICommandLimiter 17 | 18 | override __.Execute(context, settings) = 19 | if settings.IsWatch then 20 | Command.Run( 21 | "dotnet", 22 | CmdLine.empty 23 | |> CmdLine.appendRaw "watch" 24 | |> CmdLine.appendRaw "test" 25 | |> CmdLine.appendPrefix 26 | "--project" 27 | Workspace.tests.``EasyBuild.ChangelogGen.Tests.fsproj`` 28 | |> CmdLine.toString 29 | ) 30 | else 31 | Command.Run("dotnet", "test") 32 | 33 | 0 34 | -------------------------------------------------------------------------------- /build/EasyBuild.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | False 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /build/Main.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.Main 2 | 3 | open Spectre.Console.Cli 4 | open EasyBuild.Commands.Test 5 | open EasyBuild.Commands.Release 6 | open SimpleExec 7 | 8 | [] 9 | let main args = 10 | 11 | Command.Run("dotnet", "husky install") 12 | 13 | let app = CommandApp() 14 | 15 | app.Configure(fun config -> 16 | config.Settings.ApplicationName <- "./build.sh" 17 | 18 | config 19 | .AddCommand("test") 20 | .WithDescription("Run the tests") 21 | .WithExample("test") 22 | .WithExample("test --watch") 23 | |> ignore 24 | 25 | config 26 | .AddCommand("release") 27 | .WithDescription( 28 | "Package a new version of the library and publish it to NuGet. You can use `-- --help` to see all the available options." 29 | ) 30 | .WithExample("release -- --help [Show all available options]") 31 | .WithExample("release -- --major") 32 | |> ignore 33 | 34 | ) 35 | 36 | app.Run(args) 37 | -------------------------------------------------------------------------------- /build/Workspace.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.Workspace 2 | 3 | open EasyBuild.FileSystemProvider 4 | 5 | [] 6 | let root = __SOURCE_DIRECTORY__ + "/../" 7 | 8 | type Workspace = RelativeFileSystem 9 | 10 | type VirtualWorkspace = 11 | VirtualFileSystem< 12 | root, 13 | """ 14 | src 15 | bin/ 16 | demo 17 | dist/ 18 | """ 19 | > 20 | -------------------------------------------------------------------------------- /build/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net8.0": { 5 | "BlackFox.CommandLine": { 6 | "type": "Direct", 7 | "requested": "[1.0.0, )", 8 | "resolved": "1.0.0", 9 | "contentHash": "dSW7uLetl021HQXKcZd1xrXPjhsXgaJ5U4tFe64DLja1KZ2Ce6QeugHvZDvLfcPkEc1ZPRF7fWv5/T+X3ThWTA==", 10 | "dependencies": { 11 | "FSharp.Core": "4.2.3" 12 | } 13 | }, 14 | "EasyBuild.FileSystemProvider": { 15 | "type": "Direct", 16 | "requested": "[0.3.0, )", 17 | "resolved": "0.3.0", 18 | "contentHash": "gdVJpqcMDJm4IfmITy3MtpEn/lo9pH8PirVlENtXGX9Sdw3rCgoo9ch1TAthUseh28RcUGWwza9BmEWlrQX/Aw==" 19 | }, 20 | "EasyBuild.Tools": { 21 | "type": "Direct", 22 | "requested": "[3.1.0, )", 23 | "resolved": "3.1.0", 24 | "contentHash": "q+4ESny4CyHfoQ1zL0l/pfWereKxlzB/Lbcx2b5BWZi7OUbErIKRS1yMGMNho4cMqG8p6jj55HTsCvE1jDhKDg==", 25 | "dependencies": { 26 | "BlackFox.CommandLine": "1.0.0", 27 | "FSharp.Core": "6.0.0", 28 | "SimpleExec": "12.0.0", 29 | "System.Text.Json": "9.0.0" 30 | } 31 | }, 32 | "FSharp.Core": { 33 | "type": "Direct", 34 | "requested": "[8.0.200, )", 35 | "resolved": "8.0.200", 36 | "contentHash": "qnxoF3Fu0HzfOeYdrwmQOsLP1v+OtOMSIYkNVUwf6nGqWzL03Hh4r6VFCvCb54jlsgtt3WADVYkKkrgdeY5kiQ==" 37 | }, 38 | "Semver": { 39 | "type": "Direct", 40 | "requested": "[2.3.0, )", 41 | "resolved": "2.3.0", 42 | "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" 43 | }, 44 | "SimpleExec": { 45 | "type": "Direct", 46 | "requested": "[12.0.0, )", 47 | "resolved": "12.0.0", 48 | "contentHash": "ptxlWtxC8vM6Y6e3h9ZTxBBkOWnWrm/Sa1HT+2i1xcXY3Hx2hmKDZP5RShPf8Xr9D+ivlrXNy57ktzyH8kyt+Q==" 49 | }, 50 | "Spectre.Console.Cli": { 51 | "type": "Direct", 52 | "requested": "[0.49.0, )", 53 | "resolved": "0.49.0", 54 | "contentHash": "841g7PhuJFwoCatKKVoIRDaylmcaTxEzTzq7+rGHyVCBUqL7iOH0c5AsGnjgBMzhRDnUUWkP47Ho9WWfqMVqAA==", 55 | "dependencies": { 56 | "Spectre.Console": "0.49.0" 57 | } 58 | }, 59 | "Spectre.Console": { 60 | "type": "Transitive", 61 | "resolved": "0.49.0", 62 | "contentHash": "1s+hhYq5fcrqCvZhrNOPehAmCZJM6cjro85g1Qirvmm1tys+/sfFJgePCGcxr+S/nONZ/lCDTflPda3C+WlBdg==" 63 | }, 64 | "System.IO.Pipelines": { 65 | "type": "Transitive", 66 | "resolved": "9.0.0", 67 | "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" 68 | }, 69 | "System.Text.Encodings.Web": { 70 | "type": "Transitive", 71 | "resolved": "9.0.0", 72 | "contentHash": "e2hMgAErLbKyUUwt18qSBf9T5Y+SFAL3ZedM8fLupkVj8Rj2PZ9oxQ37XX2LF8fTO1wNIxvKpihD7Of7D/NxZw==" 73 | }, 74 | "System.Text.Json": { 75 | "type": "Transitive", 76 | "resolved": "9.0.0", 77 | "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==", 78 | "dependencies": { 79 | "System.IO.Pipelines": "9.0.0", 80 | "System.Text.Encodings.Web": "9.0.0" 81 | } 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.204", 4 | "rollForward": "latestMinor" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Commands/Generate.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Commands.Generate 2 | 3 | open Spectre.Console.Cli 4 | open FsToolkit.ErrorHandling 5 | open EasyBuild.ChangelogGen 6 | open EasyBuild.ChangelogGen.Generate 7 | open EasyBuild.ChangelogGen.Generate.Types 8 | open System.IO 9 | 10 | type GenerateCommand() = 11 | inherit Command() 12 | 13 | interface ICommandLimiter 14 | 15 | override __.Execute(context, settings) = 16 | let res = 17 | result { 18 | let! config = ConfigLoader.tryLoadConfig settings.Config 19 | // Apply automatic resolution of remote config if needed 20 | let! remoteConfig = Verify.resolveRemoteConfig settings 21 | 22 | let! changelogInfo = Changelog.load settings 23 | do! Verify.dirty settings 24 | do! Verify.branch settings 25 | // do! Verify.options settings changelogInfo 26 | 27 | let commits = ReleaseContext.getCommits settings changelogInfo 28 | 29 | let releaseContext = 30 | ReleaseContext.compute settings changelogInfo commits config.CommitParserConfig 31 | 32 | match releaseContext with 33 | | NoVersionBumpRequired -> 34 | Log.success "No version bump required." 35 | return 101 36 | | BumpRequired bumpInfo -> 37 | if settings.DryRun then 38 | let newVersionContent = 39 | Changelog.generateNewVersionSection 40 | remoteConfig 41 | changelogInfo.LastReleaseCommit 42 | bumpInfo 43 | 44 | Log.info "Dry run enabled, new version content:" 45 | printfn "%s" newVersionContent 46 | return 0 47 | else 48 | let newChangelogContent = 49 | Changelog.updateWithNewVersion remoteConfig bumpInfo changelogInfo 50 | 51 | File.WriteAllText(changelogInfo.File.FullName, newChangelogContent) 52 | Log.success ($"Changelog updated with new version:") 53 | // Print to stdout so it can be captured easily by other tools 54 | printfn "%s" (bumpInfo.NewVersion.ToString()) 55 | return 0 56 | } 57 | 58 | match res with 59 | | Ok exitCode -> exitCode 60 | | Error error -> 61 | Log.error error 62 | 1 63 | -------------------------------------------------------------------------------- /src/Commands/Version.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Commands.Version 2 | 3 | open Spectre.Console.Cli 4 | open System.Reflection 5 | 6 | type VersionSettings() = 7 | inherit CommandSettings() 8 | 9 | type VersionCommand() = 10 | inherit Command() 11 | interface ICommandLimiter 12 | 13 | override __.Execute(_, _) = 14 | let assembly = Assembly.GetEntryAssembly() 15 | 16 | let versionAttribute = 17 | assembly.GetCustomAttribute() 18 | 19 | let version = 20 | if versionAttribute <> null then 21 | versionAttribute.InformationalVersion 22 | else 23 | "?" 24 | 25 | Log.info ($"Version: {version}") 26 | 0 27 | -------------------------------------------------------------------------------- /src/ConfigLoader.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.ConfigLoader 2 | 3 | open EasyBuild.CommitParser.Types 4 | open System.IO 5 | open Thoth.Json.Core 6 | open Thoth.Json.Newtonsoft 7 | open EasyBuild.ChangelogGen.Types 8 | 9 | type Config = 10 | { 11 | CommitParserConfig: CommitParserConfig 12 | } 13 | 14 | static member Decoder: Decoder = 15 | Decode.object (fun get -> 16 | { CommitParserConfig = get.Required.Raw CommitParserConfig.decoder } 17 | ) 18 | 19 | static member Default = { CommitParserConfig = CommitParserConfig.Default } 20 | 21 | let tryLoadConfig (configFile: string option) : Result = 22 | let configFile = 23 | match configFile with 24 | | Some configFile -> Some <| FileInfo configFile 25 | | None -> None 26 | 27 | match configFile with 28 | | Some configFile -> 29 | if not configFile.Exists then 30 | Error $"Configuration file '{configFile.FullName}' does not exist." 31 | else 32 | 33 | let configContent = File.ReadAllText(configFile.FullName) 34 | 35 | match Decode.fromString Config.Decoder configContent with 36 | | Ok config -> config |> Ok 37 | | Error error -> Error $"Failed to parse configuration file:\n\n{error}" 38 | 39 | | None -> Config.Default |> Ok 40 | -------------------------------------------------------------------------------- /src/EasyBuild.ChangelogGen.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | Major 7 | true 8 | changelog-gen 9 | $(MSBuildThisFileDirectory)../CHANGELOG.md 10 | 11 | 12 | 13 | <_Parameter1>EasyBuild.ChangelogGen.Tests 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | all 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Generate/Changelog.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Generate.Changelog 2 | 3 | open System 4 | open System.IO 5 | open FsToolkit.ErrorHandling 6 | open Semver 7 | open EasyBuild.ChangelogGen 8 | open EasyBuild.ChangelogGen.Types 9 | open EasyBuild.ChangelogGen.Generate.Types 10 | open System.Text.RegularExpressions 11 | 12 | [] 13 | let EMPTY_CHANGELOG = 14 | """# Changelog 15 | 16 | All notable changes to this project will be documented in this file. 17 | 18 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 19 | 20 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 21 | """ 22 | 23 | let findVersions (content: string) = 24 | 25 | let matches = 26 | Regex.Matches( 27 | content, 28 | "^##\\s\\[?v?(?[\\w\\d.-]+\\.[\\w\\d.-]+[a-zA-Z0-9])\\]?(\\s-\\s(?\\d{4}-\\d{2}-\\d{2}))?$", 29 | RegexOptions.Multiline 30 | ) 31 | 32 | matches 33 | |> Seq.map (fun m -> 34 | let version = m.Groups.["version"].Value 35 | 36 | match SemVersion.TryParse(version, SemVersionStyles.Strict) with 37 | | true, version -> version 38 | | false, _ -> failwith "Invalid version" 39 | ) 40 | |> Seq.toList 41 | 42 | let load (settings: GenerateSettings) = 43 | let changelogFile = FileInfo settings.Changelog 44 | 45 | if not changelogFile.Exists then 46 | Log.info ($"File '{changelogFile.FullName}' does not exist, creating a new one.") 47 | 48 | { 49 | File = changelogFile 50 | Content = EMPTY_CHANGELOG 51 | Versions = [] 52 | } 53 | |> Ok 54 | 55 | else 56 | Log.info ($"Changelog file '{changelogFile.FullName}' found.") 57 | 58 | let changelogContent = File.ReadAllText(changelogFile.FullName) 59 | 60 | { 61 | File = changelogFile 62 | Content = changelogContent 63 | Versions = findVersions changelogContent 64 | } 65 | |> Ok 66 | 67 | let tryFindAdditionalChangelogContent (text: string) : string list list = 68 | let lines = text.Replace("\r\n", "\n").Split('\n') |> Seq.toList 69 | 70 | let rec apply 71 | (acc: string list list) 72 | (lines: string list) 73 | (currentBlock: string list) 74 | (isInsideChangelogBlock: bool) 75 | = 76 | match lines with 77 | | [] -> acc 78 | | line :: rest -> 79 | if isInsideChangelogBlock then 80 | if line = "=== changelog ===" then 81 | apply (acc @ [ currentBlock ]) rest [] false 82 | else 83 | apply acc rest (currentBlock @ [ line ]) true 84 | else if line = "=== changelog ===" then 85 | apply acc rest currentBlock true 86 | else 87 | apply acc rest currentBlock false 88 | 89 | apply [] lines [] false 90 | 91 | let private capitalizeFirstLetter (text: string) = 92 | (string text.[0]).ToUpper() + text.[1..] 93 | 94 | module Literals = 95 | 96 | module Type = 97 | 98 | [] 99 | let BREAKING_CHANGE = "breaking change" 100 | 101 | [] 102 | let FEAT = "feat" 103 | 104 | [] 105 | let PERF = "perf" 106 | 107 | [] 108 | let FIX = "fix" 109 | 110 | let (|BreakingChange|Feat|Perf|Fix|Other|) (commit: EasyBuild.CommitParser.Types.CommitMessage) = 111 | if commit.BreakingChange then 112 | BreakingChange 113 | elif commit.Type = Literals.Type.FEAT then 114 | Feat 115 | elif commit.Type = Literals.Type.PERF then 116 | Perf 117 | elif commit.Type = Literals.Type.FIX then 118 | Fix 119 | else 120 | Other 121 | 122 | type GroupedCommits = 123 | { 124 | BreakingChanges: CommitForRelease list 125 | Feats: CommitForRelease list 126 | Perfs: CommitForRelease list 127 | Fixes: CommitForRelease list 128 | } 129 | 130 | type Writer() = 131 | let lines = ResizeArray() 132 | 133 | member _.AppendLine(line: string) = lines.Add(line) 134 | 135 | member _.NewLine() = lines.Add("") 136 | 137 | member _.ToText() = lines |> String.concat "\n" 138 | 139 | let private writeSection 140 | (writer: Writer) 141 | (label: string) 142 | (githubRemote: GithubRemoteConfig) 143 | (commits: CommitForRelease list) 144 | = 145 | if commits.Length > 0 then 146 | writer.AppendLine $"### %s{label}" 147 | writer.NewLine() 148 | 149 | let commits = commits |> List.sortBy (fun commit -> commit.SemanticCommit.Scope) 150 | 151 | for commit in commits do 152 | let githubCommitUrl sha = 153 | $"https://github.com/%s{githubRemote.Owner}/%s{githubRemote.Repository}/commit/%s{sha}" 154 | 155 | let commitUrl = githubCommitUrl commit.OriginalCommit.Hash 156 | 157 | let description = capitalizeFirstLetter commit.SemanticCommit.Description 158 | 159 | [ 160 | "*" 161 | match commit.SemanticCommit.Scope with 162 | | Some scope -> $"*(%s{scope})*" 163 | | None -> () 164 | $"%s{description.Trim()} ([%s{commit.OriginalCommit.AbbrevHash}](%s{commitUrl}))" 165 | ] 166 | |> String.concat " " 167 | |> writer.AppendLine 168 | 169 | let additionalChangelogContent = 170 | tryFindAdditionalChangelogContent commit.SemanticCommit.Body 171 | 172 | for blockLines in additionalChangelogContent do 173 | writer.NewLine() 174 | 175 | for line in blockLines do 176 | $" %s{line}" |> _.TrimEnd() |> writer.AppendLine 177 | 178 | writer.NewLine() 179 | 180 | let generateNewVersionSection 181 | (githubRemote: GithubRemoteConfig) 182 | (previousReleasedSha: string option) 183 | (releaseContext: BumpInfo) 184 | = 185 | let writer = Writer() 186 | 187 | [ 188 | "##" 189 | $"%s{releaseContext.NewVersion.ToString()}" 190 | "-" 191 | // If in debug mode, use a fixed date to make testing stable 192 | #if DEBUG 193 | "2024-11-18" 194 | #else 195 | DateTime.UtcNow.ToString("yyyy-MM-dd") 196 | #endif 197 | ] 198 | |> String.concat " " 199 | |> writer.AppendLine 200 | 201 | writer.NewLine() 202 | 203 | let rec groupCommits (acc: GroupedCommits) (commits: CommitForRelease list) = 204 | 205 | match commits with 206 | | [] -> acc 207 | | commit :: rest -> 208 | match commit.SemanticCommit with 209 | | BreakingChange -> 210 | groupCommits { acc with BreakingChanges = commit :: acc.BreakingChanges } rest 211 | | Feat -> groupCommits { acc with Feats = commit :: acc.Feats } rest 212 | | Fix -> groupCommits { acc with Fixes = commit :: acc.Fixes } rest 213 | | Perf -> groupCommits { acc with Perfs = commit :: acc.Perfs } rest 214 | // This commit type is not to be emitted in the changelog 215 | | Other -> groupCommits acc rest 216 | 217 | let groupedCommits = 218 | groupCommits 219 | { 220 | BreakingChanges = [] 221 | Feats = [] 222 | Perfs = [] 223 | Fixes = [] 224 | } 225 | releaseContext.CommitsForRelease 226 | 227 | writeSection writer "🏗️ Breaking changes" githubRemote groupedCommits.BreakingChanges 228 | writeSection writer "🚀 Features" githubRemote groupedCommits.Feats 229 | writeSection writer "🐞 Bug Fixes" githubRemote groupedCommits.Fixes 230 | writeSection writer "⚡ Performance Improvements" githubRemote groupedCommits.Perfs 231 | 232 | match previousReleasedSha with 233 | | Some sha -> 234 | let compareUrl = 235 | $"https://github.com/%s{githubRemote.Owner}/%s{githubRemote.Repository}/compare/%s{sha}..%s{releaseContext.LastCommitSha}" 236 | 237 | $"[View changes on Github](%s{compareUrl})" 238 | |> writer.AppendLine 239 | 240 | writer.NewLine() 241 | 242 | | None -> () 243 | 244 | writer.ToText() 245 | 246 | let updateWithNewVersion 247 | (githubRemote: GithubRemoteConfig) 248 | (releaseContext: BumpInfo) 249 | (changelogInfo: ChangelogInfo) 250 | = 251 | let newVersionLines = 252 | generateNewVersionSection githubRemote changelogInfo.LastReleaseCommit releaseContext 253 | 254 | let rec removeConsecutiveEmptyLines 255 | (previousLineWasBlank: bool) 256 | (result: string list) 257 | (lines: string list) 258 | = 259 | match lines with 260 | | [] -> result 261 | | line :: rest -> 262 | if previousLineWasBlank && String.IsNullOrWhiteSpace(line) then 263 | removeConsecutiveEmptyLines true result rest 264 | else 265 | removeConsecutiveEmptyLines 266 | (String.IsNullOrWhiteSpace(line)) 267 | (result @ [ line ]) 268 | rest 269 | 270 | let hasEasyBuildMetadata = 271 | changelogInfo.Lines |> Seq.contains "" 272 | 273 | let newChangelogContent = 274 | [ 275 | // Add title and description of the original changelog 276 | if hasEasyBuildMetadata then 277 | yield! 278 | changelogInfo.Lines 279 | |> Seq.takeWhile (fun line -> "" <> line) 280 | else 281 | yield! 282 | changelogInfo.Lines |> Seq.takeWhile (fun line -> not (line.StartsWith("##"))) 283 | 284 | // Ad EasyBuild metadata 285 | "" 286 | $"" 287 | "" 288 | "" 289 | 290 | // New version 291 | newVersionLines 292 | 293 | // Add the rest of the changelog 294 | yield! changelogInfo.Lines |> Seq.skipWhile (fun line -> not (line.StartsWith("##"))) 295 | ] 296 | |> removeConsecutiveEmptyLines false [] 297 | |> String.concat "\n" 298 | 299 | newChangelogContent 300 | -------------------------------------------------------------------------------- /src/Generate/ReleaseContext.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Generate.ReleaseContext 2 | 3 | open Semver 4 | open EasyBuild.CommitParser 5 | open EasyBuild.CommitParser.Types 6 | open EasyBuild.ChangelogGen.Generate.Types 7 | 8 | let getCommits (settings: GenerateSettings) (changelog: ChangelogInfo) = 9 | let commitFilter = 10 | match changelog.LastReleaseCommit with 11 | | Some lastReleasedCommit -> Git.GetCommitsFilter.From lastReleasedCommit 12 | | None -> Git.GetCommitsFilter.All 13 | 14 | Git.getCommits commitFilter 15 | 16 | let computeVersion 17 | (settings: GenerateSettings) 18 | (commitsForRelease: CommitForRelease list) 19 | (refVersion: SemVersion) 20 | = 21 | 22 | if refVersion.IsPrerelease then 23 | if settings.PreRelease.IsSet then 24 | 25 | // If the pre-release identifier is the same, then increment the pre-release number 26 | // Before: 2.0.0-beta.1 -> After: 2.0.0-beta.2 27 | if refVersion.Prerelease.StartsWith(settings.PreRelease.Value) then 28 | let index = refVersion.Prerelease.IndexOf(settings.PreRelease.Value + ".") 29 | 30 | if index >= 0 then 31 | let preReleaseNumber = 32 | refVersion.Prerelease.Substring( 33 | index + settings.PreRelease.Value.Length + 1 34 | ) 35 | |> int 36 | 37 | refVersion.WithPrereleaseParsedFrom( 38 | settings.PreRelease.Value + "." + (preReleaseNumber + 1).ToString() 39 | ) 40 | |> Some 41 | else 42 | // This should never happen 43 | // If the pre-release identifier is present, then the pre-release number should also be present 44 | // If the pre-release identifier is not present, then the version should be a release 45 | // So, this should be a safe assumption 46 | failwith "Invalid pre-release identifier" 47 | // Otherwise, start a new pre-release from 1 48 | // This can happens when moving from alpha to beta, for example 49 | // Before: 2.0.0-alpha.1 -> After: 2.0.0-beta.1 50 | else 51 | refVersion.WithPrereleaseParsedFrom(settings.PreRelease.Value + ".1") |> Some 52 | // If the last version is a release, and user requested a stable release 53 | // Then, remove the pre-release identifier 54 | // Example: 2.0.0-beta.1 -> 2.0.0 55 | else 56 | refVersion.WithoutPrereleaseOrMetadata() |> Some 57 | 58 | else 59 | let shouldBumpMajor = 60 | commitsForRelease 61 | |> List.exists (fun commit -> commit.SemanticCommit.BreakingChange) 62 | 63 | let shouldBumpMinor = 64 | commitsForRelease 65 | |> List.exists (fun commit -> 66 | commit.SemanticCommit.Type = "feat" || commit.SemanticCommit.Type = "perf" 67 | ) 68 | 69 | let shouldBumpPatch = 70 | commitsForRelease 71 | |> List.exists (fun commit -> commit.SemanticCommit.Type = "fix") 72 | 73 | let bumpMajor () = 74 | refVersion 75 | .WithMajor(refVersion.Major + 1) 76 | .WithMinor(0) 77 | .WithPatch(0) 78 | .WithoutPrereleaseOrMetadata() 79 | 80 | let bumpMinor () = 81 | refVersion 82 | .WithMinor(refVersion.Minor + 1) 83 | .WithPatch(0) 84 | .WithoutPrereleaseOrMetadata() 85 | 86 | let bumpPatch () = 87 | refVersion.WithPatch(refVersion.Patch + 1).WithoutPrereleaseOrMetadata() 88 | 89 | // If the last version is a release, and user requested a pre-release 90 | // Then we compute the standard release version and add the pre-release identifier starting from 1 91 | // Example: 92 | // - Major bump needed: 2.0.0 -> 3.0.0-beta.1 93 | // - Minor bump needed: 2.0.0 -> 2.1.0-beta.1 94 | // - Patch bump needed: 2.0.0 -> 2.0.1-beta.1 95 | if settings.PreRelease.IsSet then 96 | let applyPreReleaseIdentifier (version: SemVersion) = 97 | version.WithPrereleaseParsedFrom(settings.PreRelease.Value + ".1") 98 | 99 | if shouldBumpMajor then 100 | bumpMajor () |> applyPreReleaseIdentifier |> Some 101 | elif shouldBumpMinor then 102 | bumpMinor () |> applyPreReleaseIdentifier |> Some 103 | elif shouldBumpPatch then 104 | bumpPatch () |> applyPreReleaseIdentifier |> Some 105 | else 106 | None 107 | 108 | // If the last version is a release, and user requested a stable release 109 | // Then we compute the standard release version 110 | // Example: 111 | // - Major bump needed: 2.0.0 -> 3.0.0 112 | // - Minor bump needed: 2.0.0 -> 2.1.0 113 | // - Patch bump needed: 2.0.0 -> 2.0.1 114 | else 115 | let removePreReleaseIdentifier (version: SemVersion) = 116 | version.WithoutPrereleaseOrMetadata() 117 | 118 | if shouldBumpMajor then 119 | bumpMajor () |> removePreReleaseIdentifier |> Some 120 | elif shouldBumpMinor then 121 | bumpMinor () |> removePreReleaseIdentifier |> Some 122 | elif shouldBumpPatch then 123 | bumpPatch () |> removePreReleaseIdentifier |> Some 124 | else 125 | None 126 | 127 | let compute 128 | (settings: GenerateSettings) 129 | (changelog: ChangelogInfo) 130 | (commitsCandidates: Git.Commit list) 131 | (commitParserConfig: CommitParserConfig) 132 | = 133 | 134 | let commitsForRelease = 135 | commitsCandidates 136 | // Parse the commit message 137 | |> List.choose (fun commit -> 138 | match Parser.tryParseCommitMessage commitParserConfig commit.RawBody with 139 | | Ok semanticCommit -> 140 | Some 141 | { 142 | OriginalCommit = commit 143 | SemanticCommit = semanticCommit 144 | } 145 | | Error error -> 146 | if settings.SkipInvalidCommit then 147 | Log.warning $"Failed to parse commit message: {error}" 148 | None 149 | else 150 | failwith error 151 | ) 152 | // Only include commits that have the type feat, fix or is marked as a breaking change 153 | |> List.filter (fun commit -> 154 | commit.SemanticCommit.Type = "feat" 155 | || commit.SemanticCommit.Type = "perf" 156 | || commit.SemanticCommit.Type = "fix" 157 | || commit.SemanticCommit.BreakingChange 158 | ) 159 | // Only keep the commits that have the tags we are looking for 160 | // or all commits if no tags are provided 161 | |> List.filter (fun commit -> 162 | // If no tags are provided, include all commits 163 | if settings.Tags.Length = 0 then 164 | true 165 | else 166 | settings.Tags 167 | |> Array.exists (fun searchedTag -> 168 | match Map.tryFind "Tag" commit.SemanticCommit.Footers with 169 | | Some tags -> tags |> List.contains searchedTag 170 | | None -> false 171 | ) 172 | ) 173 | 174 | let refVersion = changelog.LastVersion 175 | 176 | let makeBumpInfo newVersion = 177 | { 178 | NewVersion = newVersion 179 | CommitsForRelease = commitsForRelease 180 | LastCommitSha = commitsCandidates[0].Hash 181 | } 182 | 183 | // If the user forced a version, then use that version 184 | match settings.ForceVersion with 185 | | Some version -> 186 | SemVersion.Parse(version, SemVersionStyles.Strict) 187 | |> makeBumpInfo 188 | |> BumpRequired 189 | 190 | | None -> 191 | match computeVersion settings commitsForRelease refVersion with 192 | | Some newVersion -> makeBumpInfo newVersion |> BumpRequired 193 | | None -> NoVersionBumpRequired 194 | -------------------------------------------------------------------------------- /src/Generate/Types.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Generate.Types 2 | 3 | open Spectre.Console.Cli 4 | open System.ComponentModel 5 | open System.IO 6 | open Semver 7 | open EasyBuild.CommitParser.Types 8 | open System.Text.RegularExpressions 9 | 10 | type GenerateSettings() = 11 | inherit CommandSettings() 12 | 13 | [] 14 | [] 15 | member val Changelog: string = "CHANGELOG.md" with get, set 16 | 17 | [] 18 | [] 19 | member val Config: string option = None with get, set 20 | 21 | [] 22 | [] 23 | member val AllowDirty: bool = false with get, set 24 | 25 | [")>] 26 | [] 27 | member val AllowBranch: string array = [| "main" |] with get, set 28 | 29 | [")>] 30 | [] 31 | member val Tags: string array = [||] with get, set 32 | 33 | [] 34 | [] 35 | [] 36 | member val PreRelease: FlagValue = FlagValue() with get, set 37 | 38 | [")>] 39 | [] 40 | member val ForceVersion: string option = None with get, set 41 | 42 | [] 43 | [] 44 | member val SkipInvalidCommit: bool = true with get, set 45 | 46 | [] 47 | [] 48 | member val DryRun: bool = false with get, set 49 | 50 | [")>] 51 | [] 52 | member val GitHubRepo: string option = None with get, set 53 | 54 | type CommitForRelease = 55 | { 56 | OriginalCommit: Git.Commit 57 | SemanticCommit: CommitMessage 58 | } 59 | 60 | type BumpInfo = 61 | { 62 | NewVersion: SemVersion 63 | CommitsForRelease: CommitForRelease list 64 | LastCommitSha: string 65 | } 66 | 67 | type ReleaseContext = 68 | | NoVersionBumpRequired 69 | | BumpRequired of BumpInfo 70 | 71 | type ChangelogInfo = 72 | { 73 | File: FileInfo 74 | Content: string 75 | Versions: SemVersion list 76 | } 77 | 78 | member this.LastVersion = 79 | match List.tryHead this.Versions with 80 | | Some version -> version 81 | | None -> SemVersion(0, 0, 0) 82 | 83 | member this.Lines = this.Content.Replace("\r\n", "\n").Split('\n') 84 | 85 | member this.LastReleaseCommit = 86 | let changelogConfigSection = 87 | this.Lines 88 | |> Array.skipWhile (fun line -> "" <> line) 89 | |> Array.takeWhile (fun line -> "" <> line) 90 | 91 | let regex = Regex("^$") 92 | 93 | changelogConfigSection 94 | |> Array.tryPick (fun line -> 95 | let m = regex.Match(line) 96 | 97 | if m.Success then 98 | Some m.Groups.["hash"].Value 99 | else 100 | None 101 | ) 102 | -------------------------------------------------------------------------------- /src/Generate/Verify.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Generate.Verify 2 | 3 | open EasyBuild.ChangelogGen.Generate.Types 4 | open EasyBuild.ChangelogGen 5 | 6 | let branch (settings: GenerateSettings) = 7 | let currentBranchName = Git.getHeadBranchName () 8 | 9 | if Array.contains currentBranchName settings.AllowBranch then 10 | Ok() 11 | else 12 | let allowedBranch = 13 | settings.AllowBranch |> Array.map (fun b -> $"- {b}") |> String.concat "\n" 14 | 15 | Error 16 | $"""Branch '%s{currentBranchName}' is not allowed to generate the changelog. 17 | 18 | Allowed branches are: 19 | %s{allowedBranch} 20 | 21 | You can use the --allow-branch option to allow other branches.""" 22 | 23 | let dirty (settings: GenerateSettings) = 24 | if Git.isDirty () && not settings.AllowDirty then 25 | Error 26 | """Repository is dirty. Please commit or stash your changes before generating the changelog. 27 | 28 | You can use the --allow-dirty option to allow a dirty repository.""" 29 | else 30 | Ok() 31 | 32 | let resolveRemoteConfig (settings: GenerateSettings) = 33 | match settings.GitHubRepo with 34 | | Some githubRepo -> 35 | let segments = githubRepo.Split('/') |> Array.toList 36 | 37 | match segments with 38 | | [ owner; repo ] -> 39 | ({ 40 | Owner = owner 41 | Repository = repo 42 | } 43 | : Types.GithubRemoteConfig) 44 | |> Ok 45 | | _ -> 46 | Error $"""Invalid format for --github-repo option, expected format is 'owner/repo'.""" 47 | 48 | | None -> 49 | match Git.tryFindRemote () with 50 | | Ok remote -> 51 | ({ 52 | Owner = remote.Owner 53 | Repository = remote.Repository 54 | } 55 | : Types.GithubRemoteConfig) 56 | |> Ok 57 | | Error error -> Error error 58 | -------------------------------------------------------------------------------- /src/Git.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Git 3 | 4 | open System 5 | open SimpleExec 6 | open BlackFox.CommandLine 7 | open Thoth.Json.Core 8 | open Thoth.Json.Newtonsoft 9 | 10 | let getHeadBranchName () = 11 | let struct (standardOutput, _) = 12 | Command.ReadAsync( 13 | "git", 14 | CmdLine.empty 15 | |> CmdLine.appendRaw "rev-parse" 16 | |> CmdLine.appendPrefix "--abbrev-ref" "HEAD" 17 | |> CmdLine.toString 18 | ) 19 | |> Async.AwaitTask 20 | |> Async.RunSynchronously 21 | 22 | standardOutput.Trim() 23 | 24 | let isDirty () = 25 | let struct (standardOutput, _) = 26 | Command.ReadAsync( 27 | "git", 28 | CmdLine.empty 29 | |> CmdLine.appendRaw "status" 30 | |> CmdLine.appendRaw "--porcelain" 31 | |> CmdLine.toString 32 | ) 33 | |> Async.AwaitTask 34 | |> Async.RunSynchronously 35 | 36 | standardOutput.Trim().Length > 0 37 | 38 | type Commit = 39 | { 40 | Hash: string 41 | AbbrevHash: string 42 | Author: string 43 | ShortMessage: string 44 | RawBody: string 45 | } 46 | 47 | static member Decoder: Decoder = 48 | Decode.object (fun get -> 49 | { 50 | Hash = get.Required.Field "hash" Decode.string 51 | AbbrevHash = get.Required.Field "abbrev_hash" Decode.string 52 | Author = get.Required.Field "author" Decode.string 53 | ShortMessage = get.Required.Field "short_message" Decode.string 54 | RawBody = get.Required.Field "long_message" Decode.string 55 | } 56 | ) 57 | 58 | [] 59 | type GetCommitsFilter = 60 | | All 61 | | From of string 62 | 63 | let readCommit (sha1: string) = 64 | let struct (commitStdout, _) = 65 | Command.ReadAsync( 66 | "git", 67 | CmdLine.empty 68 | |> CmdLine.appendRaw "--no-pager" 69 | |> CmdLine.appendRaw "show" 70 | |> CmdLine.appendRaw 71 | """--format="{ 72 | \"hash\": \"%H\", 73 | \"abbrev_hash\": \"%h\", 74 | \"author\": \"%an\", 75 | \"short_message\": \"%s\", 76 | \"long_message\": \"%B\" 77 | }" 78 | """ 79 | |> CmdLine.appendRaw "-s" // suppress diff output 80 | |> CmdLine.appendRaw sha1 81 | |> CmdLine.toString 82 | ) 83 | |> Async.AwaitTask 84 | |> Async.RunSynchronously 85 | 86 | match Decode.fromString Commit.Decoder commitStdout with 87 | | Ok x -> x 88 | | Error e -> 89 | $"""Failed to parse JSON: 90 | 91 | {commitStdout} 92 | 93 | Error: {e}""" 94 | |> failwith 95 | 96 | let getCommits (filter: GetCommitsFilter) = 97 | let commitFilter = 98 | match filter with 99 | | GetCommitsFilter.All -> "HEAD" 100 | | GetCommitsFilter.From sha1 -> $"{sha1}..HEAD" 101 | 102 | let struct (shaStdout, _) = 103 | Command.ReadAsync( 104 | "git", 105 | CmdLine.empty 106 | |> CmdLine.appendRaw "rev-list" 107 | |> CmdLine.appendRaw commitFilter 108 | |> CmdLine.toString 109 | ) 110 | |> Async.AwaitTask 111 | |> Async.RunSynchronously 112 | 113 | shaStdout.Split('\n') 114 | |> Array.filter (fun x -> x.Length > 0) 115 | |> Array.map readCommit 116 | |> Array.toList 117 | 118 | type Remote = 119 | { 120 | Owner: string 121 | Repository: string 122 | } 123 | 124 | let private stripSuffix (suffix: string) (str: string) = 125 | if str.EndsWith(suffix) then 126 | str.Substring(0, str.Length - suffix.Length) 127 | else 128 | str 129 | 130 | // Url needs to be in the format: 131 | // https://hostname/owner/repo.git 132 | let tryGetRemoteFromUrl (url: string) = 133 | let normalizedUrl = url |> stripSuffix ".git" 134 | 135 | match Uri.TryCreate(normalizedUrl, UriKind.Absolute) with 136 | | true, uri -> 137 | let segments = 138 | uri.Segments 139 | |> Seq.map _.Trim('/') 140 | |> Seq.filter (String.IsNullOrEmpty >> not) 141 | |> Seq.toList 142 | 143 | if segments.Length < 2 then 144 | None 145 | else 146 | let owner = segments.[segments.Length - 2] 147 | let repo = segments.[segments.Length - 1] 148 | 149 | Some 150 | { 151 | Owner = owner.Trim('/') 152 | Repository = repo.Trim('/') 153 | } 154 | | false, _ -> None 155 | 156 | let tryGetRemoteFromSSH (url: string) = 157 | // Naive way to check the format 158 | if url.Contains("@") && url.Contains(":") && url.Contains("/") then 159 | let segments = 160 | // Remove the .git extension and split the url 161 | url |> stripSuffix ".git" |> _.Split(':') |> Seq.toList 162 | 163 | match segments with 164 | | _ :: owner_repo :: _ -> 165 | let segments = owner_repo.Split('/') |> Array.rev |> Array.toList 166 | 167 | match segments with 168 | | repo :: owner :: _ -> 169 | Some 170 | { 171 | Owner = owner 172 | Repository = repo 173 | } 174 | | _ -> None 175 | | _ -> None 176 | else 177 | None 178 | 179 | let tryFindRemote () = 180 | 181 | let struct (remoteStdout, _) = 182 | Command.ReadAsync( 183 | "git", 184 | CmdLine.empty 185 | |> CmdLine.appendRaw "config" 186 | |> CmdLine.appendPrefix "--get" "remote.origin.url" 187 | |> CmdLine.toString 188 | ) 189 | |> Async.AwaitTask 190 | |> Async.RunSynchronously 191 | 192 | let remoteUrl = remoteStdout.Trim() 193 | 194 | match tryGetRemoteFromUrl remoteUrl with 195 | | Some remote -> Ok remote 196 | | None -> 197 | match tryGetRemoteFromSSH remoteUrl with 198 | | Some remote -> Ok remote 199 | | None -> 200 | Error 201 | """Could not resolve the remote repository. 202 | 203 | Automatic detection expects URL returned by `git config --get remote.origin.url` to be of the form 'https://hostname/owner/repo.git' or 'git@hostname:owner/repo.git'. 204 | 205 | You can use the --github-repo option to specify the repository manually.""" 206 | -------------------------------------------------------------------------------- /src/Log.fs: -------------------------------------------------------------------------------- 1 | module Log 2 | 3 | open Spectre.Console 4 | 5 | let private output = 6 | let settings = new AnsiConsoleSettings() 7 | settings.Out <- AnsiConsoleOutput(System.Console.Error) 8 | 9 | AnsiConsole.Create(settings) 10 | 11 | let info msg = 12 | output.MarkupLine($"[deepskyblue3_1]%s{msg}[/]") 13 | 14 | let success msg = 15 | output.MarkupLine($"[green]%s{msg}[/]") 16 | 17 | let log msg = 18 | output.MarkupLine(msg) 19 | 20 | let error msg = 21 | output.MarkupLine($"[red]{msg}[/]") 22 | 23 | let warning msg = 24 | output.MarkupLine($"[yellow]{msg}[/]") 25 | -------------------------------------------------------------------------------- /src/Main.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Main 2 | 3 | open Spectre.Console.Cli 4 | open EasyBuild.ChangelogGen.Commands.Generate 5 | open EasyBuild.ChangelogGen.Commands.Version 6 | 7 | let mutable helpWasCalled = false 8 | 9 | type CustomHelperProvider(settings: ICommandAppSettings) = 10 | inherit Help.HelpProvider(settings) 11 | 12 | override _.GetUsage 13 | (model: Help.ICommandModel, command: Help.ICommandInfo) 14 | : System.Collections.Generic.IEnumerable 15 | = 16 | helpWasCalled <- true 17 | base.GetUsage(model, command) 18 | 19 | [] 20 | let main args = 21 | 22 | let app = CommandApp() 23 | 24 | app 25 | .WithDescription( 26 | "Generate changelog based on the Git history. 27 | 28 | Learn more at https://github.com/easybuild-org/EasyBuild.ChangelogGen" 29 | ) 30 | .Configure(fun config -> 31 | config.Settings.ApplicationName <- "changelog-gen" 32 | config.SetHelpProvider(CustomHelperProvider(config.Settings)) 33 | config.AddCommand("version") |> ignore 34 | ) 35 | 36 | let exitCode = app.Run(args) 37 | 38 | if helpWasCalled then 39 | // Make it easy for caller to know when help was called 40 | 100 41 | else 42 | exitCode 43 | -------------------------------------------------------------------------------- /src/Types.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Types 2 | 3 | open Thoth.Json.Core 4 | 5 | type GithubRemoteConfig = 6 | { 7 | Owner: string 8 | Repository: string 9 | } 10 | 11 | static member Decoder: Decoder = 12 | Decode.object (fun get -> 13 | { 14 | Owner = get.Required.Field "owner" Decode.string 15 | Repository = get.Required.Field "repository" Decode.string 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /src/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "dependencies": { 4 | "net6.0": { 5 | "BlackFox.CommandLine": { 6 | "type": "Direct", 7 | "requested": "[1.0.0, )", 8 | "resolved": "1.0.0", 9 | "contentHash": "dSW7uLetl021HQXKcZd1xrXPjhsXgaJ5U4tFe64DLja1KZ2Ce6QeugHvZDvLfcPkEc1ZPRF7fWv5/T+X3ThWTA==", 10 | "dependencies": { 11 | "FSharp.Core": "4.2.3" 12 | } 13 | }, 14 | "EasyBuild.CommitParser": { 15 | "type": "Direct", 16 | "requested": "[3.0.0, )", 17 | "resolved": "3.0.0", 18 | "contentHash": "4N8zQ3nXEdmsp8txFrrxz2SbB6ZCQ6/OYDdgrU8G9brUY17s6DYEkPL0tnWdGfjDPufBPO3Uvz4yIB8HonkY4w==", 19 | "dependencies": { 20 | "FSharp.Core": "7.0.300", 21 | "FsToolkit.ErrorHandling": "4.18.0", 22 | "Thoth.Json.Newtonsoft": "0.1.0" 23 | } 24 | }, 25 | "EasyBuild.PackageReleaseNotes.Tasks": { 26 | "type": "Direct", 27 | "requested": "[2.0.0, )", 28 | "resolved": "2.0.0", 29 | "contentHash": "jebz09lxa6pEJzft9Tr/PSNt5r7AVt5xmX3g4Ra+nLU1qtSrOh+hFgsywGn7fVME3LpbzhlX+2J8F7G9y3PxUQ==" 30 | }, 31 | "FSharp.Core": { 32 | "type": "Direct", 33 | "requested": "[8.0.101, )", 34 | "resolved": "8.0.101", 35 | "contentHash": "sOLz3O4BOxnTKfd5OChdRmDUy4Id0GfoEClRG4nzIod8LY1LJZcNyygKAV0A78XOLh8yvhA5hsDYKZXGCR9blw==" 36 | }, 37 | "FsToolkit.ErrorHandling": { 38 | "type": "Direct", 39 | "requested": "[4.18.0, )", 40 | "resolved": "4.18.0", 41 | "contentHash": "cGtOP6lWcnLcXiLTGZLHi+8JAyuUDjGhZOmJWnZfd5aPCUIyL+DqUIwmfEGkUk3j/gpcchLDk9BNwUTc1oM30w==", 42 | "dependencies": { 43 | "FSharp.Core": "7.0.300" 44 | } 45 | }, 46 | "Semver": { 47 | "type": "Direct", 48 | "requested": "[2.3.0, )", 49 | "resolved": "2.3.0", 50 | "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" 51 | }, 52 | "SimpleExec": { 53 | "type": "Direct", 54 | "requested": "[12.0.0, )", 55 | "resolved": "12.0.0", 56 | "contentHash": "ptxlWtxC8vM6Y6e3h9ZTxBBkOWnWrm/Sa1HT+2i1xcXY3Hx2hmKDZP5RShPf8Xr9D+ivlrXNy57ktzyH8kyt+Q==" 57 | }, 58 | "Spectre.Console.Cli": { 59 | "type": "Direct", 60 | "requested": "[0.49.0, )", 61 | "resolved": "0.49.0", 62 | "contentHash": "841g7PhuJFwoCatKKVoIRDaylmcaTxEzTzq7+rGHyVCBUqL7iOH0c5AsGnjgBMzhRDnUUWkP47Ho9WWfqMVqAA==", 63 | "dependencies": { 64 | "Spectre.Console": "0.49.0" 65 | } 66 | }, 67 | "Fable.Core": { 68 | "type": "Transitive", 69 | "resolved": "4.1.0", 70 | "contentHash": "NISAbAVGEcvH2s+vHLSOCzh98xMYx4aIadWacQdWPcQLploxpSQXLEe9SeszUBhbHa73KMiKREsH4/W3q4A4iA==" 71 | }, 72 | "Newtonsoft.Json": { 73 | "type": "Transitive", 74 | "resolved": "13.0.1", 75 | "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" 76 | }, 77 | "Spectre.Console": { 78 | "type": "Transitive", 79 | "resolved": "0.49.0", 80 | "contentHash": "1s+hhYq5fcrqCvZhrNOPehAmCZJM6cjro85g1Qirvmm1tys+/sfFJgePCGcxr+S/nONZ/lCDTflPda3C+WlBdg==" 81 | }, 82 | "Thoth.Json.Core": { 83 | "type": "Transitive", 84 | "resolved": "0.1.0", 85 | "contentHash": "hIo4bdnbG2BOmCrUTHhScgn0aTnlGPQtmO0KiCklSHduEQV3r7SjwscvjjcbfXyc0nuPV6UOg05KHMUgiIeXjQ==", 86 | "dependencies": { 87 | "FSharp.Core": "5.0.0", 88 | "Fable.Core": "4.1.0" 89 | } 90 | }, 91 | "Thoth.Json.Newtonsoft": { 92 | "type": "Transitive", 93 | "resolved": "0.1.0", 94 | "contentHash": "i64dDASv8UY8oPNwvcF7UTLvXYL2C3PaTeNQ8s8Woy/pNTKud616//+0V858wcS2PyunVnyQeuiND4bfvMujhQ==", 95 | "dependencies": { 96 | "FSharp.Core": "5.0.0", 97 | "Fable.Core": "4.1.0", 98 | "Newtonsoft.Json": "13.0.1", 99 | "Thoth.Json.Core": "0.1.0" 100 | } 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /tests/Changelog.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Tests.Changelog 2 | 3 | open Workspace 4 | open Expecto 5 | open Tests.Utils 6 | open EasyBuild.ChangelogGen.Types 7 | open EasyBuild.ChangelogGen.Generate 8 | open EasyBuild.ChangelogGen.Generate.Types 9 | open EasyBuild.CommitParser 10 | open EasyBuild.CommitParser.Types 11 | 12 | open type TestHelper 13 | 14 | let private findVersionsTests = 15 | testList 16 | "Changelog.findVersions" 17 | [ 18 | test "works if no versions are found" { 19 | let actual = Changelog.findVersions "Some content" 20 | 21 | Expect.equal actual [] 22 | } 23 | 24 | test "works for different type of versions syntax" { 25 | let actual = 26 | Changelog.findVersions 27 | """ 28 | ## 8.0.0 - 2021-01-01 29 | ## [7.0.0] - 2021-01-01 30 | ## v6.0.0 - 2021-01-01 31 | ## [v5.0.0] - 2021-01-01 32 | ## 4.0.0 33 | ## [3.0.0] 34 | ## v2.0.0 35 | ## [v1.0.0] 36 | """ 37 | 38 | Expect.equal 39 | actual 40 | [ 41 | Semver.SemVersion(8, 0, 0) 42 | Semver.SemVersion(7, 0, 0) 43 | Semver.SemVersion(6, 0, 0) 44 | Semver.SemVersion(5, 0, 0) 45 | Semver.SemVersion(4, 0, 0) 46 | Semver.SemVersion(3, 0, 0) 47 | Semver.SemVersion(2, 0, 0) 48 | Semver.SemVersion(1, 0, 0) 49 | ] 50 | } 51 | 52 | test "only report real versions and not similar looking strings" { 53 | let actual = 54 | Changelog.findVersions 55 | """ 56 | ## 8.0.0 - 2021-01-01 57 | 58 | This is not a version: ## 8.0.0 59 | 60 | ## 8.0.0 61 | """ 62 | 63 | Expect.equal actual [ Semver.SemVersion(8, 0, 0) ] 64 | } 65 | ] 66 | 67 | let private loadTests = 68 | testList 69 | "Changelog.load" 70 | [ 71 | test "works if no changelog file exists" { 72 | let actual = 73 | let settings = GenerateSettings(Changelog = "THIS_CHANGELOG_DOENST_EXIST.md") 74 | 75 | Changelog.load settings 76 | 77 | match actual with 78 | | Ok actual -> 79 | Expect.equal actual.Content Changelog.EMPTY_CHANGELOG 80 | Expect.equal actual.LastVersion (Semver.SemVersion(0, 0, 0)) 81 | | Error _ -> failwith "Expected Ok" 82 | } 83 | 84 | test "works if changelog file exists" { 85 | let actual = 86 | let settings = GenerateSettings(Changelog = Workspace.``valid_changelog.md``) 87 | 88 | Changelog.load settings 89 | 90 | match actual with 91 | | Ok actual -> 92 | Expect.isNotEmpty actual.Content 93 | Expect.equal actual.LastVersion (Semver.SemVersion(1, 0, 0)) 94 | | Error _ -> failwith "Expected Ok" 95 | } 96 | 97 | test "works for changelog without version" { 98 | let actual = 99 | let settings = GenerateSettings(Changelog = Workspace.``valid_no_version.md``) 100 | 101 | Changelog.load settings 102 | 103 | match actual with 104 | | Ok actual -> 105 | Expect.isNotEmpty actual.Content 106 | Expect.equal actual.LastVersion (Semver.SemVersion(0, 0, 0)) 107 | | Error _ -> failwith "Expected Ok" 108 | } 109 | ] 110 | 111 | let private tryFindAdditionalChangelogContentTests = 112 | testList 113 | "Changelog.tryFindAdditionalChangelogContent" 114 | [ 115 | test "works if no additional content is found" { 116 | let actual = Changelog.tryFindAdditionalChangelogContent "Some content" 117 | 118 | Expect.equal actual [] 119 | } 120 | 121 | test "works if additional content is found" { 122 | let actual = 123 | Changelog.tryFindAdditionalChangelogContent 124 | """Some content 125 | 126 | === changelog === 127 | This goes into the changelog 128 | === changelog === 129 | """ 130 | 131 | Expect.equal actual [ [ "This goes into the changelog" ] ] 132 | } 133 | 134 | test "works for multiple additional content blocks" { 135 | let actual = 136 | Changelog.tryFindAdditionalChangelogContent 137 | """Some content 138 | 139 | === changelog === 140 | This goes into the changelog 141 | === changelog === 142 | 143 | Some more content 144 | 145 | === changelog === 146 | This goes into the changelog as well 147 | === changelog === 148 | """ 149 | 150 | Expect.equal 151 | actual 152 | [ 153 | [ "This goes into the changelog" ] 154 | [ "This goes into the changelog as well" ] 155 | ] 156 | } 157 | ] 158 | 159 | let private gitCommitToCommitForRelease (commit: Git.Commit) = 160 | { 161 | OriginalCommit = commit 162 | SemanticCommit = 163 | Parser.tryParseCommitMessage CommitParserConfig.Default commit.RawBody 164 | |> function 165 | | Ok semanticCommit -> semanticCommit 166 | | Error error -> failwith error 167 | } 168 | 169 | let private generateNewVersionSectionTests = 170 | testList 171 | "Changelog.generateNewVersionSection" 172 | [ 173 | testMarkdown ( 174 | "works for feat type commit", 175 | (fun _ -> 176 | Changelog.generateNewVersionSection 177 | { 178 | Owner = "owner" 179 | Repository = "repository" 180 | } 181 | (Some "fefd5e0bf242e034f86ad23a886e2d71ded4f7bb") 182 | { 183 | NewVersion = Semver.SemVersion(1, 0, 0) 184 | CommitsForRelease = 185 | [ 186 | Git.Commit.Create( 187 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 188 | "feat: Add feature" 189 | ) 190 | |> gitCommitToCommitForRelease 191 | Git.Commit.Create( 192 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 193 | "feat: Add another feature" 194 | ) 195 | |> gitCommitToCommitForRelease 196 | ] 197 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 198 | } 199 | ) 200 | ) 201 | 202 | testMarkdown ( 203 | "works for fix type commit", 204 | (fun _ -> 205 | Changelog.generateNewVersionSection 206 | { 207 | Owner = "owner" 208 | Repository = "repository" 209 | } 210 | (Some "fefd5e0bf242e034f86ad23a886e2d71ded4f7bb") 211 | { 212 | NewVersion = Semver.SemVersion(1, 0, 0) 213 | CommitsForRelease = 214 | [ 215 | Git.Commit.Create( 216 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 217 | "fix: Fix bug" 218 | ) 219 | |> gitCommitToCommitForRelease 220 | Git.Commit.Create( 221 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 222 | "fix: Fix another bug" 223 | ) 224 | |> gitCommitToCommitForRelease 225 | ] 226 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 227 | } 228 | ) 229 | ) 230 | 231 | testMarkdown ( 232 | "breaking change are going into their own section if configured", 233 | (fun _ -> 234 | Changelog.generateNewVersionSection 235 | { 236 | Owner = "owner" 237 | Repository = "repository" 238 | } 239 | (Some "fefd5e0bf242e034f86ad23a886e2d71ded4f7bb") 240 | { 241 | NewVersion = Semver.SemVersion(1, 0, 0) 242 | CommitsForRelease = 243 | [ 244 | Git.Commit.Create( 245 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 246 | "feat: Add feature" 247 | ) 248 | |> gitCommitToCommitForRelease 249 | Git.Commit.Create( 250 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 251 | "fix: Fix bug" 252 | ) 253 | |> gitCommitToCommitForRelease 254 | Git.Commit.Create( 255 | "9156258d463ba78ac21ebb5fcd32147657bfe86f", 256 | "fix!: Fix bug via breaking change" 257 | ) 258 | |> gitCommitToCommitForRelease 259 | Git.Commit.Create( 260 | "4057b1a703845efcdf2f3b49240dd79d8ce7150e", 261 | "feat!: Add another feature via breaking change" 262 | ) 263 | |> gitCommitToCommitForRelease 264 | ] 265 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 266 | } 267 | ) 268 | ) 269 | 270 | testMarkdown ( 271 | "breaking change stays in their original group if they don't have a dedicated group", 272 | (fun _ -> 273 | Changelog.generateNewVersionSection 274 | { 275 | Owner = "owner" 276 | Repository = "repository" 277 | } 278 | (Some "fefd5e0bf242e034f86ad23a886e2d71ded4f7bb") 279 | { 280 | NewVersion = Semver.SemVersion(1, 0, 0) 281 | CommitsForRelease = 282 | [ 283 | Git.Commit.Create( 284 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 285 | "feat: Add feature" 286 | ) 287 | |> gitCommitToCommitForRelease 288 | Git.Commit.Create( 289 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 290 | "fix: Fix bug" 291 | ) 292 | |> gitCommitToCommitForRelease 293 | Git.Commit.Create( 294 | "9156258d463ba78ac21ebb5fcd32147657bfe86f", 295 | "fix!: Fix bug via breaking change" 296 | ) 297 | |> gitCommitToCommitForRelease 298 | Git.Commit.Create( 299 | "4057b1a703845efcdf2f3b49240dd79d8ce7150e", 300 | "feat!: Add another feature via breaking change" 301 | ) 302 | |> gitCommitToCommitForRelease 303 | 304 | Git.Commit.Create( 305 | "d4212797a454d591068e18480843c87766b0291e", 306 | "perf!: performance improvement via breaking change" 307 | ) 308 | |> gitCommitToCommitForRelease 309 | Git.Commit.Create( 310 | "287a4ee6f89ab84e52283d69f4304ece97e5e87c", 311 | "perf: performance improvement" 312 | ) 313 | |> gitCommitToCommitForRelease 314 | ] 315 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 316 | } 317 | ) 318 | ) 319 | 320 | testMarkdown ( 321 | "only commit of type feat, perf and fix are included in the changelog", 322 | (fun _ -> 323 | Changelog.generateNewVersionSection 324 | { 325 | Owner = "owner" 326 | Repository = "repository" 327 | } 328 | (Some "fefd5e0bf242e034f86ad23a886e2d71ded4f7bb") 329 | { 330 | NewVersion = Semver.SemVersion(1, 0, 0) 331 | CommitsForRelease = 332 | [ 333 | Git.Commit.Create( 334 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 335 | "feat: Add feature" 336 | ) 337 | |> gitCommitToCommitForRelease 338 | Git.Commit.Create( 339 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 340 | "fix: Fix bug" 341 | ) 342 | |> gitCommitToCommitForRelease 343 | Git.Commit.Create( 344 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 345 | "perf: Performance improvement" 346 | ) 347 | |> gitCommitToCommitForRelease 348 | Git.Commit.Create( 349 | "9156258d463ba78ac21ebb5fcd32147657bfe86f", 350 | "chore: Do some chore" 351 | ) 352 | |> gitCommitToCommitForRelease 353 | Git.Commit.Create( 354 | "4057b1a703845efcdf2f3b49240dd79d8ce7150e", 355 | "style: Fix style" 356 | ) 357 | |> gitCommitToCommitForRelease 358 | ] 359 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 360 | } 361 | ) 362 | ) 363 | 364 | testMarkdown ( 365 | "include changelog additional data when present", 366 | (fun _ -> 367 | Changelog.generateNewVersionSection 368 | { 369 | Owner = "owner" 370 | Repository = "repository" 371 | } 372 | (Some "fefd5e0bf242e034f86ad23a886e2d71ded4f7bb") 373 | { 374 | NewVersion = Semver.SemVersion(1, 0, 0) 375 | CommitsForRelease = 376 | [ 377 | Git.Commit.Create( 378 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 379 | """feat: Add feature 380 | 381 | === changelog === 382 | ```fs 383 | let upper (s: string) = s.ToUpper() 384 | ``` 385 | === changelog === 386 | 387 | === changelog === 388 | This is a list of changes: 389 | 390 | * Added upper function 391 | * Added lower function 392 | === changelog === 393 | """ 394 | ) 395 | |> gitCommitToCommitForRelease 396 | Git.Commit.Create( 397 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 398 | "fix: Fix bug" 399 | ) 400 | |> gitCommitToCommitForRelease 401 | ] 402 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 403 | } 404 | ) 405 | ) 406 | 407 | testMarkdown ( 408 | "compare link is not generated if no previous release sha is provided", 409 | (fun _ -> 410 | Changelog.generateNewVersionSection 411 | { 412 | Owner = "owner" 413 | Repository = "repository" 414 | } 415 | None 416 | { 417 | NewVersion = Semver.SemVersion(1, 0, 0) 418 | CommitsForRelease = 419 | [ 420 | Git.Commit.Create( 421 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 422 | "feat: Add feature" 423 | ) 424 | |> gitCommitToCommitForRelease 425 | Git.Commit.Create( 426 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 427 | "feat: Add another feature" 428 | ) 429 | |> gitCommitToCommitForRelease 430 | ] 431 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 432 | } 433 | ) 434 | ) 435 | 436 | testMarkdown ( 437 | "commits are ordered by scope", 438 | (fun _ -> 439 | Changelog.generateNewVersionSection 440 | { 441 | Owner = "owner" 442 | Repository = "repository" 443 | } 444 | None 445 | { 446 | NewVersion = Semver.SemVersion(1, 0, 0) 447 | CommitsForRelease = 448 | [ 449 | Git.Commit.Create( 450 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 451 | "feat(js): Add feature #2" 452 | ) 453 | |> gitCommitToCommitForRelease 454 | Git.Commit.Create( 455 | "21033aae357447dbfac30557a2dee0c4b5b03f68", 456 | "feat(js): Add feature #1" 457 | ) 458 | |> gitCommitToCommitForRelease 459 | Git.Commit.Create( 460 | "be429a973ac2f6d1c009b7efcd15360c9e585450", 461 | "feat(js): JavaScript is awesome" 462 | ) 463 | |> gitCommitToCommitForRelease 464 | Git.Commit.Create( 465 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 466 | "feat(rust): Rust is awesome" 467 | ) 468 | |> gitCommitToCommitForRelease 469 | Git.Commit.Create( 470 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 471 | "feat(all): All Fable targets are awesome" 472 | ) 473 | |> gitCommitToCommitForRelease 474 | ] 475 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 476 | } 477 | ) 478 | ) 479 | ] 480 | 481 | let private updateChangelogWithNewVersionTests = 482 | testList 483 | "Changelog.updateChangelogWithNewVersion" 484 | [ 485 | testMarkdown ( 486 | "works if metadata was existing", 487 | (fun _ -> 488 | let changelogInfo = 489 | let settings = 490 | GenerateSettings(Changelog = Workspace.``valid_changelog.md``) 491 | 492 | match Changelog.load settings with 493 | | Ok changelogInfo -> changelogInfo 494 | | Error _ -> failwith "Expected Ok" 495 | 496 | Changelog.updateWithNewVersion 497 | { 498 | Owner = "owner" 499 | Repository = "repository" 500 | } 501 | { 502 | NewVersion = Semver.SemVersion(1, 1, 0) 503 | CommitsForRelease = 504 | [ 505 | Git.Commit.Create( 506 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 507 | "fix: Fix bug" 508 | ) 509 | |> gitCommitToCommitForRelease 510 | Git.Commit.Create( 511 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 512 | "fix: Fix another bug" 513 | ) 514 | |> gitCommitToCommitForRelease 515 | Git.Commit.Create( 516 | "fef5f479d65172bd385b781bbed83f6eee2a32c6", 517 | "feat: Add feature" 518 | ) 519 | |> gitCommitToCommitForRelease 520 | Git.Commit.Create( 521 | "46d380257c08fe1f74e4596b8720d71a39f6e629", 522 | "feat: Add another feature" 523 | ) 524 | |> gitCommitToCommitForRelease 525 | ] 526 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 527 | } 528 | changelogInfo 529 | 530 | ) 531 | ) 532 | 533 | testMarkdown ( 534 | "works if metadata was not existing", 535 | (fun _ -> 536 | let changelogInfo = 537 | let settings = 538 | GenerateSettings( 539 | Changelog = Workspace.``valid_changelog_no_metadata.md`` 540 | ) 541 | 542 | match Changelog.load settings with 543 | | Ok changelogInfo -> changelogInfo 544 | | Error _ -> failwith "Expected Ok" 545 | 546 | Changelog.updateWithNewVersion 547 | { 548 | Owner = "owner" 549 | Repository = "repository" 550 | } 551 | { 552 | NewVersion = Semver.SemVersion(1, 1, 0) 553 | CommitsForRelease = 554 | [ 555 | Git.Commit.Create( 556 | "0b1899bb03d3eb86a30c84aa4c66c037527fbd14", 557 | "fix: Fix bug" 558 | ) 559 | |> gitCommitToCommitForRelease 560 | Git.Commit.Create( 561 | "2a6f3b3403aaa629de6e65558448b37f126f8e86", 562 | "fix: Fix another bug" 563 | ) 564 | |> gitCommitToCommitForRelease 565 | Git.Commit.Create( 566 | "fef5f479d65172bd385b781bbed83f6eee2a32c6", 567 | "feat: Add feature" 568 | ) 569 | |> gitCommitToCommitForRelease 570 | Git.Commit.Create( 571 | "46d380257c08fe1f74e4596b8720d71a39f6e629", 572 | "feat: Add another feature" 573 | ) 574 | |> gitCommitToCommitForRelease 575 | ] 576 | LastCommitSha = "0b1899bb03d3eb86a30c84aa4c66c037527fbd14" 577 | } 578 | changelogInfo 579 | 580 | ) 581 | ) 582 | ] 583 | 584 | let tests = 585 | testList 586 | "Changelog" 587 | [ 588 | findVersionsTests 589 | loadTests 590 | tryFindAdditionalChangelogContentTests 591 | generateNewVersionSectionTests 592 | updateChangelogWithNewVersionTests 593 | ] 594 | -------------------------------------------------------------------------------- /tests/EasyBuild.ChangelogGen.Tests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/Git.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Tests.Git 2 | 3 | open Expecto 4 | open Tests.Utils 5 | 6 | let tests = 7 | testList 8 | "Git" 9 | [ 10 | // We can't test all the Git functions, because: 11 | // - getHeadBranchName: we can't garantee the branch name 12 | // - isDirty: we can't garantee the repository is clean 13 | // - getCommits(All): number of commits change over time 14 | // - getCommits(From): number of commits change over time 15 | // 16 | // But we still test some of the function to try minimize the risk of bugs and regressions 17 | 18 | test "readCommit works" { 19 | let actual = Git.readCommit "5e74c5f6ccd3ef71bbfc58bae943333460ad13ee" 20 | 21 | let expected: Git.Commit = 22 | { 23 | Hash = "5e74c5f6ccd3ef71bbfc58bae943333460ad13ee" 24 | AbbrevHash = "5e74c5f" 25 | Author = "Maxime Mangel" 26 | ShortMessage = 27 | "chore: Move test to Fable.Pyxpecto to prepare for Fable support in the future + and more low level Api" 28 | RawBody = 29 | "chore: Move test to Fable.Pyxpecto to prepare for Fable support in the future + and more low level Api 30 | " 31 | } 32 | 33 | Expect.equal actual expected 34 | } 35 | 36 | testList 37 | "tryGetRemoteFromUrl" 38 | [ 39 | test "works with https" { 40 | let actual = Git.tryGetRemoteFromUrl "https://github.com/owner/repo.git" 41 | 42 | let expected: Git.Remote = 43 | { 44 | Owner = "owner" 45 | Repository = "repo" 46 | } 47 | 48 | Expect.equal actual (Some expected) 49 | } 50 | 51 | test "works even without .git suffix" { 52 | let actual = Git.tryGetRemoteFromUrl "https://github.com/owner/repo" 53 | 54 | let expected: Git.Remote = 55 | { 56 | Owner = "owner" 57 | Repository = "repo" 58 | } 59 | 60 | Expect.equal actual (Some expected) 61 | } 62 | 63 | test "returns None when url is invalid" { 64 | let actual = Git.tryGetRemoteFromUrl "https://github.com/missing-segments" 65 | 66 | Expect.equal actual None 67 | } 68 | ] 69 | 70 | testList 71 | "tryGetRemoteFromSSH" 72 | [ 73 | test "works with ssh" { 74 | let actual = Git.tryGetRemoteFromSSH "git@github.com:owner/repo.git" 75 | 76 | let expected: Git.Remote = 77 | { 78 | Owner = "owner" 79 | Repository = "repo" 80 | } 81 | 82 | Expect.equal actual (Some expected) 83 | } 84 | 85 | test "works even without .git suffix" { 86 | let actual = Git.tryGetRemoteFromSSH "git@github.com:owner/repo" 87 | 88 | let expected: Git.Remote = 89 | { 90 | Owner = "owner" 91 | Repository = "repo" 92 | } 93 | 94 | Expect.equal actual (Some expected) 95 | } 96 | 97 | test "returns None when url is invalid" { 98 | Expect.equal (Git.tryGetRemoteFromSSH "github.com:owner/repo.git") None 99 | Expect.equal (Git.tryGetRemoteFromSSH "git@github.comowner/repo.git") None 100 | Expect.equal (Git.tryGetRemoteFromSSH "git@github.com:ownerrepo.git") None 101 | } 102 | 103 | ] 104 | ] 105 | -------------------------------------------------------------------------------- /tests/Main.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Tests.Main 2 | 3 | open Expecto 4 | 5 | [] 6 | let allTests = 7 | testList "All Tests" [ 8 | ReleaseContext.tests 9 | Git.tests 10 | Changelog.tests 11 | Verify.tests 12 | ] 13 | 14 | [] 15 | let main argv = 16 | runTestsWithCLIArgs [] Array.empty allTests 17 | -------------------------------------------------------------------------------- /tests/ReleaseContext.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Tests.ReleaseContext 2 | 3 | open Expecto 4 | open Tests.Utils 5 | open EasyBuild.ChangelogGen.Generate 6 | open EasyBuild.ChangelogGen.Generate.Types 7 | open System.IO 8 | open Semver 9 | open EasyBuild.CommitParser 10 | open EasyBuild.CommitParser.Types 11 | open FsToolkit.ErrorHandling 12 | open Spectre.Console.Cli 13 | 14 | [] 15 | let STANDARD_CHANGELOG = 16 | """# Changelog 17 | 18 | All notable changes to this project will be documented in this file. 19 | 20 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 21 | 22 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 23 | 24 | 25 | 26 | 27 | ## 0.0.0 28 | 29 | 33 | """ 34 | 35 | let private computeTests = 36 | testList 37 | "compute" 38 | [ 39 | test "No version bump required if no commits" { 40 | let defaultGenerateSettings = GenerateSettings(Changelog = "CHANGELOG.md") 41 | 42 | let changelogInfo = 43 | { 44 | File = FileInfo(Path.GetTempFileName()) 45 | Content = STANDARD_CHANGELOG 46 | Versions = [ SemVersion(0, 0, 0) ] 47 | } 48 | 49 | let actual = 50 | ReleaseContext.compute 51 | defaultGenerateSettings 52 | changelogInfo 53 | [] 54 | CommitParserConfig.Default 55 | 56 | Expect.equal actual NoVersionBumpRequired 57 | } 58 | 59 | test "No version bump required if no commits have a suitable type" { 60 | 61 | let defaultGenerateSettings = GenerateSettings(Changelog = "CHANGELOG.md") 62 | 63 | let changelogInfo = 64 | { 65 | File = FileInfo(Path.GetTempFileName()) 66 | Content = STANDARD_CHANGELOG 67 | Versions = [ SemVersion(0, 0, 0) ] 68 | } 69 | 70 | let commits: Git.Commit list = 71 | [ 72 | Git.Commit.Create( 73 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 74 | "chore: update dependencies" 75 | ) 76 | Git.Commit.Create( 77 | "43c60e4fc9585a9f235ab6a6dd97c4c1cf945e46", 78 | "test: add a lot of tests" 79 | ) 80 | Git.Commit.Create( 81 | "f3c60e4fc9585a9f235ab6a6dd97c4c1cf945e46", 82 | "style: fix formatting" 83 | ) 84 | Git.Commit.Create( 85 | "24da54e481726924b8cb03c28fe0821141883c28", 86 | "refactor: refactor code" 87 | ) 88 | Git.Commit.Create( 89 | "e468776fc99ec895bf6942c8e2f16d02bbbd6e61", 90 | "docs: update documentation" 91 | ) 92 | Git.Commit.Create( 93 | "95a0f02adc4a69e2d3f516cb11c1be8a4ef5c803", 94 | "ci: update CI/CD configuration" 95 | ) 96 | ] 97 | 98 | let actual = 99 | ReleaseContext.compute 100 | defaultGenerateSettings 101 | changelogInfo 102 | commits 103 | CommitParserConfig.Default 104 | 105 | Expect.equal actual NoVersionBumpRequired 106 | } 107 | 108 | test "If commit is of type feat bump minor" { 109 | let defaultGenerateSettings = GenerateSettings(Changelog = "CHANGELOG.md") 110 | 111 | let changelogInfo = 112 | { 113 | File = FileInfo(Path.GetTempFileName()) 114 | Content = STANDARD_CHANGELOG 115 | Versions = [ SemVersion(0, 0, 0) ] 116 | } 117 | 118 | let commits: Git.Commit list = 119 | [ 120 | Git.Commit.Create( 121 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 122 | "feat: add a new feature" 123 | ) 124 | ] 125 | 126 | let actual = 127 | ReleaseContext.compute 128 | defaultGenerateSettings 129 | changelogInfo 130 | commits 131 | CommitParserConfig.Default 132 | 133 | let expected = 134 | { 135 | NewVersion = SemVersion(0, 1, 0) 136 | CommitsForRelease = 137 | [ 138 | { 139 | OriginalCommit = commits[0] 140 | SemanticCommit = 141 | Parser.tryParseCommitMessage 142 | CommitParserConfig.Default 143 | commits[0].RawBody 144 | |> Result.valueOr failwith 145 | } 146 | ] 147 | LastCommitSha = "49c0699af98a67f1e8efcac8b1467b283a244aa8" 148 | } 149 | |> BumpRequired 150 | 151 | Expect.equal actual expected 152 | } 153 | 154 | test "If commit is of type perf bump minor" { 155 | let defaultGenerateSettings = GenerateSettings(Changelog = "CHANGELOG.md") 156 | 157 | let changelogInfo = 158 | { 159 | File = FileInfo(Path.GetTempFileName()) 160 | Content = STANDARD_CHANGELOG 161 | Versions = [ SemVersion(0, 0, 0) ] 162 | } 163 | 164 | let commits: Git.Commit list = 165 | [ 166 | Git.Commit.Create( 167 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 168 | "perf: i am speed !!!" 169 | ) 170 | ] 171 | 172 | let actual = 173 | ReleaseContext.compute 174 | defaultGenerateSettings 175 | changelogInfo 176 | commits 177 | CommitParserConfig.Default 178 | 179 | let expected = 180 | { 181 | NewVersion = SemVersion(0, 1, 0) 182 | CommitsForRelease = 183 | [ 184 | { 185 | OriginalCommit = commits[0] 186 | SemanticCommit = 187 | Parser.tryParseCommitMessage 188 | CommitParserConfig.Default 189 | commits[0].RawBody 190 | |> Result.valueOr failwith 191 | } 192 | ] 193 | LastCommitSha = "49c0699af98a67f1e8efcac8b1467b283a244aa8" 194 | } 195 | |> BumpRequired 196 | 197 | Expect.equal actual expected 198 | } 199 | 200 | test "If commit is of type fix bump patch" { 201 | let defaultGenerateSettings = GenerateSettings(Changelog = "CHANGELOG.md") 202 | 203 | let changelogInfo = 204 | { 205 | File = FileInfo(Path.GetTempFileName()) 206 | Content = STANDARD_CHANGELOG 207 | Versions = [ SemVersion(0, 0, 0) ] 208 | } 209 | 210 | let commits: Git.Commit list = 211 | [ 212 | Git.Commit.Create( 213 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 214 | "fix: fix a bug" 215 | ) 216 | ] 217 | 218 | let actual = 219 | ReleaseContext.compute 220 | defaultGenerateSettings 221 | changelogInfo 222 | commits 223 | CommitParserConfig.Default 224 | 225 | let expected = 226 | { 227 | NewVersion = SemVersion(0, 0, 1) 228 | CommitsForRelease = 229 | [ 230 | { 231 | OriginalCommit = commits[0] 232 | SemanticCommit = 233 | Parser.tryParseCommitMessage 234 | CommitParserConfig.Default 235 | commits[0].RawBody 236 | |> Result.valueOr failwith 237 | } 238 | ] 239 | LastCommitSha = "49c0699af98a67f1e8efcac8b1467b283a244aa8" 240 | } 241 | |> BumpRequired 242 | 243 | Expect.equal actual expected 244 | } 245 | 246 | test "If --force-version is set, use that version" { 247 | let defaultGenerateSettings = 248 | GenerateSettings(Changelog = "CHANGELOG.md", ForceVersion = Some "4.9.3") 249 | 250 | let changelogInfo = 251 | { 252 | File = FileInfo(Path.GetTempFileName()) 253 | Content = STANDARD_CHANGELOG 254 | Versions = [ SemVersion(0, 0, 0) ] 255 | } 256 | 257 | let commits: Git.Commit list = 258 | [ 259 | Git.Commit.Create( 260 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 261 | "feat: add a new feature" 262 | ) 263 | Git.Commit.Create( 264 | "43c60e4fc9585a9f235ab6a6dd97c4c1cf945e46", 265 | "fix: fix a bug" 266 | ) 267 | ] 268 | 269 | let actual = 270 | ReleaseContext.compute 271 | defaultGenerateSettings 272 | changelogInfo 273 | commits 274 | CommitParserConfig.Default 275 | 276 | match actual with 277 | | BumpRequired { NewVersion = version } -> 278 | Expect.equal version (SemVersion(4, 9, 3)) 279 | | _ -> failtest "Expected BumpRequired" 280 | } 281 | 282 | test "If tag filter is set, only include commits with one of the requested tag" { 283 | let defaultGenerateSettings = 284 | GenerateSettings(Changelog = "CHANGELOG.md", Tags = [| "converter" |]) 285 | 286 | let changelogInfo = 287 | { 288 | File = FileInfo(Path.GetTempFileName()) 289 | Content = STANDARD_CHANGELOG 290 | Versions = [ SemVersion(0, 0, 0) ] 291 | } 292 | 293 | let commits: Git.Commit list = 294 | [ 295 | Git.Commit.Create( 296 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 297 | "fix: fix a bug", 298 | "fix: fix a bug 299 | 300 | Tag: converter" 301 | ) 302 | Git.Commit.Create( 303 | "43c60e4fc9585a9f235ab6a6dd97c4c1cf945e46", 304 | "feat: add a new feature", 305 | "feat: add a new feature 306 | 307 | Tag: cli" 308 | ) 309 | Git.Commit.Create( 310 | "b7eafe7744e4738d9578c09e1d128bbb2f5c40d3", 311 | "feat: add another feature", 312 | "feat: add another feature 313 | 314 | Tag: cli" 315 | ) 316 | ] 317 | 318 | let actual = 319 | ReleaseContext.compute 320 | defaultGenerateSettings 321 | changelogInfo 322 | commits 323 | CommitParserConfig.Default 324 | 325 | let expected = 326 | { 327 | NewVersion = SemVersion(0, 0, 1) 328 | CommitsForRelease = 329 | [ 330 | { 331 | OriginalCommit = commits[0] 332 | SemanticCommit = 333 | Parser.tryParseCommitMessage 334 | CommitParserConfig.Default 335 | commits[0].RawBody 336 | |> Result.valueOr failwith 337 | } 338 | ] 339 | LastCommitSha = "49c0699af98a67f1e8efcac8b1467b283a244aa8" 340 | } 341 | |> BumpRequired 342 | 343 | Expect.equal actual expected 344 | } 345 | 346 | test "several tags can be provided" { 347 | let defaultGenerateSettings = 348 | GenerateSettings( 349 | Changelog = "CHANGELOG.md", 350 | Tags = 351 | [| 352 | "converter" 353 | "cli" 354 | |] 355 | ) 356 | 357 | let changelogInfo = 358 | { 359 | File = FileInfo(Path.GetTempFileName()) 360 | Content = STANDARD_CHANGELOG 361 | Versions = [ SemVersion(0, 0, 0) ] 362 | } 363 | 364 | let commits: Git.Commit list = 365 | [ 366 | Git.Commit.Create( 367 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 368 | "fix: fix a bug", 369 | "fix: fix a bug 370 | 371 | Tag: converter" 372 | ) 373 | Git.Commit.Create( 374 | "43c60e4fc9585a9f235ab6a6dd97c4c1cf945e46", 375 | "feat: add a new feature", 376 | "feat: add a new feature 377 | 378 | Tag: cli" 379 | ) 380 | Git.Commit.Create( 381 | "34941a75efeeb3649c1adcec2c7f5c6257117a96", 382 | "feat: make it do something", 383 | "feat: make it do something 384 | 385 | Tag: web" 386 | ) 387 | 388 | Git.Commit.Create( 389 | "c4bb772982b988db7d032263ae824bd2db653d6c", 390 | "feat: make it do something" 391 | ) 392 | Git.Commit.Create( 393 | "b7eafe7744e4738d9578c09e1d128bbb2f5c40d3", 394 | "feat: add another feature", 395 | "feat: add another feature 396 | 397 | Tag: cli" 398 | ) 399 | ] 400 | 401 | let actual = 402 | ReleaseContext.compute 403 | defaultGenerateSettings 404 | changelogInfo 405 | commits 406 | CommitParserConfig.Default 407 | 408 | let expected = 409 | { 410 | NewVersion = SemVersion(0, 1, 0) 411 | CommitsForRelease = 412 | [ 413 | { 414 | OriginalCommit = commits[0] 415 | SemanticCommit = 416 | Parser.tryParseCommitMessage 417 | CommitParserConfig.Default 418 | commits[0].RawBody 419 | |> Result.valueOr failwith 420 | } 421 | { 422 | OriginalCommit = commits[1] 423 | SemanticCommit = 424 | Parser.tryParseCommitMessage 425 | CommitParserConfig.Default 426 | commits[1].RawBody 427 | |> Result.valueOr failwith 428 | } 429 | { 430 | OriginalCommit = commits[4] 431 | SemanticCommit = 432 | Parser.tryParseCommitMessage 433 | CommitParserConfig.Default 434 | commits[4].RawBody 435 | |> Result.valueOr failwith 436 | } 437 | ] 438 | LastCommitSha = "49c0699af98a67f1e8efcac8b1467b283a244aa8" 439 | } 440 | |> BumpRequired 441 | 442 | Expect.equal actual expected 443 | } 444 | ] 445 | 446 | let private computeVersionTests = 447 | testList 448 | "computeVersionTests" 449 | [ 450 | test "if previous version was a pre-release, release it as stable" { 451 | let settings = GenerateSettings(Changelog = "CHANGELOG.md") 452 | 453 | let commits: Git.Commit list = 454 | [ 455 | Git.Commit.Create( 456 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 457 | "fix: fix a bug" 458 | ) 459 | ] 460 | 461 | let commits: CommitForRelease list = 462 | [ 463 | { 464 | OriginalCommit = commits[0] 465 | SemanticCommit = 466 | Parser.tryParseCommitMessage 467 | CommitParserConfig.Default 468 | commits[0].RawBody 469 | |> Result.valueOr failwith 470 | } 471 | ] 472 | 473 | let actual = 474 | ReleaseContext.computeVersion 475 | settings 476 | commits 477 | (SemVersion(2, 0, 0).WithPrereleaseParsedFrom("beta.1")) 478 | 479 | Expect.equal actual (Some(SemVersion(2, 0, 0))) 480 | } 481 | 482 | test 483 | "If user request a pre-release, should bump major and make start a pre-release if previous version was stable and changes include breaking change" { 484 | let settings = 485 | GenerateSettings( 486 | Changelog = "CHANGELOG.md", 487 | PreRelease = FlagValue(Value = "beta", IsSet = true) 488 | ) 489 | 490 | let commits: Git.Commit list = 491 | [ 492 | Git.Commit.Create( 493 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 494 | "fix!: fix a bug" 495 | ) 496 | ] 497 | 498 | let commits: CommitForRelease list = 499 | [ 500 | { 501 | OriginalCommit = commits[0] 502 | SemanticCommit = 503 | Parser.tryParseCommitMessage 504 | CommitParserConfig.Default 505 | commits[0].RawBody 506 | |> Result.valueOr failwith 507 | } 508 | ] 509 | 510 | let actual = ReleaseContext.computeVersion settings commits (SemVersion(2, 2, 45)) 511 | 512 | Expect.equal actual (Some(SemVersion.ParsedFrom(3, 0, 0, "beta.1"))) 513 | } 514 | 515 | test 516 | "If user request a pre-release, should bump minor and make start a pre-release if previous version was stable and changes include new features" { 517 | let settings = 518 | GenerateSettings( 519 | Changelog = "CHANGELOG.md", 520 | PreRelease = FlagValue(Value = "beta", IsSet = true) 521 | ) 522 | 523 | let commits: Git.Commit list = 524 | [ 525 | Git.Commit.Create( 526 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 527 | "fix: fix a bug" 528 | ) 529 | Git.Commit.Create( 530 | "49c0699af98a67f1e8efcac8b1467b283a244aa7", 531 | "feat: add a new feature" 532 | ) 533 | ] 534 | 535 | let commits: CommitForRelease list = 536 | [ 537 | { 538 | OriginalCommit = commits[0] 539 | SemanticCommit = 540 | Parser.tryParseCommitMessage 541 | CommitParserConfig.Default 542 | commits[0].RawBody 543 | |> Result.valueOr failwith 544 | } 545 | { 546 | OriginalCommit = commits[1] 547 | SemanticCommit = 548 | Parser.tryParseCommitMessage 549 | CommitParserConfig.Default 550 | commits[1].RawBody 551 | |> Result.valueOr failwith 552 | } 553 | ] 554 | 555 | let actual = ReleaseContext.computeVersion settings commits (SemVersion(2, 2, 45)) 556 | 557 | Expect.equal actual (Some(SemVersion.ParsedFrom(2, 3, 0, "beta.1"))) 558 | } 559 | 560 | test 561 | "If user request a pre-release, should bump patch and make start a pre-release if previous version was stable and changes include only bug fixes" { 562 | let settings = 563 | GenerateSettings( 564 | Changelog = "CHANGELOG.md", 565 | PreRelease = FlagValue(Value = "beta", IsSet = true) 566 | ) 567 | 568 | let commits: Git.Commit list = 569 | [ 570 | Git.Commit.Create( 571 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 572 | "fix: fix a bug" 573 | ) 574 | Git.Commit.Create( 575 | "49c0699af98a67f1e8efcac8b1467b283a244aa7", 576 | "fix: add a new feature" 577 | ) 578 | ] 579 | 580 | let commits: CommitForRelease list = 581 | [ 582 | { 583 | OriginalCommit = commits[0] 584 | SemanticCommit = 585 | Parser.tryParseCommitMessage 586 | CommitParserConfig.Default 587 | commits[0].RawBody 588 | |> Result.valueOr failwith 589 | } 590 | { 591 | OriginalCommit = commits[1] 592 | SemanticCommit = 593 | Parser.tryParseCommitMessage 594 | CommitParserConfig.Default 595 | commits[1].RawBody 596 | |> Result.valueOr failwith 597 | } 598 | ] 599 | 600 | let actual = ReleaseContext.computeVersion settings commits (SemVersion(2, 2, 45)) 601 | 602 | Expect.equal actual (Some(SemVersion.ParsedFrom(2, 2, 46, "beta.1"))) 603 | } 604 | 605 | test 606 | "If user request a pre-release, should increment the pre-release number if previous version was a pre-release (check for major version)" { 607 | let settings = 608 | GenerateSettings( 609 | Changelog = "CHANGELOG.md", 610 | PreRelease = FlagValue(Value = "beta", IsSet = true) 611 | ) 612 | 613 | let commits: Git.Commit list = 614 | [ 615 | Git.Commit.Create( 616 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 617 | "fix!: fix a bug" 618 | ) 619 | ] 620 | 621 | let commits: CommitForRelease list = 622 | [ 623 | { 624 | OriginalCommit = commits[0] 625 | SemanticCommit = 626 | Parser.tryParseCommitMessage 627 | CommitParserConfig.Default 628 | commits[0].RawBody 629 | |> Result.valueOr failwith 630 | } 631 | ] 632 | 633 | let actual = 634 | ReleaseContext.computeVersion 635 | settings 636 | commits 637 | (SemVersion.ParsedFrom(3, 0, 0, "beta.10")) 638 | 639 | Expect.equal actual (Some(SemVersion.ParsedFrom(3, 0, 0, "beta.11"))) 640 | } 641 | 642 | test 643 | "If user request a pre-release, should increment the pre-release number if previous version was a pre-release (check for minor version)" { 644 | let settings = 645 | GenerateSettings( 646 | Changelog = "CHANGELOG.md", 647 | PreRelease = FlagValue(Value = "beta", IsSet = true) 648 | ) 649 | 650 | let commits: Git.Commit list = 651 | [ 652 | Git.Commit.Create( 653 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 654 | "fix: fix a bug" 655 | ) 656 | Git.Commit.Create( 657 | "49c0699af98a67f1e8efcac8b1467b283a244aa7", 658 | "feat: add a new feature" 659 | ) 660 | ] 661 | 662 | let commits: CommitForRelease list = 663 | [ 664 | { 665 | OriginalCommit = commits[0] 666 | SemanticCommit = 667 | Parser.tryParseCommitMessage 668 | CommitParserConfig.Default 669 | commits[0].RawBody 670 | |> Result.valueOr failwith 671 | } 672 | { 673 | OriginalCommit = commits[1] 674 | SemanticCommit = 675 | Parser.tryParseCommitMessage 676 | CommitParserConfig.Default 677 | commits[1].RawBody 678 | |> Result.valueOr failwith 679 | } 680 | ] 681 | 682 | let actual = 683 | ReleaseContext.computeVersion 684 | settings 685 | commits 686 | (SemVersion.ParsedFrom(2, 2, 0, "beta.233")) 687 | 688 | Expect.equal actual (Some(SemVersion.ParsedFrom(2, 2, 0, "beta.234"))) 689 | } 690 | 691 | test 692 | "If user request a pre-release, should increment the pre-release number if previous version was a pre-release (check for patch version)" { 693 | let settings = 694 | GenerateSettings( 695 | Changelog = "CHANGELOG.md", 696 | PreRelease = FlagValue(Value = "beta", IsSet = true) 697 | ) 698 | 699 | let commits: Git.Commit list = 700 | [ 701 | Git.Commit.Create( 702 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 703 | "fix: fix a bug" 704 | ) 705 | Git.Commit.Create( 706 | "49c0699af98a67f1e8efcac8b1467b283a244aa7", 707 | "fix: add a new feature" 708 | ) 709 | ] 710 | 711 | let commits: CommitForRelease list = 712 | [ 713 | { 714 | OriginalCommit = commits[0] 715 | SemanticCommit = 716 | Parser.tryParseCommitMessage 717 | CommitParserConfig.Default 718 | commits[0].RawBody 719 | |> Result.valueOr failwith 720 | } 721 | { 722 | OriginalCommit = commits[1] 723 | SemanticCommit = 724 | Parser.tryParseCommitMessage 725 | CommitParserConfig.Default 726 | commits[1].RawBody 727 | |> Result.valueOr failwith 728 | } 729 | ] 730 | 731 | let actual = 732 | ReleaseContext.computeVersion 733 | settings 734 | commits 735 | (SemVersion.ParsedFrom(2, 2, 45, "beta.5")) 736 | 737 | Expect.equal actual (Some(SemVersion.ParsedFrom(2, 2, 45, "beta.6"))) 738 | } 739 | 740 | test 741 | "If previous version was a pre-release, and user don't request a pre-release, release it as stable (check for major version)" { 742 | let settings = GenerateSettings(Changelog = "CHANGELOG.md") 743 | 744 | let commits: Git.Commit list = 745 | [ 746 | Git.Commit.Create( 747 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 748 | "fix!: fix a bug" 749 | ) 750 | ] 751 | 752 | let commits: CommitForRelease list = 753 | [ 754 | { 755 | OriginalCommit = commits[0] 756 | SemanticCommit = 757 | Parser.tryParseCommitMessage 758 | CommitParserConfig.Default 759 | commits[0].RawBody 760 | |> Result.valueOr failwith 761 | } 762 | ] 763 | 764 | let actual = 765 | ReleaseContext.computeVersion 766 | settings 767 | commits 768 | (SemVersion(2, 0, 0).WithPrereleaseParsedFrom("beta.1")) 769 | 770 | Expect.equal actual (Some(SemVersion(2, 0, 0))) 771 | } 772 | 773 | test 774 | "If previous version was a pre-release, and user don't request a pre-release, release it as stable (check for minor version)" { 775 | let settings = GenerateSettings(Changelog = "CHANGELOG.md") 776 | 777 | let commits: Git.Commit list = 778 | [ 779 | Git.Commit.Create( 780 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 781 | "feat: fix a bug" 782 | ) 783 | ] 784 | 785 | let commits: CommitForRelease list = 786 | [ 787 | { 788 | OriginalCommit = commits[0] 789 | SemanticCommit = 790 | Parser.tryParseCommitMessage 791 | CommitParserConfig.Default 792 | commits[0].RawBody 793 | |> Result.valueOr failwith 794 | } 795 | ] 796 | 797 | let actual = 798 | ReleaseContext.computeVersion 799 | settings 800 | commits 801 | (SemVersion(2, 0, 0).WithPrereleaseParsedFrom("beta.1")) 802 | 803 | Expect.equal actual (Some(SemVersion(2, 0, 0))) 804 | } 805 | 806 | test 807 | "If previous version was a pre-release, and user don't request a pre-release, release it as stable (check for patch version)" { 808 | let settings = GenerateSettings(Changelog = "CHANGELOG.md") 809 | 810 | let commits: Git.Commit list = 811 | [ 812 | Git.Commit.Create( 813 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 814 | "fix: fix a bug" 815 | ) 816 | ] 817 | 818 | let commits: CommitForRelease list = 819 | [ 820 | { 821 | OriginalCommit = commits[0] 822 | SemanticCommit = 823 | Parser.tryParseCommitMessage 824 | CommitParserConfig.Default 825 | commits[0].RawBody 826 | |> Result.valueOr failwith 827 | } 828 | ] 829 | 830 | let actual = 831 | ReleaseContext.computeVersion 832 | settings 833 | commits 834 | (SemVersion(2, 0, 0).WithPrereleaseParsedFrom("beta.1")) 835 | 836 | Expect.equal actual (Some(SemVersion(2, 0, 0))) 837 | } 838 | 839 | test 840 | "If pre-release identifier is different start a new pre-release from 1 (check for major)" { 841 | let settings = 842 | GenerateSettings( 843 | Changelog = "CHANGELOG.md", 844 | PreRelease = FlagValue(Value = "alpha", IsSet = true) 845 | ) 846 | 847 | let commits: Git.Commit list = 848 | [ 849 | Git.Commit.Create( 850 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 851 | "fix: fix a bug" 852 | ) 853 | ] 854 | 855 | let commits: CommitForRelease list = 856 | [ 857 | { 858 | OriginalCommit = commits[0] 859 | SemanticCommit = 860 | Parser.tryParseCommitMessage 861 | CommitParserConfig.Default 862 | commits[0].RawBody 863 | |> Result.valueOr failwith 864 | } 865 | ] 866 | 867 | let actual = 868 | ReleaseContext.computeVersion 869 | settings 870 | commits 871 | (SemVersion(2, 0, 0).WithPrereleaseParsedFrom("beta.10")) 872 | 873 | Expect.equal actual (Some(SemVersion.ParsedFrom(2, 0, 0, "alpha.1"))) 874 | } 875 | 876 | test 877 | "If pre-release identifier is different start a new pre-release from 1 (check for minor)" { 878 | let settings = 879 | GenerateSettings( 880 | Changelog = "CHANGELOG.md", 881 | PreRelease = FlagValue(Value = "alpha", IsSet = true) 882 | ) 883 | 884 | let commits: Git.Commit list = 885 | [ 886 | Git.Commit.Create( 887 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 888 | "fix: fix a bug" 889 | ) 890 | ] 891 | 892 | let commits: CommitForRelease list = 893 | [ 894 | { 895 | OriginalCommit = commits[0] 896 | SemanticCommit = 897 | Parser.tryParseCommitMessage 898 | CommitParserConfig.Default 899 | commits[0].RawBody 900 | |> Result.valueOr failwith 901 | } 902 | ] 903 | 904 | let actual = 905 | ReleaseContext.computeVersion 906 | settings 907 | commits 908 | (SemVersion(2, 2, 0).WithPrereleaseParsedFrom("beta.10")) 909 | 910 | Expect.equal actual (Some(SemVersion.ParsedFrom(2, 2, 0, "alpha.1"))) 911 | } 912 | 913 | test 914 | "If pre-release identifier is different start a new pre-release from 1 (check for patch)" { 915 | let settings = 916 | GenerateSettings( 917 | Changelog = "CHANGELOG.md", 918 | PreRelease = FlagValue(Value = "alpha", IsSet = true) 919 | ) 920 | 921 | let commits: Git.Commit list = 922 | [ 923 | Git.Commit.Create( 924 | "49c0699af98a67f1e8efcac8b1467b283a244aa8", 925 | "fix: fix a bug" 926 | ) 927 | ] 928 | 929 | let commits: CommitForRelease list = 930 | [ 931 | { 932 | OriginalCommit = commits[0] 933 | SemanticCommit = 934 | Parser.tryParseCommitMessage 935 | CommitParserConfig.Default 936 | commits[0].RawBody 937 | |> Result.valueOr failwith 938 | } 939 | ] 940 | 941 | let actual = 942 | ReleaseContext.computeVersion 943 | settings 944 | commits 945 | (SemVersion(2, 2, 45).WithPrereleaseParsedFrom("beta.10")) 946 | 947 | Expect.equal actual (Some(SemVersion.ParsedFrom(2, 2, 45, "alpha.1"))) 948 | } 949 | ] 950 | 951 | let tests = 952 | testList 953 | "ReleaseContext" 954 | [ 955 | computeTests 956 | computeVersionTests 957 | ] 958 | -------------------------------------------------------------------------------- /tests/Utils.fs: -------------------------------------------------------------------------------- 1 | module Tests.Utils 2 | 3 | open Expecto 4 | 5 | module Expect = 6 | 7 | let equal actual expected = Expect.equal actual expected "" 8 | 9 | let notEqual actual expected = Expect.notEqual actual expected "" 10 | 11 | let isNotEmpty actual = Expect.isNotEmpty actual "" 12 | 13 | let isOk actual = Expect.isOk actual "" 14 | 15 | let isError actual = Expect.isError actual "" 16 | 17 | type Git.Commit with 18 | 19 | static member Create 20 | (hash: string, shortMessage: string, ?longMessage: string, ?author: string) 21 | : Git.Commit 22 | = 23 | let author = defaultArg author "Kaladin Stormblessed" 24 | let longMessage = defaultArg longMessage (shortMessage + "\n") 25 | 26 | { 27 | Hash = hash 28 | AbbrevHash = hash.Substring(0, 7) 29 | Author = author 30 | ShortMessage = shortMessage 31 | RawBody = longMessage 32 | } 33 | 34 | open VerifyTests 35 | open VerifyExpecto 36 | open DiffEngine 37 | open Workspace 38 | open System.Runtime.CompilerServices 39 | open System.Runtime.InteropServices 40 | 41 | let mutable diffToolRegistered = false 42 | 43 | let registerDiffTool () = 44 | if not diffToolRegistered then 45 | let launchArguments = 46 | LaunchArguments( 47 | Left = fun temp target -> $"-n --diff \"{target}\" \"{temp}\"" 48 | , Right = fun temp target -> $"-n --diff \"{temp}\" \"{target}\"" 49 | ) 50 | 51 | DiffTools.AddToolBasedOn( 52 | DiffTool.VisualStudioCode, 53 | "VisualStudioCodeNewWindow", 54 | launchArguments = launchArguments 55 | ) 56 | |> ignore 57 | 58 | diffToolRegistered <- true 59 | 60 | let inline verify (name: string) (value: string) = 61 | // registerDiffTool() 62 | 63 | let settings = VerifySettings() 64 | settings.UseDirectory(Workspace.``..``.VerifyTests.``.``) 65 | Verifier.Verify(name, value, settings, "dwdw").ToTask() 66 | 67 | type Verify = 68 | 69 | static member Markdown 70 | ( 71 | name: string, 72 | value: string, 73 | [] callerFilePath: string 74 | ) 75 | 76 | = 77 | let settings = VerifySettings() 78 | settings.UseDirectory(Workspace.``..``.VerifyTests.``.``) 79 | 80 | Verifier.Verify(name, value, "md", settings, callerFilePath).ToTask() 81 | 82 | type TestHelper = 83 | static member testMarkdown 84 | ( 85 | name: string, 86 | func: unit -> string, 87 | [] callerFilePath: string 88 | ) 89 | = 90 | testTask name { do! Verify.Markdown(name, func (), callerFilePath) } 91 | -------------------------------------------------------------------------------- /tests/Verify.fs: -------------------------------------------------------------------------------- 1 | module EasyBuild.ChangelogGen.Tests.Verify 2 | 3 | open Expecto 4 | open Tests.Utils 5 | open EasyBuild.ChangelogGen.Generate 6 | open EasyBuild.ChangelogGen.Generate.Types 7 | open EasyBuild.ChangelogGen.Types 8 | 9 | let tests = 10 | testList 11 | "Verify" 12 | [ 13 | testList 14 | "resolveRemoteConfig" 15 | [ 16 | test "resolveRemoteConfig priorize CLI arguments" { 17 | let actual = 18 | Verify.resolveRemoteConfig ( 19 | GenerateSettings(GitHubRepo = Some "owner/repo") 20 | ) 21 | 22 | let expected = 23 | Ok( 24 | { 25 | Owner = "owner" 26 | Repository = "repo" 27 | } 28 | : GithubRemoteConfig 29 | ) 30 | 31 | Expect.equal expected actual 32 | } 33 | 34 | test "returns an error if CLI argument is not in the right format" { 35 | let actual = 36 | Verify.resolveRemoteConfig (GenerateSettings(GitHubRepo = Some "owner")) 37 | 38 | let expected = 39 | Error 40 | "Invalid format for --github-repo option, expected format is 'owner/repo'." 41 | 42 | Expect.equal expected actual 43 | } 44 | ] 45 | ] 46 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.breaking change are going into their own section if configured.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🏗️ Breaking changes 4 | 5 | * Add another feature via breaking change ([4057b1a](https://github.com/owner/repository/commit/4057b1a703845efcdf2f3b49240dd79d8ce7150e)) 6 | * Fix bug via breaking change ([9156258](https://github.com/owner/repository/commit/9156258d463ba78ac21ebb5fcd32147657bfe86f)) 7 | 8 | ### 🚀 Features 9 | 10 | * Add feature ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 11 | 12 | ### 🐞 Bug Fixes 13 | 14 | * Fix bug ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 15 | 16 | [View changes on Github](https://github.com/owner/repository/compare/fefd5e0bf242e034f86ad23a886e2d71ded4f7bb..0b1899bb03d3eb86a30c84aa4c66c037527fbd14) 17 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.breaking change stays in their original group if they don't have a dedicated group.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🏗️ Breaking changes 4 | 5 | * Performance improvement via breaking change ([d421279](https://github.com/owner/repository/commit/d4212797a454d591068e18480843c87766b0291e)) 6 | * Add another feature via breaking change ([4057b1a](https://github.com/owner/repository/commit/4057b1a703845efcdf2f3b49240dd79d8ce7150e)) 7 | * Fix bug via breaking change ([9156258](https://github.com/owner/repository/commit/9156258d463ba78ac21ebb5fcd32147657bfe86f)) 8 | 9 | ### 🚀 Features 10 | 11 | * Add feature ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 12 | 13 | ### 🐞 Bug Fixes 14 | 15 | * Fix bug ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 16 | 17 | ### ⚡ Performance Improvements 18 | 19 | * Performance improvement ([287a4ee](https://github.com/owner/repository/commit/287a4ee6f89ab84e52283d69f4304ece97e5e87c)) 20 | 21 | [View changes on Github](https://github.com/owner/repository/compare/fefd5e0bf242e034f86ad23a886e2d71ded4f7bb..0b1899bb03d3eb86a30c84aa4c66c037527fbd14) 22 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.commits are ordered by scope.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🚀 Features 4 | 5 | * *(all)* All Fable targets are awesome ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 6 | * *(js)* JavaScript is awesome ([be429a9](https://github.com/owner/repository/commit/be429a973ac2f6d1c009b7efcd15360c9e585450)) 7 | * *(js)* Add feature #1 ([21033aa](https://github.com/owner/repository/commit/21033aae357447dbfac30557a2dee0c4b5b03f68)) 8 | * *(js)* Add feature #2 ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 9 | * *(rust)* Rust is awesome ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 10 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.compare link is not generated if no previous release sha is provided.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🚀 Features 4 | 5 | * Add another feature ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 6 | * Add feature ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 7 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.include changelog additional data when present.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🚀 Features 4 | 5 | * Add feature ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 6 | 7 | ```fs 8 | let upper (s: string) = s.ToUpper() 9 | ``` 10 | 11 | This is a list of changes: 12 | 13 | * Added upper function 14 | * Added lower function 15 | 16 | ### 🐞 Bug Fixes 17 | 18 | * Fix bug ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 19 | 20 | [View changes on Github](https://github.com/owner/repository/compare/fefd5e0bf242e034f86ad23a886e2d71ded4f7bb..0b1899bb03d3eb86a30c84aa4c66c037527fbd14) 21 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.only commit of type feat, perf and fix are included in the changelog.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🚀 Features 4 | 5 | * Add feature ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 6 | 7 | ### 🐞 Bug Fixes 8 | 9 | * Fix bug ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 10 | 11 | ### ⚡ Performance Improvements 12 | 13 | * Performance improvement ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 14 | 15 | [View changes on Github](https://github.com/owner/repository/compare/fefd5e0bf242e034f86ad23a886e2d71ded4f7bb..0b1899bb03d3eb86a30c84aa4c66c037527fbd14) 16 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.works for feat type commit.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🚀 Features 4 | 5 | * Add another feature ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 6 | * Add feature ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 7 | 8 | [View changes on Github](https://github.com/owner/repository/compare/fefd5e0bf242e034f86ad23a886e2d71ded4f7bb..0b1899bb03d3eb86a30c84aa4c66c037527fbd14) 9 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.works for fix type commit.verified.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2024-11-18 2 | 3 | ### 🐞 Bug Fixes 4 | 5 | * Fix another bug ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 6 | * Fix bug ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 7 | 8 | [View changes on Github](https://github.com/owner/repository/compare/fefd5e0bf242e034f86ad23a886e2d71ded4f7bb..0b1899bb03d3eb86a30c84aa4c66c037527fbd14) 9 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.works if metadata was existing.verified.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 8 | 9 | 10 | 11 | 12 | 13 | ## 1.1.0 - 2024-11-18 14 | 15 | ### 🚀 Features 16 | 17 | * Add another feature ([46d3802](https://github.com/owner/repository/commit/46d380257c08fe1f74e4596b8720d71a39f6e629)) 18 | * Add feature ([fef5f47](https://github.com/owner/repository/commit/fef5f479d65172bd385b781bbed83f6eee2a32c6)) 19 | 20 | ### 🐞 Bug Fixes 21 | 22 | * Fix another bug ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 23 | * Fix bug ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 24 | 25 | [View changes on Github](https://github.com/owner/repository/compare/fefd5e0bf242e034f86ad23a886e2d71ded4f7bb..0b1899bb03d3eb86a30c84aa4c66c037527fbd14) 26 | 27 | ## 1.0.0 28 | 29 | ### 🚀 Features 30 | 31 | * Add new feature 32 | * Add another new feature 33 | 34 | ## 0.1.0 35 | 36 | ### 🚀 Features 37 | 38 | * Initial release 39 | 40 | 44 | -------------------------------------------------------------------------------- /tests/VerifyTests/Changelog.works if metadata was not existing.verified.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 8 | 9 | 10 | 11 | 12 | 13 | ## 1.1.0 - 2024-11-18 14 | 15 | ### 🚀 Features 16 | 17 | * Add another feature ([46d3802](https://github.com/owner/repository/commit/46d380257c08fe1f74e4596b8720d71a39f6e629)) 18 | * Add feature ([fef5f47](https://github.com/owner/repository/commit/fef5f479d65172bd385b781bbed83f6eee2a32c6)) 19 | 20 | ### 🐞 Bug Fixes 21 | 22 | * Fix another bug ([2a6f3b3](https://github.com/owner/repository/commit/2a6f3b3403aaa629de6e65558448b37f126f8e86)) 23 | * Fix bug ([0b1899b](https://github.com/owner/repository/commit/0b1899bb03d3eb86a30c84aa4c66c037527fbd14)) 24 | 25 | ## 1.0.0 26 | 27 | ### 🚀 Features 28 | 29 | * Add new feature 30 | * Add another new feature 31 | 32 | ## 0.1.0 33 | 34 | ### 🚀 Features 35 | 36 | * Initial release 37 | -------------------------------------------------------------------------------- /tests/Workspace.fs: -------------------------------------------------------------------------------- 1 | module Workspace 2 | 3 | open EasyBuild.FileSystemProvider 4 | 5 | type Workspace = RelativeFileSystem<"./fixtures"> 6 | -------------------------------------------------------------------------------- /tests/fixtures/valid_changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 8 | 9 | 10 | 11 | 12 | 13 | ## 1.0.0 14 | 15 | ### 🚀 Features 16 | 17 | * Add new feature 18 | * Add another new feature 19 | 20 | ## 0.1.0 21 | 22 | ### 🚀 Features 23 | 24 | * Initial release 25 | 26 | 30 | -------------------------------------------------------------------------------- /tests/fixtures/valid_changelog_no_metadata.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 8 | 9 | ## 1.0.0 10 | 11 | ### 🚀 Features 12 | 13 | * Add new feature 14 | * Add another new feature 15 | 16 | ## 0.1.0 17 | 18 | ### 🚀 Features 19 | 20 | * Initial release 21 | -------------------------------------------------------------------------------- /tests/fixtures/valid_no_version.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | This changelog is generated using [EasyBuild.ChangelogGen](https://github.com/easybuild-org/EasyBuild.ChangelogGen). Do not edit this file manually. 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "dependencies": { 4 | "net8.0": { 5 | "EasyBuild.FileSystemProvider": { 6 | "type": "Direct", 7 | "requested": "[0.3.0, )", 8 | "resolved": "0.3.0", 9 | "contentHash": "gdVJpqcMDJm4IfmITy3MtpEn/lo9pH8PirVlENtXGX9Sdw3rCgoo9ch1TAthUseh28RcUGWwza9BmEWlrQX/Aw==" 10 | }, 11 | "Expecto": { 12 | "type": "Direct", 13 | "requested": "[10.2.1, )", 14 | "resolved": "10.2.1", 15 | "contentHash": "nV9ACIccY2cv23KvtMfflQdMPhF7oTXf0dRMz1Jq6GuQjLnuf+zBj4IlBzmZEz9VeCrCOWxdnt/5VFCrGcn+pA==", 16 | "dependencies": { 17 | "FSharp.Core": "7.0.200", 18 | "Mono.Cecil": "[0.11.4, 1.0.0)" 19 | } 20 | }, 21 | "FSharp.Core": { 22 | "type": "Direct", 23 | "requested": "[8.0.401, )", 24 | "resolved": "8.0.401", 25 | "contentHash": "pvmquLrs82m8WqWB49Gj+hsmnWURkUbc++pJ0P2NdrEv1ZVVrhDNjSiFNP4wW4HiTowwaRhjiBc5IKNV35Vf/A==" 26 | }, 27 | "Microsoft.NET.Test.Sdk": { 28 | "type": "Direct", 29 | "requested": "[17.8.0, )", 30 | "resolved": "17.8.0", 31 | "contentHash": "BmTYGbD/YuDHmApIENdoyN1jCk0Rj1fJB0+B/fVekyTdVidr91IlzhqzytiUgaEAzL1ZJcYCme0MeBMYvJVzvw==", 32 | "dependencies": { 33 | "Microsoft.CodeCoverage": "17.8.0", 34 | "Microsoft.TestPlatform.TestHost": "17.8.0" 35 | } 36 | }, 37 | "Verify.Expecto": { 38 | "type": "Direct", 39 | "requested": "[28.2.0, )", 40 | "resolved": "28.2.0", 41 | "contentHash": "DPA98YmnGnYf4NHsYVXh+Cdprtc1nKRES1iBaD9qmqvJUVzN6FPaodRBer8ZEAjEj71hJGpuftDS5j1QO+khKw==", 42 | "dependencies": { 43 | "Argon": "0.24.2", 44 | "DiffEngine": "15.5.3", 45 | "Expecto": "10.2.1", 46 | "FSharp.Core": "8.0.401", 47 | "SimpleInfoName": "3.0.1", 48 | "System.IO.Hashing": "8.0.0", 49 | "Verify": "28.2.0" 50 | } 51 | }, 52 | "YoloDev.Expecto.TestSdk": { 53 | "type": "Direct", 54 | "requested": "[0.14.3, )", 55 | "resolved": "0.14.3", 56 | "contentHash": "tu24//r9EbDPNRHwy5EU3oVeX98WIu7N9u6SwdQNUmJkWHYN19ihlTF5TAtQ/0sHlzbANj433LfXw2ze9m6XwA==", 57 | "dependencies": { 58 | "Expecto": "[10.0.0, 11.0.0)", 59 | "FSharp.Core": "7.0.200", 60 | "System.Collections.Immutable": "6.0.0" 61 | } 62 | }, 63 | "Argon": { 64 | "type": "Transitive", 65 | "resolved": "0.24.2", 66 | "contentHash": "O7JY7brhNcYvRNzByqx0cp0w4RlmyyxPyaVlkKOoA6FmWst6fIIkM5uwzQUErLYPx13diUKyhlwpB4KuC9UT2w==" 67 | }, 68 | "DiffEngine": { 69 | "type": "Transitive", 70 | "resolved": "15.5.3", 71 | "contentHash": "5TaF7O1deN05sR3f0Tkr7XwyPTtPza1UAlRN0KlBUZNoRsB3wOSqd62oSvLYvhrV/vSyYa+AZ9WmCu4I+vQe6Q==", 72 | "dependencies": { 73 | "EmptyFiles": "8.5.0", 74 | "System.Management": "8.0.0" 75 | } 76 | }, 77 | "EmptyFiles": { 78 | "type": "Transitive", 79 | "resolved": "8.5.0", 80 | "contentHash": "qTePu7un5yoto0VHO4hEjU/uEq+EuY0NGpo4JHs10dXbEiXPmUeKS1IVow0vWnPIIBW8OLhQu8TV9o5bR3nlCQ==" 81 | }, 82 | "Fable.Core": { 83 | "type": "Transitive", 84 | "resolved": "4.1.0", 85 | "contentHash": "NISAbAVGEcvH2s+vHLSOCzh98xMYx4aIadWacQdWPcQLploxpSQXLEe9SeszUBhbHa73KMiKREsH4/W3q4A4iA==" 86 | }, 87 | "Microsoft.CodeCoverage": { 88 | "type": "Transitive", 89 | "resolved": "17.8.0", 90 | "contentHash": "KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==" 91 | }, 92 | "Microsoft.TestPlatform.ObjectModel": { 93 | "type": "Transitive", 94 | "resolved": "17.8.0", 95 | "contentHash": "AYy6vlpGMfz5kOFq99L93RGbqftW/8eQTqjT9iGXW6s9MRP3UdtY8idJ8rJcjeSja8A18IhIro5YnH3uv1nz4g==", 96 | "dependencies": { 97 | "NuGet.Frameworks": "6.5.0", 98 | "System.Reflection.Metadata": "1.6.0" 99 | } 100 | }, 101 | "Microsoft.TestPlatform.TestHost": { 102 | "type": "Transitive", 103 | "resolved": "17.8.0", 104 | "contentHash": "9ivcl/7SGRmOT0YYrHQGohWiT5YCpkmy/UEzldfVisLm6QxbLaK3FAJqZXI34rnRLmqqDCeMQxKINwmKwAPiDw==", 105 | "dependencies": { 106 | "Microsoft.TestPlatform.ObjectModel": "17.8.0", 107 | "Newtonsoft.Json": "13.0.1" 108 | } 109 | }, 110 | "Mono.Cecil": { 111 | "type": "Transitive", 112 | "resolved": "0.11.4", 113 | "contentHash": "IC1h5g0NeJGHIUgzM1P82ld57knhP0IcQfrYITDPXlNpMYGUrsG5TxuaWTjaeqDNQMBDNZkB8L0rBnwsY6JHuQ==" 114 | }, 115 | "Newtonsoft.Json": { 116 | "type": "Transitive", 117 | "resolved": "13.0.1", 118 | "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" 119 | }, 120 | "NuGet.Frameworks": { 121 | "type": "Transitive", 122 | "resolved": "6.5.0", 123 | "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" 124 | }, 125 | "SimpleInfoName": { 126 | "type": "Transitive", 127 | "resolved": "3.0.1", 128 | "contentHash": "tMAM400r41N7gzRZ/MsGpPfSU3lqkYLFA7hlVEPpyLOMtp4bglC4spCmiH3j1Dro3WH/6hTXZ20G/c4bs5P8WQ==" 129 | }, 130 | "Spectre.Console": { 131 | "type": "Transitive", 132 | "resolved": "0.49.0", 133 | "contentHash": "1s+hhYq5fcrqCvZhrNOPehAmCZJM6cjro85g1Qirvmm1tys+/sfFJgePCGcxr+S/nONZ/lCDTflPda3C+WlBdg==" 134 | }, 135 | "System.CodeDom": { 136 | "type": "Transitive", 137 | "resolved": "8.0.0", 138 | "contentHash": "WTlRjL6KWIMr/pAaq3rYqh0TJlzpouaQ/W1eelssHgtlwHAH25jXTkUphTYx9HaIIf7XA6qs/0+YhtLEQRkJ+Q==" 139 | }, 140 | "System.Collections.Immutable": { 141 | "type": "Transitive", 142 | "resolved": "6.0.0", 143 | "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", 144 | "dependencies": { 145 | "System.Runtime.CompilerServices.Unsafe": "6.0.0" 146 | } 147 | }, 148 | "System.IO.Hashing": { 149 | "type": "Transitive", 150 | "resolved": "8.0.0", 151 | "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" 152 | }, 153 | "System.Management": { 154 | "type": "Transitive", 155 | "resolved": "8.0.0", 156 | "contentHash": "jrK22i5LRzxZCfGb+tGmke2VH7oE0DvcDlJ1HAKYU8cPmD8XnpUT0bYn2Gy98GEhGjtfbR/sxKTVb+dE770pfA==", 157 | "dependencies": { 158 | "System.CodeDom": "8.0.0" 159 | } 160 | }, 161 | "System.Reflection.Metadata": { 162 | "type": "Transitive", 163 | "resolved": "1.6.0", 164 | "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" 165 | }, 166 | "System.Runtime.CompilerServices.Unsafe": { 167 | "type": "Transitive", 168 | "resolved": "6.0.0", 169 | "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" 170 | }, 171 | "Thoth.Json.Core": { 172 | "type": "Transitive", 173 | "resolved": "0.1.0", 174 | "contentHash": "hIo4bdnbG2BOmCrUTHhScgn0aTnlGPQtmO0KiCklSHduEQV3r7SjwscvjjcbfXyc0nuPV6UOg05KHMUgiIeXjQ==", 175 | "dependencies": { 176 | "FSharp.Core": "5.0.0", 177 | "Fable.Core": "4.1.0" 178 | } 179 | }, 180 | "Thoth.Json.Newtonsoft": { 181 | "type": "Transitive", 182 | "resolved": "0.1.0", 183 | "contentHash": "i64dDASv8UY8oPNwvcF7UTLvXYL2C3PaTeNQ8s8Woy/pNTKud616//+0V858wcS2PyunVnyQeuiND4bfvMujhQ==", 184 | "dependencies": { 185 | "FSharp.Core": "5.0.0", 186 | "Fable.Core": "4.1.0", 187 | "Newtonsoft.Json": "13.0.1", 188 | "Thoth.Json.Core": "0.1.0" 189 | } 190 | }, 191 | "Verify": { 192 | "type": "Transitive", 193 | "resolved": "28.2.0", 194 | "contentHash": "D09dlamyhJ2kxYgIlwRhajefIonmAaaZfCIjAJrB+QR3zoRMBSvHtHa8ah3hQrowzpEeKsnD0mBMyqkJyfh3jg==", 195 | "dependencies": { 196 | "Argon": "0.24.2", 197 | "DiffEngine": "15.5.3", 198 | "SimpleInfoName": "3.0.1", 199 | "System.IO.Hashing": "8.0.0" 200 | } 201 | }, 202 | "easybuild.changeloggen": { 203 | "type": "Project", 204 | "dependencies": { 205 | "BlackFox.CommandLine": "[1.0.0, )", 206 | "EasyBuild.CommitParser": "[3.0.0, )", 207 | "FSharp.Core": "[8.0.101, )", 208 | "FsToolkit.ErrorHandling": "[4.18.0, )", 209 | "Semver": "[2.3.0, )", 210 | "SimpleExec": "[12.0.0, )", 211 | "Spectre.Console.Cli": "[0.49.0, )" 212 | } 213 | }, 214 | "BlackFox.CommandLine": { 215 | "type": "CentralTransitive", 216 | "requested": "[1.0.0, )", 217 | "resolved": "1.0.0", 218 | "contentHash": "dSW7uLetl021HQXKcZd1xrXPjhsXgaJ5U4tFe64DLja1KZ2Ce6QeugHvZDvLfcPkEc1ZPRF7fWv5/T+X3ThWTA==", 219 | "dependencies": { 220 | "FSharp.Core": "4.2.3" 221 | } 222 | }, 223 | "EasyBuild.CommitParser": { 224 | "type": "CentralTransitive", 225 | "requested": "[3.0.0, )", 226 | "resolved": "3.0.0", 227 | "contentHash": "4N8zQ3nXEdmsp8txFrrxz2SbB6ZCQ6/OYDdgrU8G9brUY17s6DYEkPL0tnWdGfjDPufBPO3Uvz4yIB8HonkY4w==", 228 | "dependencies": { 229 | "FSharp.Core": "7.0.300", 230 | "FsToolkit.ErrorHandling": "4.18.0", 231 | "Thoth.Json.Newtonsoft": "0.1.0" 232 | } 233 | }, 234 | "FsToolkit.ErrorHandling": { 235 | "type": "CentralTransitive", 236 | "requested": "[4.18.0, )", 237 | "resolved": "4.18.0", 238 | "contentHash": "cGtOP6lWcnLcXiLTGZLHi+8JAyuUDjGhZOmJWnZfd5aPCUIyL+DqUIwmfEGkUk3j/gpcchLDk9BNwUTc1oM30w==", 239 | "dependencies": { 240 | "FSharp.Core": "7.0.300" 241 | } 242 | }, 243 | "Semver": { 244 | "type": "CentralTransitive", 245 | "requested": "[2.3.0, )", 246 | "resolved": "2.3.0", 247 | "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" 248 | }, 249 | "SimpleExec": { 250 | "type": "CentralTransitive", 251 | "requested": "[12.0.0, )", 252 | "resolved": "12.0.0", 253 | "contentHash": "ptxlWtxC8vM6Y6e3h9ZTxBBkOWnWrm/Sa1HT+2i1xcXY3Hx2hmKDZP5RShPf8Xr9D+ivlrXNy57ktzyH8kyt+Q==" 254 | }, 255 | "Spectre.Console.Cli": { 256 | "type": "CentralTransitive", 257 | "requested": "[0.49.0, )", 258 | "resolved": "0.49.0", 259 | "contentHash": "841g7PhuJFwoCatKKVoIRDaylmcaTxEzTzq7+rGHyVCBUqL7iOH0c5AsGnjgBMzhRDnUUWkP47Ho9WWfqMVqAA==", 260 | "dependencies": { 261 | "Spectre.Console": "0.49.0" 262 | } 263 | } 264 | } 265 | } 266 | } --------------------------------------------------------------------------------