diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5b89a5..16050c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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: @@ -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') diff --git a/.github/workflows/create-major-tag.yml b/.github/workflows/create-major-tag.yml new file mode 100644 index 0000000..4215fe6 --- /dev/null +++ b/.github/workflows/create-major-tag.yml @@ -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}" diff --git a/README.md b/README.md index b33dc71..1e5e122 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..e3a6529 --- /dev/null +++ b/action.yml @@ -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 }} diff --git a/build/scripts/CommandLine.fs b/build/scripts/CommandLine.fs index a9672ac..77db63f 100644 --- a/build/scripts/CommandLine.fs +++ b/build/scripts/CommandLine.fs @@ -15,10 +15,12 @@ type Arguments = | [] Release | [] CreateReleaseOnGithub + | [] PublishContainers | [] Publish | [] SingleTarget of bool | [] Token of string + | [] Push with interface IArgParserTemplate with member this.Usage = @@ -30,6 +32,7 @@ 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 @@ -37,6 +40,7 @@ with | GenerateReleaseNotes | GenerateApiChanges | CreateReleaseOnGithub + | PublishContainers -> "Undocumented, dependent target" member this.Name = match FSharpValue.GetUnionFields(this, typeof) with diff --git a/build/scripts/Targets.fs b/build/scripts/Targets.fs index 4057484..dfeb831 100644 --- a/build/scripts/Targets.fs +++ b/build/scripts/Targets.fs @@ -149,6 +149,45 @@ let private createReleaseOnGithub (arguments:ParseResults) = 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) = + 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) = printfn "release" let private publish (arguments:ParseResults) = printfn "publish" @@ -180,7 +219,8 @@ let Setup (parsed:ParseResults) (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 diff --git a/src/release-notes/release-notes.csproj b/src/release-notes/release-notes.csproj index 16866e8..14b6bcd 100644 --- a/src/release-notes/release-notes.csproj +++ b/src/release-notes/release-notes.csproj @@ -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. --> linux-x64;linux-arm64;win-x64;win-arm64;osx-arm64;any + + true