|
| 1 | +module ReleaseFromNotesTask |
| 2 | + |
| 3 | +open BlackFox.Fake |
| 4 | +open Fake.Core |
| 5 | +open Fake.DotNet |
| 6 | +open Fake.IO |
| 7 | +open Fake.IO.Globbing.Operators |
| 8 | +open Fake.Tools |
| 9 | + |
| 10 | +open ProjectInfo |
| 11 | + |
| 12 | +open System.Text.RegularExpressions |
| 13 | + |
| 14 | +// Owns *all* logic for the "release on RELEASE_NOTES.md change" CI flow: |
| 15 | +// parse the topmost ### header, decide whether to skip (Unreleased / tag |
| 16 | +// already exists), and otherwise run the full release pipeline (clean → |
| 17 | +// build → test → pack → push → tag). The GitHub Actions workflow stays |
| 18 | +// thin and only invokes this target. |
| 19 | +// |
| 20 | +// Why duplicate a few lines from PackageTasks / ReleaseTasks instead of |
| 21 | +// chaining their build-tasks? Those targets gate on interactive prompts |
| 22 | +// (auto-accepted via CI=true) and unconditionally chain clean + build + |
| 23 | +// test, which we want to skip entirely when the gate says no release. |
| 24 | +// Keeping a self-contained body lets the gate short-circuit cleanly. |
| 25 | + |
| 26 | +/// First "### <version>" line from RELEASE_NOTES.md (raw, so we can detect |
| 27 | +/// the (Unreleased) marker — Fake.Core.ReleaseNotes.load strips it). |
| 28 | +let private firstHeaderLine () = |
| 29 | + System.IO.File.ReadLines "RELEASE_NOTES.md" |
| 30 | + |> Seq.tryFind (fun line -> line.StartsWith "### ") |
| 31 | + |
| 32 | +let private tagExists (tag: string) = |
| 33 | + // `git rev-parse --verify --quiet refs/tags/<tag>` exits 0 iff the ref resolves. |
| 34 | + let ok, _, _ = |
| 35 | + Git.CommandHelper.runGitCommand "" (sprintf "rev-parse --verify --quiet refs/tags/%s" tag) |
| 36 | + ok |
| 37 | + |
| 38 | +let private cleanArtifacts () = |
| 39 | + !! "src/**/bin" |
| 40 | + ++ "src/**/obj" |
| 41 | + ++ "tests/**/bin" |
| 42 | + ++ "tests/**/obj" |
| 43 | + ++ pkgDir |
| 44 | + |> Shell.cleanDirs |
| 45 | + |
| 46 | +let private buildAndTest () = |
| 47 | + solutionFile |
| 48 | + |> DotNet.build (fun p -> |
| 49 | + { p with |
| 50 | + Configuration = DotNet.BuildConfiguration.fromString configuration |
| 51 | + MSBuildParams = { p.MSBuildParams with DisableInternalBinLog = true } } |
| 52 | + |> DotNet.Options.withCustomParams (Some "-tl")) |
| 53 | + |
| 54 | + for testProject in testProjects do |
| 55 | + testProject |
| 56 | + |> DotNet.test (fun p -> |
| 57 | + { p with |
| 58 | + Configuration = DotNet.BuildConfiguration.fromString configuration |
| 59 | + NoBuild = true |
| 60 | + MSBuildParams = { p.MSBuildParams with DisableInternalBinLog = true } } |
| 61 | + |> DotNet.Options.withCustomParams (Some "-tl")) |
| 62 | + |
| 63 | +// Mirrors PackageTasks.replaceCommitLink — keep PackageReleaseNotes free of |
| 64 | +// the `[[#sha](url)]` link prefixes some release notes carry. |
| 65 | +let private commitLinkPattern = Regex @"\[\[#[a-z0-9]*\]\(.*\)\] " |
| 66 | + |
| 67 | +let private packAll (version: string) = |
| 68 | + let notes = |
| 69 | + release.Notes |
| 70 | + |> List.map (fun line -> commitLinkPattern.Replace(line, "")) |
| 71 | + |> String.concat "\r\n" |
| 72 | + |
| 73 | + !! "src/**/*.*proj" |
| 74 | + -- "src/bin/*" |
| 75 | + |> Seq.iter (DotNet.pack (fun p -> |
| 76 | + let msb = |
| 77 | + { p.MSBuildParams with |
| 78 | + Properties = |
| 79 | + [ "Version", version |
| 80 | + "PackageReleaseNotes", notes ] |
| 81 | + @ p.MSBuildParams.Properties |
| 82 | + DisableInternalBinLog = true } |
| 83 | + { p with |
| 84 | + Configuration = DotNet.BuildConfiguration.fromString configuration |
| 85 | + MSBuildParams = msb |
| 86 | + OutputPath = Some pkgDir |
| 87 | + NoBuild = true } |
| 88 | + |> DotNet.Options.withCustomParams (Some "-tl"))) |
| 89 | + |
| 90 | +let private pushNupkgs () = |
| 91 | + let apikey = |
| 92 | + match Environment.environVarOrNone "NUGET_API_KEY" with |
| 93 | + | Some k when k <> "" -> k |
| 94 | + | _ -> failwith "NUGET_API_KEY env var is not set." |
| 95 | + let source = "https://api.nuget.org/v3/index.json" |
| 96 | + let artifacts = !! (sprintf "%s/*.*pkg" pkgDir) |> Seq.toList |
| 97 | + if List.isEmpty artifacts then |
| 98 | + failwithf "No packages found under %s/ to push." pkgDir |
| 99 | + for artifact in artifacts do |
| 100 | + let result = DotNet.exec id "nuget" (sprintf "push -s %s -k %s %s --skip-duplicate" source apikey artifact) |
| 101 | + if not result.OK then |
| 102 | + failwithf "dotnet nuget push failed for %s" artifact |
| 103 | + |
| 104 | +let private pushReleaseTag (version: string) = |
| 105 | + // GitHub Actions checkout doesn't preconfigure committer identity; set a |
| 106 | + // bot identity here so the annotated tag has author metadata. No FAKE |
| 107 | + // wrapper for `git config`, so use runSimpleGitCommand (throws on failure). |
| 108 | + Git.CommandHelper.runSimpleGitCommand "" "config user.name 'github-actions[bot]'" |> ignore |
| 109 | + Git.CommandHelper.runSimpleGitCommand "" "config user.email 'github-actions[bot]@users.noreply.github.com'" |> ignore |
| 110 | + // `Git.Branches.tag` creates a *lightweight* tag — we want an annotated |
| 111 | + // one so the release carries author + message metadata. Drop to the raw |
| 112 | + // git command for that, then use Git.Branches.pushTag for the push. |
| 113 | + Git.CommandHelper.runSimpleGitCommand "" (sprintf "tag -a %s -m \"Release %s\"" version version) |> ignore |
| 114 | + Git.Branches.pushTag "" "origin" version |
| 115 | + |
| 116 | +let releaseFromNotes = BuildTask.create "ReleaseFromNotes" [] { |
| 117 | + match firstHeaderLine () with |
| 118 | + | None -> |
| 119 | + failwith "RELEASE_NOTES.md has no '### <version>' header — cannot determine version to release." |
| 120 | + | Some header -> |
| 121 | + let version = stableVersionTag |
| 122 | + Trace.log (sprintf "Top RELEASE_NOTES.md entry: %s" header) |
| 123 | + Trace.log (sprintf "Parsed version: %s" version) |
| 124 | + if Regex.IsMatch(header, @"\(unreleased\)", RegexOptions.IgnoreCase) then |
| 125 | + Trace.log (sprintf "Entry is marked (Unreleased) — skipping release of %s." version) |
| 126 | + elif tagExists version then |
| 127 | + Trace.log (sprintf "Git tag %s already exists — skipping release." version) |
| 128 | + else |
| 129 | + Trace.log (sprintf "Releasing %s..." version) |
| 130 | + cleanArtifacts () |
| 131 | + buildAndTest () |
| 132 | + packAll version |
| 133 | + pushNupkgs () |
| 134 | + pushReleaseTag version |
| 135 | + Trace.log (sprintf "Released %s." version) |
| 136 | +} |
0 commit comments