diff --git a/src/app/Fake.Core.ReleaseNotes/Changelog.fs b/src/app/Fake.Core.ReleaseNotes/Changelog.fs
index c63a0a06a80..b3fac548c94 100644
--- a/src/app/Fake.Core.ReleaseNotes/Changelog.fs
+++ b/src/app/Fake.Core.ReleaseNotes/Changelog.fs
@@ -108,6 +108,16 @@ module Changelog =
| Security s -> sprintf "Security: %s" s.CleanedText
| Custom (h, s) -> sprintf "%s: %s" h s.CleanedText
+ member x.ChangeText() =
+ match x with
+ | Added (changeText)
+ | Changed (changeText)
+ | Deprecated (changeText)
+ | Removed (changeText)
+ | Fixed (changeText)
+ | Security (changeText)
+ | Custom (_, changeText) -> changeText
+
/// Create a new change type changelog entry
static member New(header: string, line: string) : Change =
let text =
@@ -267,6 +277,32 @@ module Changelog =
assemblyVersion, nugetVersion
+ ///
+ /// A version or "Unreleased"
+ ///
+ type ReferenceVersion =
+ | SemVerRef of SemVerInfo
+ | UnreleasedRef
+
+ override x.ToString() =
+ match x with
+ | SemVerRef (semVerInfo) -> semVerInfo.ToString()
+ | UnreleasedRef -> "Unreleased"
+
+ ///
+ /// A reference from a version to a repository URL (e.g. a tag or a compare link)
+ ///
+ ///
+ /// [Unreleased]: https://github.com/user/MyCoolNewLib.git/compare/v0.1.0...HEAD
+ /// [0.1.0]: https://github.com/user/MyCoolNewLib.git/releases/tag/v0.1.0
+ ///
+ type Reference =
+ { SemVer: ReferenceVersion
+ RepoUrl: Uri }
+
+ override x.ToString() =
+ sprintf "[%s]: %s" (x.SemVer.ToString()) (x.RepoUrl.ToString())
+
///
/// Holds data for a changelog file, which include changelog entries an other metadata
///
@@ -283,17 +319,21 @@ module Changelog =
/// The change log entries
Entries: ChangelogEntry list
+
+ /// The references to repository URLs
+ References: Reference list
}
/// the latest change log entry
member x.LatestEntry = x.Entries |> Seq.head
/// Create a new changelog record from given data
- static member New(header, description, unreleased, entries) =
+ static member New(header, description, unreleased, entries, references) =
{ Header = header
Description = description
Unreleased = unreleased
- Entries = entries }
+ Entries = entries
+ References = references }
/// Promote an unreleased changelog entry to a released one
member x.PromoteUnreleased(assemblyVersion: string, nugetVersion: string) : Changelog =
@@ -311,7 +351,7 @@ module Changelog =
)
let unreleased' = Some { Description = None; Changes = [] }
- Changelog.New(x.Header, x.Description, unreleased', newEntry :: x.Entries)
+ Changelog.New(x.Header, x.Description, unreleased', newEntry :: x.Entries, x.References)
/// Promote an unreleased changelog entry to a released one using version number
member x.PromoteUnreleased(version: string) : Changelog =
@@ -346,7 +386,10 @@ module Changelog =
| "" -> "Changelog"
| h -> h
- (sprintf "# %s\n\n%s\n\n%s" header description entries)
+ let references =
+ x.References |> List.map (fun reference -> reference.ToString()) |> joinLines
+
+ $"# {header}\n\n{description}\n\n{entries}\n\n{references}"
|> fixMultipleNewlines
|> String.trim
@@ -358,8 +401,9 @@ module Changelog =
/// the descriptive text for changelog
/// the unreleased list of changelog entries
/// the list of changelog entries
- let createWithCustomHeader header description unreleased entries =
- Changelog.New(header, description, unreleased, entries)
+ /// the list of references
+ let createWithCustomHeader header description unreleased entries references =
+ Changelog.New(header, description, unreleased, entries, references)
///
/// Create a changelog with given data
@@ -368,8 +412,9 @@ module Changelog =
/// the descriptive text for changelog
/// the unreleased list of changelog entries
/// the list of changelog entries
- let create description unreleased entries =
- createWithCustomHeader "Changelog" description unreleased entries
+ /// the list of references
+ let create description unreleased entries references =
+ createWithCustomHeader "Changelog" description unreleased entries references
///
/// Create a changelog with given entries and default values for other data including
@@ -377,7 +422,7 @@ module Changelog =
///
///
/// the list of changelog entries
- let fromEntries entries = create None None entries
+ let fromEntries entries = create None None entries []
let internal isMainHeader line : bool = "# " <* line
let internal isVersionHeader line : bool = "## " <* line
@@ -538,7 +583,43 @@ module Changelog =
| h :: _ -> h
| _ -> "Changelog"
- Changelog.New(header, description, unreleased, entries)
+ // Move references from last changelog entry into references.
+ let entriesWithoutReferences, references =
+ let referenceRegex =
+ $"""^\[(({nugetRegex.ToString()})|Unreleased)\]: +(http[s]://.+)$"""
+ |> String.getRegEx
+
+ match entries with
+ | [] -> [], []
+ | entries ->
+ let front, lastChange = entries |> List.splitAt (List.length entries - 1)
+ let last = List.head lastChange
+
+ let refChanges, trueChanges =
+ last.Changes
+ |> List.partition (fun (change: Change) ->
+ referenceRegex.Match(change.ChangeText().CleanedText).Success)
+
+ let references =
+ refChanges
+ |> List.map (fun change ->
+ let referenceMatch = referenceRegex.Match(change.ChangeText().CleanedText)
+ let version = referenceMatch.Groups[1].Value
+ let uri = referenceMatch.Groups[6].Value
+
+ { SemVer =
+ if version = "Unreleased" then
+ UnreleasedRef
+ else
+ SemVerRef(SemVer.parse version)
+ RepoUrl = Uri(uri) })
+
+ let newLastEntry =
+ ChangelogEntry.New(last.AssemblyVersion, last.NuGetVersion, trueChanges)
+
+ front @ [ newLastEntry ], references
+
+ Changelog.New(header, description, unreleased, entriesWithoutReferences, references)
///
/// Parses a Changelog text file and returns the latest changelog.
diff --git a/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs b/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs
index 5fe826d8df1..9a25bf72aac 100644
--- a/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs
+++ b/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs
@@ -2,6 +2,37 @@ module Fake.Core.ReleaseNotesTests
open Fake.Core
open Expecto
+open System
+
+[]
+let private changelogReleasesText =
+ """# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## Unreleased
+
+### Changed
+- Foo 2
+
+## [0.1.0-pre.2] - 2023-10-19
+
+### Added
+- Foo 1
+
+## [0.1.0-pre.1] - 2023-10-11
+
+### Added
+- Foo 0"""
+
+[]
+let private changelogReferencesText =
+ """[Unreleased]: https://github.com/bogus/Foo/compare/v0.1.0-pre.2...HEAD
+[0.1.0-pre.2]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.2
+[0.1.0-pre.1]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.1"""
[]
let tests =
@@ -245,4 +276,108 @@ let tests =
checkPreRelease releaseNotesLines_case2 (Some "---RC-SNAPSHOT.12.9.1--.12") (Some "788")
checkPreRelease releaseNotesLines_case3 (Some "---R-S.12.9.1--.12") (Some "meta")
checkPreRelease releaseNotesLines_case4 (None) (Some "0.build.1-rc.10000aaa-kk-0.1")
- checkPreRelease releaseNotesLines_case5 (Some "0A.is.legal") (None) ] ]
+ checkPreRelease releaseNotesLines_case5 (Some "0A.is.legal") (None) ]
+
+ // https://keepachangelog.com
+ testList
+ "Changelog"
+ [ testCase "Test that we can parse changelog without references"
+ <| fun _ ->
+ let changelog = changelogReleasesText |> String.splitStr "\n" |> Changelog.parse
+
+ Expect.isEmpty changelog.References "References not empty"
+ Expect.isSome changelog.Unreleased "Unreleased section empty"
+ Expect.hasLength changelog.Entries 2 "Wrong number of release entries parsed"
+ testCase "Test that we can parse changelog with references"
+ <| fun _ ->
+ let changelogText = changelogReleasesText + "\n\n" + changelogReferencesText
+ let changelog = changelogText |> String.splitStr "\n" |> Changelog.parse
+
+ Expect.hasLength changelog.References 3 "Wrong number of references parsed"
+
+ Expect.hasLength
+ (changelog.References
+ |> List.filter (fun r ->
+ match r.SemVer with
+ | Changelog.SemVerRef (_) -> true
+ | _ -> false))
+ 2
+ "Wrong number of released references parsed"
+
+ Expect.hasLength
+ (changelog.References
+ |> List.filter (fun r ->
+ match r.SemVer with
+ | Changelog.SemVerRef (_) -> false
+ | _ -> true))
+ 1
+ "Wrong number of unreleased references parsed"
+
+ Expect.hasLength changelog.References 3 "Wrong number of references parsed"
+ Expect.isSome changelog.Unreleased "Unreleased section empty"
+ Expect.hasLength changelog.Entries 2 "Wrong number of release entries parsed"
+ testCase "Test that references are not in the last changelog entry"
+ <| fun _ ->
+ let changelogText = changelogReleasesText + "\n\n" + changelogReferencesText
+ let changelog = changelogText |> String.splitStr "\n" |> Changelog.parse
+ let lastEntry = changelog.Entries |> List.last
+ let lastChanges = lastEntry.Changes
+
+ Expect.isFalse
+ (lastChanges
+ |> List.exists (fun change ->
+ change.ChangeText().CleanedText.Contains("https://github.com/bogus/Foo/")))
+ "URL of reference contained in change text"
+ testCase "Test that a release and reference can be added and correctly turned into a string"
+ <| fun _ ->
+ let changelogText = changelogReleasesText + "\n\n" + changelogReferencesText
+ let changelog = changelogText |> String.splitStr "\n" |> Changelog.parse
+ let versionText = "0.1.0-pre.3"
+ let semVerInfo = SemVer.parse versionText
+
+ let newUnreleasedRef =
+ { Changelog.Reference.SemVer = Changelog.UnreleasedRef
+ Changelog.Reference.RepoUrl = Uri("https://github.com/bogus/Foo/compare/v0.1.0-pre.3...HEAD") }
+
+ let releasedRefs =
+ changelog.References
+ |> List.filter (fun r ->
+ match r.SemVer with
+ | Changelog.SemVerRef (_) -> true
+ | _ -> false)
+
+ let newReference =
+ { Changelog.Reference.SemVer = Changelog.SemVerRef(semVerInfo)
+ Changelog.Reference.RepoUrl = Uri("https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.3") }
+
+ let newFixed =
+ Changelog.Fixed(
+ { CleanedText = "Foo 3"
+ OriginalText = None }
+ )
+
+ let newReleaseEntry =
+ Changelog.ChangelogEntry.New(
+ "",
+ versionText,
+ Some(DateTime(2023, 11, 23)),
+ None,
+ [ newFixed ],
+ false
+ )
+
+ let changelogNew =
+ { changelog with
+ Entries = newReleaseEntry :: changelog.Entries
+ References = [ newUnreleasedRef; newReference ] @ releasedRefs }
+
+ let expectedEnd =
+ """[Unreleased]: https://github.com/bogus/Foo/compare/v0.1.0-pre.3...HEAD
+[0.1.0-pre.3]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.3
+[0.1.0-pre.2]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.2
+[0.1.0-pre.1]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.1"""
+
+ Expect.stringEnds
+ (changelogNew.ToString())
+ expectedEnd
+ "Invalid references at end of changelog text" ] ]