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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ jobs:
name: Pack native-AOT tool for ${{ matrix.rid }}
shell: bash

# Container image, linux-x64 only for now — the platform every GitHub-hosted Linux runner
# already matches. No --push here and no registry login on this leg, so this only proves the
# container build itself still works: AOT compiles, the chiseled base image resolves, and the
# SDK's container tooling produces an image in the local Docker daemon. The real push to
# ghcr.io happens once, in the build job below, which does pass --push.
- name: Build container image (no push)
if: matrix.rid == 'linux-x64'
run: ./build.sh publishcontainers -s true
shell: bash

- name: Report container image size
if: matrix.rid == 'linux-x64'
shell: bash
run: docker images nullean/release-notes --format '{{.Tag}}\t{{.Size}}'

- name: Upload per-RID package
if: github.event_name == 'push'
uses: actions/upload-artifact@v4
Expand All @@ -50,6 +65,10 @@ jobs:
build:
runs-on: ubuntu-latest
needs: aot-pack
permissions:
contents: write # create GitHub release
packages: write # push to GitHub Packages and ghcr.io
issues: write # release-notes creates labels for its categories
steps:
- uses: actions/checkout@v5
with:
Expand Down Expand Up @@ -90,6 +109,21 @@ jobs:
run: |
until dotnet nuget push 'build/output/*.nupkg' -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate --no-symbols true; do echo "Retrying"; sleep 1; done;

# ghcr.io/nullean/release-notes, matching curb's own tagging: "edge" on every push, plus
# "latest" and the semver when this push is an exact release tag — see
# publishContainers/containerImageTags in Targets.fs for how that split is decided.
- name: Log in to ghcr.io
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Publish container image
if: github.event_name == 'push'
run: ./build.sh publishcontainers -s true --push

- run: ./build.sh generatereleasenotes -s true
name: Generate release notes for tag
if: github.event_name == 'push' && startswith(github.ref, 'refs/tags')
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/create-major-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: create-major-tag

# Lets a workflow pin this repo as a GitHub Action with `uses: nullean/release-notes@v1` instead of
# an exact release tag, the same way actions/checkout@v5 stays on v5 across patch and minor releases.

on:
release:
types:
- published

permissions: {}

concurrency:
group: create-major-tag
cancel-in-progress: false

jobs:
create-major-tag:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: true

- name: Validate and extract major version
env:
TAG: ${{ github.event.release.tag_name }}
run: |
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Tag '${TAG}' does not match expected semver format (N.N.N)"
exit 1
fi
echo "MAJOR_VERSION=${TAG%%.*}" >> "${GITHUB_ENV}"

- name: Create major tag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git tag "v${MAJOR_VERSION}"
git push -f origin "refs/tags/v${MAJOR_VERSION}"
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,35 @@ On Linux, Windows and macOS/arm64, this resolves to a self-contained native-AOT
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).

## GitHub Action

```yaml
- uses: nullean/release-notes@main
with:
command: generate
owner: nullean
repo: release-notes
args: --version 1.0.0
```

Runs `release-notes` from a pre-built, distroless container (`ghcr.io/nullean/release-notes`) — no .NET
SDK install needed in the workflow. `command` is one of `generate`, `apply-labels`, `find-previous`,
`current-version`, or `create-release`; extra flags pass through verbatim via `args`. Linux runners only
(`ubuntu-latest` and similar) — container actions can't run on Windows or macOS runners.

## Container image

`ghcr.io/nullean/release-notes` also works as a general-purpose container, outside GitHub Actions —
GitLab CI, a local machine without the .NET SDK, anywhere `docker run` works:

```sh
docker run --rm ghcr.io/nullean/release-notes:edge generate nullean release-notes --version 1.0.0 --token "$GITHUB_TOKEN"
```

Distroless: native-AOT, chiseled `runtime-deps` base, no shell, runs as a non-root user. Tags follow
`release-notes`'s own releases — `edge` tracks the latest commit on `master`, `latest` and a semver tag
(e.g. `0.10.0`) follow tagged releases.

## Run

