├── .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 | [](https://www.nuget.org/packages/EasyBuild.ChangelogGen)
4 |
5 | [](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 | }
--------------------------------------------------------------------------------