Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse references at end of changelog as separate part of a changelog. #2779

Merged
merged 3 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 91 additions & 10 deletions src/app/Fake.Core.ReleaseNotes/Changelog.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -267,6 +277,32 @@ module Changelog =

assemblyVersion, nugetVersion

/// <summary>
/// A version or "Unreleased"
/// </summary>
type ReferenceVersion =
| SemVerRef of SemVerInfo
| UnreleasedRef

override x.ToString() =
match x with
| SemVerRef (semVerInfo) -> semVerInfo.ToString()
| UnreleasedRef -> "Unreleased"

/// <summary>
/// A reference from a version to a repository URL (e.g. a tag or a compare link)
/// </summary>
/// <code>
/// [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
/// </code>
type Reference =
{ SemVer: ReferenceVersion
RepoUrl: Uri }

override x.ToString() =
sprintf "[%s]: %s" (x.SemVer.ToString()) (x.RepoUrl.ToString())

/// <summary>
/// Holds data for a changelog file, which include changelog entries an other metadata
/// </summary>
Expand All @@ -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 =
Expand All @@ -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 =
Expand Down Expand Up @@ -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

Expand All @@ -358,8 +401,9 @@ module Changelog =
/// <param name="description">the descriptive text for changelog</param>
/// <param name="unreleased">the unreleased list of changelog entries</param>
/// <param name="entries">the list of changelog entries</param>
let createWithCustomHeader header description unreleased entries =
Changelog.New(header, description, unreleased, entries)
/// <param name="references">the list of references</param>
let createWithCustomHeader header description unreleased entries references =
Changelog.New(header, description, unreleased, entries, references)

/// <summary>
/// Create a changelog with given data
Expand All @@ -368,16 +412,17 @@ module Changelog =
/// <param name="description">the descriptive text for changelog </param>
/// <param name="unreleased">the unreleased list of changelog entries</param>
/// <param name="entries">the list of changelog entries</param>
let create description unreleased entries =
createWithCustomHeader "Changelog" description unreleased entries
/// <param name="references">the list of references</param>
let create description unreleased entries references =
createWithCustomHeader "Changelog" description unreleased entries references

/// <summary>
/// Create a changelog with given entries and default values for other data including
/// header and description.
/// </summary>
///
/// <param name="entries">the list of changelog entries</param>
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
Expand Down Expand Up @@ -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)

/// <summary>
/// Parses a Changelog text file and returns the latest changelog.
Expand Down
137 changes: 136 additions & 1 deletion src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ module Fake.Core.ReleaseNotesTests

open Fake.Core
open Expecto
open System

[<Literal>]
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"""

[<Literal>]
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"""

[<Tests>]
let tests =
Expand Down Expand Up @@ -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" ] ]