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
52 changes: 40 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
name: Validate (${{ matrix.os }})
Expand All @@ -18,29 +22,53 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- name: Check out source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up .NET 10
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: 10.0.x
- run: dotnet restore SharedMemoryStore.slnx
- run: dotnet build SharedMemoryStore.slnx -c Release --no-restore
- run: dotnet format SharedMemoryStore.slnx --verify-no-changes --no-restore
- run: dotnet test SharedMemoryStore.slnx -c Release --no-build
- shell: pwsh

- name: Restore
run: dotnet restore SharedMemoryStore.slnx

- name: Build
run: dotnet build SharedMemoryStore.slnx -c Release --no-restore

- name: Verify formatting
run: dotnet format SharedMemoryStore.slnx --verify-no-changes --no-restore

- name: Test
run: dotnet test SharedMemoryStore.slnx -c Release --no-build

- name: Validate documentation
shell: pwsh
run: ./scripts/validate-docs.ps1
- shell: pwsh

- name: Validate package consumption
shell: pwsh
run: ./scripts/validate-package-consumption.ps1 -Configuration Release
- run: dotnet pack src/SharedMemoryStore/SharedMemoryStore.csproj -c Release --no-build -o artifacts/package

- name: Pack
run: dotnet pack src/SharedMemoryStore/SharedMemoryStore.csproj -c Release --no-build -o artifacts/package

docker:
name: Docker sharing
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- name: Check out source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up .NET 10
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: 10.0.x
- shell: pwsh

- name: Validate Docker sharing
shell: pwsh
run: ./scripts/validate-docker-shared-memory.ps1 -Configuration Release
199 changes: 199 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
name: Release

on:
workflow_dispatch:
inputs:
version:
description: Version to publish; must match the project (for example, 1.0.1)
required: true
type: string

permissions:
contents: read

concurrency:
group: release
cancel-in-progress: false

jobs:
validate:
name: Validate and package
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.value }}

steps:
- name: Check out source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up .NET 10
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: 10.0.x

- name: Verify release version and target
id: version
shell: pwsh
env:
REQUESTED_VERSION: ${{ inputs.version }}
run: |
if ($env:GITHUB_REF -ne "refs/heads/main") {
throw "Releases must run from the main branch, not '$env:GITHUB_REF'."
}

$requested = $env:REQUESTED_VERSION.Trim()
if ($requested -notmatch '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$') {
throw "'$requested' is not a supported semantic version."
}

[xml]$project = Get-Content -Raw -LiteralPath "src/SharedMemoryStore/SharedMemoryStore.csproj"
$projectVersion = [string](
$project.Project.PropertyGroup |
Where-Object { $_.Version } |
Select-Object -First 1
).Version
if ($projectVersion -ne $requested) {
throw "Requested version '$requested' does not match project version '$projectVersion'."
}

git ls-remote --exit-code --tags origin "refs/tags/v$requested" *> $null
if ($LASTEXITCODE -eq 0) {
throw "Tag 'v$requested' already exists. Versions and tags are immutable."
}
if ($LASTEXITCODE -ne 2) {
throw "Could not verify whether tag 'v$requested' exists (git exit code $LASTEXITCODE)."
}

$published = Invoke-RestMethod `
-Uri "https://api.nuget.org/v3-flatcontainer/sharedmemorystore/index.json" `
-Headers @{ "User-Agent" = "SharedMemoryStore-release-workflow" }
if ($published.versions -contains $requested.ToLowerInvariant()) {
throw "SharedMemoryStore $requested is already published on NuGet.org. Package versions are immutable."
}

"value=$requested" >> $env:GITHUB_OUTPUT

- name: Run full release validation
shell: pwsh
run: ./scripts/validate-cross-platform.ps1

