Skip to content
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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,41 @@ on:
- "*.*.*"

jobs:
# On pull requests, only linux-x64 runs, to prove AOT still links without burning the full matrix
# on packages nobody sees. All five run on push, where the packages are actually uploaded.
aot-pack:
runs-on: ${{ matrix.runner }}
name: AOT pack (${{ matrix.rid }})
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(github.event_name == 'pull_request'
&& '[{"rid":"linux-x64","runner":"ubuntu-latest"}]'
|| '[{"rid":"linux-x64","runner":"ubuntu-latest"},{"rid":"linux-arm64","runner":"ubuntu-24.04-arm"},{"rid":"win-x64","runner":"windows-latest"},{"rid":"win-arm64","runner":"windows-11-arm"},{"rid":"osx-arm64","runner":"macos-latest"}]') }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 1
- uses: actions/setup-dotnet@v5
with:
dotnet-version: |
10.0.x

- run: dotnet pack src/release-notes/release-notes.csproj -c Release -r ${{ matrix.rid }} -o build/output
name: Pack native-AOT tool for ${{ matrix.rid }}
shell: bash

- name: Upload per-RID package
if: github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: nupkg-${{ matrix.rid }}
path: build/output/*.nupkg
if-no-files-found: error

build:
runs-on: ubuntu-latest
needs: aot-pack
steps:
- uses: actions/checkout@v5
with:
Expand All @@ -37,6 +70,15 @@ jobs:
name: Build
- run: ./build.sh generatepackages -s true
name: Generate local nuget packages

- name: Download per-RID AOT packages
if: github.event_name == 'push'
uses: actions/download-artifact@v4
with:
pattern: nupkg-*
path: build/output
merge-multiple: true

- run: ./build.sh validatepackages -s true
name: "validate *.npkg files that were created"
- run: ./build.sh generateapichanges -s true
Expand Down
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<PropertyGroup>
<MinVerDefaultPreReleaseIdentifiers>canary.0</MinVerDefaultPreReleaseIdentifiers>
<MinVerMinimumMajorMinor>0.1</MinVerMinimumMajorMinor>
<!-- Nullean.Argh's source-generated dispatch code uses C# 13 syntax regardless of the TFM it targets. -->
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MinVer" Version="6.0.0">
Expand Down
105 changes: 82 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,98 @@ Distributed as a .NET tool so install using the following
dotnet tool install release-notes
```

## Run
On Linux, Windows and macOS/arm64, this resolves to a self-contained native-AOT executable — no
shared .NET runtime required, and no first-run JIT warmup. Everywhere else, it falls back to a
framework-dependent build (requires the .NET runtime the tool targets to already be installed).

## Run

```bat
dotnet release-notes
dotnet release-notes <command> <owner> <repository-name> --version <string> [options]
```

You can omit `dotnet` if you install this as a global tool.

> [!IMPORTANT]
> Starting from `1.0.0`, every invocation requires an explicit command name (`generate`,
> `apply-labels`, `find-previous`, `current-version` or `create-release`) — there is no longer a bare
> `release-notes <owner> <repo> ...` shorthand for `generate`. `--label` also changes from two tokens
> (`--label <label> <description>`) to one combined token (`--label <label>=<description>`), and every
> other flag moves from a single word to kebab-case (e.g. `--oldversion` → `--old-version`). This is a
> breaking change from earlier `0.x` releases, a side effect of moving off `Argu` (which no longer
> worked once this tool was AOT-compiled) onto [`Nullean.Argh`](https://github.com/nullean/argh).

```bat
Usage: release-notes <namespace|command> [options]

Commands:
apply-labels Creates the version and backport labels for the next release.
create-release Makes sure the tag exists as a release on GitHub, and
introduces new version labels for the next major/minor/patch.
current-version Given a search query, finds the current and the next versions
and prints them on separate lines.
find-previous Finds and prints the previous release for the given version.
generate Generates release notes for version from closed GitHub issues
and PRs, printed to standard out and optionally written to a
file.
```

You can omit `dotnet` if you install this as a global tool
Every command takes the same `<owner> <repository-name>` positionals and shared options:

```bat
USAGE: release-notes [--help] [--label <label> <description>] [--token <string>] [--version <string>]
[--oldversion <string>] [--releaselabel <string>] [--uncategorizedheader <string>]
<owner> <repository name>
Arguments:
<owner> GitHub repository owner.
<repository-name> GitHub repository name.

REPOSITORY:
Options:
--version <string> [required] Version that is being released.
--token <string> The GitHub token to use. If the issue list is long this may be necessary; defaults to anonymous.
--old-version <string> The previous version to generate release notes since. Optional; the tool will find the previous release.
--release-tag-format <string> The release tag format. VERSION is replaced by the actual version. [default: VERSION]
--release-label-format <string> The release label format. VERSION is replaced by the actual version. [default: vVERSION]
--backport-label-format <string> The backport label format, e.g. "Backport BRANCH". BRANCH is calculated from the version.
--format <enum> The format in which to print the results: Markdown or AsciiDoc. [default: markdown]
--uncategorized-header <string> The header to use in the markdown for uncategorized issues/PRs.
--label <values> [repeatable] Map a GitHub label to a categorization heading, formatted as <label>=<description>. May be given
more than once. Defaults to bug=Bug Fixes, enhancement=New Features, documentation=Documentation Improvements.
--output <string> Write the release notes to a file as well as standard out. VERSION is replaced by the actual version.
```

<owner> <repository name>
Repository to use in <owner> <repos_name> format
#### Examples

OPTIONS:
Generate markdown release notes to standard out (and print to a file with `--output`):

--label <label> <description>
Map Github labels to categorizations, format <label> <description>, can be specified
multiple times
--token <string> The github token to use, if the issue list is long this may be necessary, defaults to
anonymoys
--version <string> The version that we are generating release notes for
--oldversion <string> The previous version to generates release notes since
--releaselabel <string>
The version label on the issues / github prs, defaults to v[VERSION]
--uncategorizedheader <string>
The header to use in the markdown for uncategorized issues/prs
--help display this list of options.
```bat
dotnet release-notes generate nullean release-notes --version 1.0.0
```

#### Examples:
Find the previous release for a version:

```bat
dotnet release-notes find-previous nullean release-notes --version 1.0.0
```

Given a search query (`M.N`, `M.x`, or `master`/`main`), print the current and next version on separate lines:

```bat
dotnet release-notes current-version nullean release-notes --version 1.0.0 --query master
```

Create the version and backport labels for the next release:

```bat
dotnet release-notes apply-labels nullean release-notes --version 1.0.0 --backport-label-format "Backport BRANCH"
```

Make sure the tag exists as a release on GitHub, using one or more files for the release body, and
introduce new version labels for the next major/minor/patch:

```bat
dotnet release-notes create-release nullean release-notes --version 1.0.0 --body notes.md --body breaking-changes.md --token $GITHUB_TOKEN
```

Map GitHub labels to custom categorization headings (replaces the default bug/enhancement/documentation set):

```bat
dotnet release-notes generate nullean release-notes --version 1.0.0 --label bug=Bug Fixes --label enhancement=New Features
```
8 changes: 8 additions & 0 deletions build/scripts/Paths.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ open System.IO
let ToolName = "release-notes"
let Repository = sprintf "nullean/%s" ToolName

/// The RIDs we ship native-AOT tool packages for. AOT compilation requires a matching
/// OS/arch, so CI packs one RID per runner; this list only documents the set.
let AotRuntimeIdentifiers = ["linux-x64"; "linux-arm64"; "win-x64"; "win-arm64"; "osx-arm64"]

/// Must mirror release-notes.csproj's TargetFrameworks. Used to patch the signed managed dll back
/// into the packed 'any' fallback for every TFM it ships — see fixAnyPackageSigning in Targets.fs.
let ManagedTargetFrameworks = ["net8.0"; "net10.0"]

let Root =
let mutable dir = DirectoryInfo(".")
while dir.GetFiles("*.slnx").Length = 0 do dir <- dir.Parent
Expand Down
71 changes: 61 additions & 10 deletions build/scripts/Targets.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Targets
open Argu
open System
open System.IO
open System.IO.Compression
open Bullseye
open CommandLine
open Fake.Tools.Git
Expand Down Expand Up @@ -34,13 +35,62 @@ let private pristineCheck (arguments:ParseResults<Arguments>) =
| true -> printfn "The checkout folder does not have pending changes, proceeding"
| _ -> failwithf "The checkout folder has pending changes, aborting"

let private isPerRidPackage (name: string) =
Paths.AotRuntimeIdentifiers |> List.exists (fun rid -> name.Contains(sprintf ".%s." rid))

/// `dotnet pack`'s RID-aware tool-packaging path (used once RuntimeIdentifiers is declared) copies
/// the *unsigned* obj/ build of this project's own assembly into the portable 'any' package, even
/// though the normal bin/ output is correctly strong-name signed — a long-standing obj-vs-bin mixup
/// in `dotnet pack` (see https://github.com/dotnet/sdk/issues/20197) that resurfaces here. Patched in
/// place after packing by swapping in the signed bin/ copies for every TFM the 'any' package ships.
let private fixAnyPackageSigning (anyPackagePath: string) =
use archive = ZipFile.Open(anyPackagePath, ZipArchiveMode.Update)
for tfm in Paths.ManagedTargetFrameworks do
let entryName = sprintf "tools/%s/any/%s.dll" tfm Paths.ToolName
let signedDll = Path.Combine(Paths.ToolProject.FullName, "bin", "Release", tfm, sprintf "%s.dll" Paths.ToolName)
match archive.GetEntry(entryName), File.Exists signedDll with
| null, _ | _, false -> ()
| entry, true ->
entry.Delete()
let newEntry = archive.CreateEntry(entryName)
use entryStream = newEntry.Open()
use fileStream = File.OpenRead(signedDll)
fileStream.CopyTo(entryStream)

let private generatePackages (arguments:ParseResults<Arguments>) =
let output = Paths.RootRelative Paths.Output.FullName
exec "dotnet" ["pack"; "-c"; "Release"; "-o"; output] |> ignore

if not Paths.Output.Exists then Paths.Output.Create()

// A plain `dotnet pack` emits the root package (whose DotnetToolSettings.xml v2 maps each RID to
// its own package) AND a package per RID — but native AOT can only compile for the machine it
// runs on, so those per-RID outputs from a single machine are self-contained MANAGED builds,
// silently missing the AOT compilation. We therefore keep only the root and the portable 'any'
// fallback here, and take the real per-RID packages from the CI matrix, where each is compiled
// on a matching runner (see aot-pack in .github/workflows/ci.yml).
let staging = Paths.RootRelative <| Path.Combine(Paths.Output.FullName, "..", "release-notes-staging")
if Directory.Exists staging then Directory.Delete(staging, true)
exec "dotnet" ["pack"; sprintf "src/%s/%s.csproj" Paths.ToolName Paths.ToolName; "-c"; "Release"; "-o"; staging] |> ignore

DirectoryInfo(staging).GetFiles("*.nupkg")
|> Seq.filter (fun f -> not (isPerRidPackage f.Name))
|> Seq.iter (fun f ->
let destination = Path.Combine(Paths.Output.FullName, f.Name)
printfn "keeping %s" f.Name
f.CopyTo(destination, true) |> ignore
if f.Name.Contains(sprintf "%s.any." Paths.ToolName) then
fixAnyPackageSigning destination)

Directory.Delete(staging, true)

let private validatePackages (arguments:ParseResults<Arguments>) =
let nugetPackage =
let p = Paths.Output.GetFiles("*.nupkg") |> Seq.sortByDescending(fun f -> f.CreationTimeUtc) |> Seq.head
// Only the 'any' package carries a signed managed assembly to check: the root package is
// just a DotnetToolSettings.xml pointer with no dll of its own, and the per-RID AOT packages
// hold a native binary with no managed identity either.
let p =
Paths.Output.GetFiles("*.nupkg")
|> Seq.filter (fun f -> f.Name.Contains(sprintf "%s.any." Paths.ToolName))
|> Seq.sortByDescending(fun f -> f.CreationTimeUtc) |> Seq.head
Paths.RootRelative p.FullName
exec "dotnet" ["nupkg-validator"; nugetPackage; "-v"; currentVersionInformational.Value; "-a"; Paths.ToolName; "-k"; "96c599bbe3e70f5d"] |> ignore

Expand Down Expand Up @@ -68,11 +118,12 @@ let private generateReleaseNotes (arguments:ParseResults<Arguments>) =
| None -> []
| Some token -> ["--token"; token;]
let releaseNotesArgs =
(Paths.Repository.Split("/") |> Seq.toList)
["generate"]
@ (Paths.Repository.Split("/") |> Seq.toList)
@ ["--version"; currentVersion
"--label"; "enhancement"; "New Features"
"--label"; "bug"; "Bug Fixes"
"--label"; "documentation"; "Docs Improvements"
"--label"; "enhancement=New Features"
"--label"; "bug=Bug Fixes"
"--label"; "documentation=Docs Improvements"
] @ tokenArgs
@ ["--output"; output]

Expand All @@ -89,9 +140,9 @@ let private createReleaseOnGithub (arguments:ParseResults<Arguments>) =
let releaseNotes = Paths.RootRelative <| Path.Combine(Paths.Output.FullName, sprintf "release-notes-%s.md" currentVersion)
let breakingChanges = Paths.RootRelative <| Path.Combine(Paths.Output.FullName, "github-breaking-changes-comments.md")
let releaseArgs =
(Paths.Repository.Split("/") |> Seq.toList)
@ ["create-release"
"--version"; currentVersion
["create-release"]
@ (Paths.Repository.Split("/") |> Seq.toList)
@ ["--version"; currentVersion
"--body"; releaseNotes;
"--body"; breakingChanges;
] @ tokenArgs
Expand Down
2 changes: 1 addition & 1 deletion release-notes.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
<Project Path="build/scripts/scripts.fsproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/release-notes/release-notes.fsproj" />
<Project Path="src/release-notes/release-notes.csproj" />
</Folder>
</Solution>
Loading
Loading