1- #r " nuget: Fun.Build, 1.0.2 "
1+ #r " nuget: Fun.Build, 1.0.3 "
22#r " nuget: Fake.IO.FileSystem, 6.0.0"
3+ #r " nuget: NuGet.Protocol, 6.7.0"
4+ #r " nuget: Ionide.KeepAChangelog, 0.1.8"
5+ #r " nuget: Humanizer.Core, 2.14.1"
36
7+ open System
48open System.Text .Json
9+ open System.Threading
510open Fake.IO
11+ open Fake.IO .FileSystemOperators
612open Fake.IO .Globbing .Operators
713open Fun.Build
14+ open Fun.Build .Internal
15+ open NuGet.Common
16+ open NuGet.Protocol
17+ open NuGet.Protocol .Core .Types
18+ open Ionide.KeepAChangelog
19+ open Ionide.KeepAChangelog .Domain
20+ open SemVersion
21+ open Humanizer
822
923let cleanDirs globExpr = (!! globExpr) |> Shell.cleanDirs
1024
1125/// Workaround for https://github.com/dotnet/sdk/issues/35989
12- let restoreTools ( ctx : Internal. StageContext) =
26+ let restoreTools ( ctx : StageContext ) =
1327 async {
1428 let json = File.readAsString " .config/dotnet-tools.json"
1529 let jsonDocument = JsonDocument.Parse( json)
@@ -39,6 +53,9 @@ let restoreTools (ctx: Internal.StageContext) =
3953 return 1
4054 }
4155
56+ let packStage =
57+ stage " pack" { run " dotnet pack ./src/Ionide.Analyzers/Ionide.Analyzers.fsproj -c Release -o bin" }
58+
4259pipeline " Build" {
4360 workingDir __ SOURCE_ DIRECTORY__
4461 stage " clean" {
@@ -63,7 +80,7 @@ pipeline "Build" {
6380 run " dotnet build --no-restore -c Release ionide-analyzers.sln"
6481 }
6582 stage " test" { run " dotnet test --no-restore --no-build -c Release" }
66- stage " pack " { run " dotnet pack ./src/Ionide.Analyzers/Ionide.Analyzers.fsproj -c Release -o bin " }
83+ packStage
6784 stage " docs" {
6885 envVars
6986 [|
@@ -89,4 +106,244 @@ pipeline "Docs" {
89106 runIfOnlySpecified true
90107}
91108
109+ let getLatestPublishedNugetVersion packageName =
110+ task {
111+ let logger = NullLogger.Instance
112+ let cancellationToken = CancellationToken.None
113+
114+ let cache = new SourceCacheContext()
115+ let repository = Repository.Factory.GetCoreV3( " https://api.nuget.org/v3/index.json" )
116+ let! resource = repository.GetResourceAsync< FindPackageByIdResource>()
117+ let! versions = resource.GetAllVersionsAsync( packageName, cache, logger, cancellationToken)
118+ if Seq.isEmpty versions then
119+ return None
120+ else
121+ return versions |> Seq.max |> Some
122+ }
123+
124+ let getLatestChangeLogVersion () : SemanticVersion * DateTime * ChangelogData option =
125+ let changelog = System.IO.FileInfo(__ SOURCE_ DIRECTORY__ </> " CHANGELOG.md" )
126+ let changeLogResult =
127+ match Parser.parseChangeLog changelog with
128+ | Error error -> failwithf " %A " error
129+ | Ok result -> result
130+
131+ changeLogResult.Releases
132+ |> List.sortByDescending ( fun ( _ , d , _ ) -> d)
133+ |> List.head
134+
135+ type CommandRunner =
136+ abstract member LogWhenDryRun: string -> unit
137+ abstract member RunCommand: string -> Async < Result < unit , string >>
138+ abstract member RunCommandCaptureOutput: string -> Async < Result < string , string >>
139+
140+ /// Push *.nupkg
141+ let releaseNuGetPackage ( ctx : CommandRunner ) ( version : SemanticVersion , _ , _ ) =
142+ async {
143+ let key = Environment.GetEnvironmentVariable " NUGET_KEY"
144+
145+ let! result =
146+ ctx.RunCommand
147+ $" dotnet nuget push bin/Ionide.Analyzers.%s {string version}.nupkg --api-key {key} --source \" https://api.nuget.org/v3/index.json\" "
148+
149+ match result with
150+ | Error _ -> return 1
151+ | Ok _ -> return 0
152+ }
153+
154+ type GithubRelease =
155+ {
156+ /// Is not suffixed with `v`
157+ Version: string
158+ Title: string
159+ Date: DateTime
160+ Draft: string
161+ }
162+
163+ let mapToGithubRelease ( v : SemanticVersion , d : DateTime , cd : ChangelogData option ) =
164+ match cd with
165+ | None -> failwith " Each Ionide.Analyzers release is expected to have at least one section."
166+ | Some cd ->
167+
168+ let version = $" {v.Major}.{v.Minor}.{v.Patch}"
169+ let title =
170+ let month = d.ToString( " MMMM" )
171+ let day = d.Day.Ordinalize()
172+ $" {month} {day} Release"
173+
174+ let sections =
175+ [
176+ " Added" , cd.Added
177+ " Changed" , cd.Changed
178+ " Fixed" , cd.Fixed
179+ " Deprecated" , cd.Deprecated
180+ " Removed" , cd.Removed
181+ " Security" , cd.Security
182+ yield ! ( Map.toList cd.Custom)
183+ ]
184+ |> List.choose ( fun ( header , lines ) ->
185+ if lines.IsEmpty then
186+ None
187+ else
188+ lines
189+ |> List.map ( fun line -> line.TrimStart())
190+ |> String.concat " \n "
191+ |> sprintf " ### %s \n %s " header
192+ |> Some
193+ )
194+ |> String.concat " \n\n "
195+
196+ let draft =
197+ $""" # {version}
198+
199+ {sections}"""
200+
201+ {
202+ Version = version
203+ Title = title
204+ Date = d
205+ Draft = draft
206+ }
207+
208+ let getReleaseNotes ( ctx : CommandRunner ) ( currentRelease : GithubRelease ) ( previousReleaseDate : string option ) =
209+ async {
210+ let closedFilter =
211+ match previousReleaseDate with
212+ | None -> " "
213+ | Some date -> $" closed:>%s {date}"
214+
215+ let! authorsStdOut =
216+ ctx.RunCommandCaptureOutput
217+ $" gh pr list -S \" state:closed base:main %s {closedFilter} -author:app/robot -author:app/dependabot\" --json author --jq \" .[].author.login\" "
218+
219+ let authorMsg =
220+ match authorsStdOut with
221+ | Error e -> failwithf $" Could not get authors: %s {e}"
222+ | Ok stdOut ->
223+
224+ let authors =
225+ stdOut.Split([| '\n' |], StringSplitOptions.RemoveEmptyEntries)
226+ |> Array.distinct
227+ |> Array.sort
228+
229+ if authors.Length = 1 then
230+ $" Special thanks to @%s {authors.[0]}!"
231+ else
232+ let lastAuthor = Array.last authors
233+ let otherAuthors =
234+ if authors.Length = 2 then
235+ $" @{authors.[0]}"
236+ else
237+ authors
238+ |> Array.take ( authors.Length - 1 )
239+ |> Array.map ( sprintf " @%s " )
240+ |> String.concat " , "
241+ $" Special thanks to %s {otherAuthors} and @%s {lastAuthor}!"
242+
243+ return
244+ $""" {currentRelease.Draft}
245+
246+ {authorMsg}
247+
248+ [https://www.nuget.org/packages/Ionide.Analyzers/{currentRelease.Version}](https://www.nuget.org/packages/Ionide.Analyzers/{currentRelease.Version})
249+ """
250+ }
251+
252+ /// <summary>
253+ /// Create a GitHub release via the CLI.
254+ /// </summary>
255+ /// <param name="ctx"></param>
256+ /// <param name="currentVersion">From the ChangeLog file.</param>
257+ /// <param name="previousReleaseDate">Filter used to find the users involved in the release. This will be passed a parameter to the GitHub CLI.</param>
258+ let mkGitHubRelease
259+ ( ctx : CommandRunner )
260+ ( currentVersion : SemanticVersion * DateTime * ChangelogData option )
261+ ( previousReleaseDate : string option )
262+ =
263+ async {
264+ let ghReleaseInfo = mapToGithubRelease currentVersion
265+ let! notes = getReleaseNotes ctx ghReleaseInfo previousReleaseDate
266+ ctx.LogWhenDryRun $" NOTES:\n %s {notes}"
267+ let noteFile = System.IO.Path.GetTempFileName()
268+ System.IO.File.WriteAllText( noteFile, notes)
269+ let file = $" ./bin/Ionide.Analyzers.%s {ghReleaseInfo.Version}.nupkg"
270+
271+ let! releaseResult =
272+ ctx.RunCommand
273+ $" gh release create v%s {ghReleaseInfo.Version} {file} --title \" {ghReleaseInfo.Title}\" --notes-file \" {noteFile}\" "
274+
275+ if System.IO.File.Exists noteFile then
276+ System.IO.File.Delete( noteFile)
277+
278+ match releaseResult with
279+ | Error _ -> return 1
280+ | Ok _ -> return 0
281+ }
282+
283+ pipeline " Release" {
284+ workingDir __ SOURCE_ DIRECTORY__
285+ stage " Release " {
286+ packStage
287+ run ( fun ctx ->
288+ async {
289+ let commandRunner =
290+ match ctx.TryGetCmdArg " --dry-run" with
291+ | ValueNone ->
292+ { new CommandRunner with
293+ member x.LogWhenDryRun _ = ()
294+ member x.RunCommand command = ctx.RunCommand command
295+ member x.RunCommandCaptureOutput command = ctx.RunCommandCaptureOutput command
296+ }
297+ | ValueSome _ ->
298+ { new CommandRunner with
299+ member x.LogWhenDryRun msg = printfn " %s " msg
300+ member x.RunCommand command =
301+ async {
302+ printfn $" [dry-run]:{command}"
303+ return Ok()
304+ }
305+ member x.RunCommandCaptureOutput command =
306+ async {
307+ printfn $" [dry-run]:{command}"
308+ return Ok " nojaf\n dawedawe\n baronfel"
309+ }
310+ }
311+
312+ let currentVersion = getLatestChangeLogVersion ()
313+ let currentVersionText , _ , _ = currentVersion
314+ let! latestNugetVersion = getLatestPublishedNugetVersion " Ionide.Analyzers" |> Async.AwaitTask
315+ match latestNugetVersion with
316+ | None ->
317+ let! nugetResult = releaseNuGetPackage commandRunner currentVersion
318+ let! githubResult = mkGitHubRelease commandRunner currentVersion None
319+ return nugetResult + githubResult
320+
321+ | Some nugetVersion when ( nugetVersion.OriginalVersion <> string currentVersionText) ->
322+ let! nugetResult = releaseNuGetPackage commandRunner currentVersion
323+ let! previousReleaseDate =
324+ ctx.RunCommandCaptureOutput
325+ $" gh release view v%s {nugetVersion.OriginalVersion} --json createdAt -t \" {{{{.createdAt}}}}\" "
326+
327+ let previousReleaseDate =
328+ match previousReleaseDate with
329+ | Error e ->
330+ printfn " Unable to format previous release data, %s " e
331+ None
332+ | Ok d ->
333+ let output = d.Trim()
334+ let lastIdx = output.LastIndexOf( " Z" , StringComparison.Ordinal)
335+ Some( output.Substring( 0 , lastIdx))
336+
337+ let! githubResult = mkGitHubRelease commandRunner currentVersion previousReleaseDate
338+ return nugetResult + githubResult
339+
340+ | Some nugetVersion ->
341+ printfn " %s is already published" nugetVersion.OriginalVersion
342+ return 0
343+ }
344+ )
345+ }
346+ runIfOnlySpecified true
347+ }
348+
92349tryPrintPipelineCommandHelp ()
0 commit comments