- name: Create release packages and notes
shell: pwsh
env:
RELEASE_VERSION: ${{ steps.version.outputs.value }}
run: |
dotnet pack src/SharedMemoryStore/SharedMemoryStore.csproj `
-c Release `
--no-build `
-o artifacts/release
if ($LASTEXITCODE -ne 0) {
throw "dotnet pack failed with exit code $LASTEXITCODE."
}

$package = "artifacts/release/SharedMemoryStore.$env:RELEASE_VERSION.nupkg"
$symbols = "artifacts/release/SharedMemoryStore.$env:RELEASE_VERSION.snupkg"
if (-not (Test-Path -LiteralPath $package -PathType Leaf)) {
throw "Expected package was not created: $package"
}
if (-not (Test-Path -LiteralPath $symbols -PathType Leaf)) {
throw "Expected symbol package was not created: $symbols"
}

./scripts/get-release-notes.ps1 `
-Version $env:RELEASE_VERSION `
-OutputPath artifacts/release/release-notes.md

- name: Preserve release artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: nuget-release-${{ steps.version.outputs.value }}
path: |
artifacts/release/SharedMemoryStore.${{ steps.version.outputs.value }}.nupkg
artifacts/release/SharedMemoryStore.${{ steps.version.outputs.value }}.snupkg
artifacts/release/release-notes.md
if-no-files-found: error
retention-days: 14

publish:
name: Publish NuGet and GitHub release
needs: validate
runs-on: ubuntu-latest
environment:
name: release
url: https://www.nuget.org/packages/SharedMemoryStore/${{ needs.validate.outputs.version }}
permissions:
contents: write
id-token: write

steps:
- name: Download release artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: nuget-release-${{ needs.validate.outputs.version }}
path: artifacts/release

- name: Exchange GitHub identity for a temporary NuGet API key
id: nuget
uses: NuGet/login@ebc737b6fc418a6ca0073cf116ec8dc156d8b81e # v1
with:
user: rantri

- name: Create draft GitHub release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: |
$tag = "v$env:RELEASE_VERSION"
$arguments = @(
"release", "create", $tag,
"artifacts/release/SharedMemoryStore.$env:RELEASE_VERSION.nupkg",
"artifacts/release/SharedMemoryStore.$env:RELEASE_VERSION.snupkg",
"--repo", $env:GITHUB_REPOSITORY,
"--target", $env:GITHUB_SHA,
"--title", $tag,
"--notes-file", "artifacts/release/release-notes.md",
"--draft"
)
if ($env:RELEASE_VERSION.Contains("-")) {
$arguments += "--prerelease"
}

gh @arguments
if ($LASTEXITCODE -ne 0) {
throw "Creating the draft GitHub release failed with exit code $LASTEXITCODE."
}

- name: Publish package and symbols to NuGet.org
shell: pwsh
env:
NUGET_API_KEY: ${{ steps.nuget.outputs.NUGET_API_KEY }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: |
dotnet nuget push `
"artifacts/release/SharedMemoryStore.$env:RELEASE_VERSION.nupkg" `
--api-key $env:NUGET_API_KEY `
--source https://api.nuget.org/v3/index.json
if ($LASTEXITCODE -ne 0) {
throw "NuGet publishing failed with exit code $LASTEXITCODE. The GitHub release remains a draft."
}

- name: Publish GitHub release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: |
$arguments = @(
"release", "edit", "v$env:RELEASE_VERSION",
"--repo", $env:GITHUB_REPOSITORY,
"--draft=false"
)
if (-not $env:RELEASE_VERSION.Contains("-")) {
$arguments += "--latest"
}

gh @arguments
if ($LASTEXITCODE -ne 0) {
throw "NuGet publishing succeeded, but publishing the draft GitHub release failed. Publish the existing draft manually."
}
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
All notable package and documentation changes are recorded in reverse
chronological order.

## 1.0.1 - 2026-07-09
## 1.0.1 - 2026-07-10

### Packaging

- Added Linux and Windows GitHub Actions validation and a manually triggered,
trusted-publishing release workflow for NuGet.org and GitHub Releases.
- Added portable `.snupkg` symbols to improve package debugging without growing
the primary package.

### Fixed

Expand Down
18 changes: 18 additions & 0 deletions docs/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ and targets `net10.0`. Runtime dependencies are limited to the .NET BCL.
| `Description` | `A bounded named shared-memory key-value store for opaque binary values.` |
| `PackageTags` | `shared-memory;memory-mapped-file;zero-copy;linux;windows;docker;library` |
| `PackageLicenseExpression` | `MIT` |
| `PackageProjectUrl` | `https://github.com/rantri/SharedMemoryStore` |
| `PackageReadmeFile` | `README.md` |
| `PackageReleaseNotes` | `Linux, Windows, and same-host Docker support hardening: fixes bounded waits, crash-safe ownership and index maintenance, private Linux resource permissions, layout overflow validation, and cleanup reliability while preserving the 1.0.0 public API and layout.` |
| `RepositoryType` | `git` |
| `RepositoryUrl` | `https://github.com/rantri/SharedMemoryStore` |
| `SymbolPackageFormat` | `snupkg` |

