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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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