```bat
Expand Down
27 changes: 27 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: 'release-notes'
description: 'Generate release notes from closed GitHub issues and PRs, grouped by version label'
branding:
icon: 'file-text'
color: 'blue'
inputs:
command:
description: 'One of: generate, apply-labels, find-previous, current-version, create-release'
required: true
owner:
description: 'GitHub repository owner'
required: true
repo:
description: 'GitHub repository name'
required: true
args:
description: 'Extra arguments passed through to release-notes verbatim, e.g. "--version 1.0.0 --label bug=Bug Fixes"'
required: false
default: ''
runs:
using: 'docker'
image: 'docker://ghcr.io/nullean/release-notes:edge'
args:
- ${{ inputs.command }}
- ${{ inputs.owner }}
- ${{ inputs.repo }}
- ${{ inputs.args }}
4 changes: 4 additions & 0 deletions build/scripts/CommandLine.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ type Arguments =
| [<CliPrefix(CliPrefix.None);SubCommand>] Release

| [<CliPrefix(CliPrefix.None);Hidden;SubCommand>] CreateReleaseOnGithub
| [<CliPrefix(CliPrefix.None);Hidden;SubCommand>] PublishContainers
| [<CliPrefix(CliPrefix.None);SubCommand>] Publish

| [<Inherit;AltCommandLine("-s")>] SingleTarget of bool
| [<Inherit>] Token of string
| [<Inherit>] Push
with
interface IArgParserTemplate with
member this.Usage =
Expand All @@ -30,13 +32,15 @@ with

| SingleTarget _ -> "Runs the provided sub command without running their dependencies"
| Token _ -> "Token to be used to authenticate with github"
| Push -> "publishcontainers only: push the built image to ghcr.io instead of building it into the local Docker daemon"

| PristineCheck
| GeneratePackages
| ValidatePackages
| GenerateReleaseNotes
| GenerateApiChanges
| CreateReleaseOnGithub
| PublishContainers
-> "Undocumented, dependent target"
member this.Name =
match FSharpValue.GetUnionFields(this, typeof<Arguments>) with
Expand Down
42 changes: 41 additions & 1 deletion build/scripts/Targets.fs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,45 @@ let private createReleaseOnGithub (arguments:ParseResults<Arguments>) =

exec "dotnet" (dotnetRun @ ["--"; ] @ releaseArgs) |> ignore

/// Tags for the container image, mirroring the versioning currentVersion already derives from git:
/// "edge" always (so `ghcr.io/nullean/release-notes:edge` is always the latest master build), plus
/// "latest" and the plain semver when this is an exact release tag rather than a canary commit —
/// MinVer's canary suffix always contains a hyphen, a clean tag never does.
let private containerImageTags =
lazy(
let version = currentVersion.Value
if version.Contains("-") then "edge" else sprintf "edge;latest;%s" version
)

/// Publishes the CLI's native-AOT build as a container image via the .NET SDK's own container
/// support (`dotnet publish -t:PublishContainer`), the same mechanism used for curb — see
/// https://github.com/nullean/curb/pull/72. linux-x64 only for now; a second RID becomes a second
/// manifest-list platform later with no change to action.yml.
///
/// Base image is the chiseled/distroless runtime-deps image: no shell, minimal surface, and correct
/// for an AOT binary specifically because there is no managed runtime to host — a plain `runtime`
/// image would carry a CLR this binary never uses.
///
/// --push is an explicit flag, not inferred from a CI/event-name environment variable: the aot-pack
/// job's linux-x64 leg calls this on every trigger (PR, push, tag) purely to prove the container
/// build itself still works, with no ghcr.io credentials configured there, and an env-based "is this
/// a push?" check would have tried (and failed) to push from that job on every non-PR trigger. Only
/// the build job, which does log in, passes --push.
let private publishContainers (arguments:ParseResults<Arguments>) =
let baseImageTag = "10.0-noble-chiseled"
let registryArgs =
if arguments.Contains Push then ["-p"; "ContainerRegistry=ghcr.io"] else []
let args =
["publish"; Paths.RootRelative Paths.ToolProject.FullName; "-c"; "Release"; "-f"; "net10.0"; "-r"; "linux-x64"]
@ ["/t:PublishContainer"
"-p"; "DebugType=none"
"-p"; sprintf "ContainerBaseImage=mcr.microsoft.com/dotnet/runtime-deps:%s" baseImageTag
"-p"; sprintf "ContainerRepository=%s" Paths.Repository
"-p"; sprintf "ContainerImageTags=\"%s\"" containerImageTags.Value
"-p"; "ContainerUser=1001:1001"]
@ registryArgs
exec "dotnet" args |> ignore

let private release (arguments:ParseResults<Arguments>) = printfn "release"

let private publish (arguments:ParseResults<Arguments>) = printfn "publish"
Expand Down Expand Up @@ -180,7 +219,8 @@ let Setup (parsed:ParseResults<Arguments>) (subCommand:Arguments) =
<| fun _ -> release parsed

step CreateReleaseOnGithub.Name createReleaseOnGithub
step PublishContainers.Name publishContainers
cmd Publish.Name
(Some [Release.Name])
(Some [CreateReleaseOnGithub.Name; ])
(Some [CreateReleaseOnGithub.Name; PublishContainers.Name])
<| fun _ -> publish parsed
4 changes: 4 additions & 0 deletions src/release-notes/release-notes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
The root package's DotnetToolSettings.xml (v2) maps each RID to its per-RID package.
Installing requires the .NET 10 SDK or greater. -->
<RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64;osx-arm64;any</RuntimeIdentifiers>
<!-- Lets `dotnet publish -f net10.0 -r <rid> -t:PublishContainer` containerize this same AOT
publish output — see build/scripts/Targets.fs's publishContainers target and
ghcr.io/nullean/release-notes. -->
<EnableSdkContainerSupport>true</EnableSdkContainerSupport>
</PropertyGroup>

<!-- AOT settings apply only when a specific RID is targeted (dotnet pack -r <rid>). Without this
Expand Down
Loading