The package project packs the root [README.md](../README.md) at the package
root so NuGet consumers see the same package purpose, status, first-use
Expand All @@ -32,6 +34,22 @@ dotnet build SharedMemoryStore.slnx -c Release
dotnet pack src/SharedMemoryStore/SharedMemoryStore.csproj -c Release -o artifacts/package
```

Packing produces both `SharedMemoryStore.<version>.nupkg` and the portable-symbol
package `SharedMemoryStore.<version>.snupkg`. NuGet.org publishes the symbol
package to its symbol server alongside the primary package.

## Automated Publication

The [CI workflow](../.github/workflows/ci.yml) validates Linux and Windows on
pull requests and pushes to `main`. The manually triggered
[release workflow](../.github/workflows/release.yml) performs the full release
validation, including Docker, verifies that the version is unused, creates the
package and symbols, publishes them to NuGet.org with trusted publishing, and
creates the matching GitHub release and `v<version>` tag.

The one-time trusted-publishing policy and the exact release procedure are
documented in [Release preparation](releases.md).

## Clean Consumer Validation

```powershell
Expand Down
46 changes: 43 additions & 3 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,45 @@ written for the current `1.0.1` package and should be updated when package
metadata, public API behavior, compatibility scope, support policy, security
reporting, documentation scope, or sample behavior changes.

## Automated Release Process

Releases are deliberately started by a maintainer and automated after that
point. The [release workflow](../.github/workflows/release.yml) validates the
version and release target, runs the full Linux and Docker release suite, packs
the primary and symbol packages, creates a draft GitHub release, publishes to
NuGet.org, and then publishes the GitHub release. CI separately validates the
same commit on Linux and Windows through
[ci.yml](../.github/workflows/ci.yml).

Configure NuGet.org trusted publishing once before the first automated release:

1. Sign in to NuGet.org as the `rantri` owner and open **Trusted Publishing**.
2. Add a GitHub policy with owner `rantri`, repository
`SharedMemoryStore`, workflow file `release.yml`, and environment `release`.
3. In GitHub, confirm the `release` environment exists. An optional required
reviewer adds a second approval before publication.
4. Merge all release changes to `main` and confirm the CI workflow succeeds.

To publish a release:

1. Set `<Version>` and `PackageReleaseNotes` in the package project, then align
README, packaging documentation, and `CHANGELOG.md`.
2. Complete the compatibility and validation review below.
3. On GitHub, open **Actions**, choose **Release**, select **Run workflow** on
`main`, enter the exact version without a `v` prefix, and run it.
4. Confirm the workflow publishes `SharedMemoryStore.<version>.nupkg` and
`.snupkg`, creates tag `v<version>`, and publishes the GitHub release.
5. Allow time for NuGet.org validation and indexing, then install the exact
published version in a clean consumer.

Do not create the tag or GitHub release first; the workflow owns both. NuGet
package versions are immutable, so a failed release must be diagnosed rather
than retried under the same version with different contents. If NuGet publishing
fails, the workflow intentionally leaves the GitHub release as a draft. If the
package is still absent from NuGet.org, delete that draft and its tag before a
retry. If NuGet.org already has the version, keep the tag and publish the
existing draft after verifying its attached package rather than rebuilding it.

## Package Metadata

Verify
Expand Down Expand Up @@ -200,9 +239,10 @@ The 2026-07-09 review completed the following release evidence:
- Public API names, status values, runtime dependencies, and the shared-memory
layout remain compatible with `1.0.0`.

External publication gate: the GitHub repository reported private
vulnerability reporting as disabled during this review. Enable it or publish an
owner-approved private reporting channel before publishing `1.0.1`.
External publication gates completed on 2026-07-10: GitHub private vulnerability
reporting is enabled for the public repository, the `release` GitHub environment
is restricted to `main`, and the NuGet.org trusted-publishing policy targets
`rantri/SharedMemoryStore`, `release.yml`, and the `release` environment.

## 1.0.0 Documentation and Samples Excellence Notes

Expand Down
Loading
Loading