Skip to content
Draft
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
47 changes: 47 additions & 0 deletions .github/actions/compute-nuget-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Compute NuGet Version
description: Computes the next NuGet package version from release history and nuget-version.yaml

inputs:
branch:
description: The branch name (e.g. main, feature/foo)
required: true

outputs:
stable-version:
description: The next stable version (e.g. 4.0.21)
value: ${{ steps.compute.outputs.stable_version }}
beta-version:
description: The next beta version (e.g. 4.0.21-beta.1.branch-name)
value: ${{ steps.compute.outputs.beta_version }}
last-patch:
description: The last published patch number (-1 if no releases exist for this major.minor)
value: ${{ steps.compute.outputs.last_patch }}

runs:
using: composite
steps:
- name: Compute version
id: compute
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
MAJOR=$(grep -E '^major:' nuget-version.yaml | awk '{print $2}')
MINOR=$(grep -E '^minor:' nuget-version.yaml | awk '{print $2}')

LAST_PATCH=$(gh release list --exclude-drafts --limit 200 --json tagName \
--jq "[.[] | select(.tagName | test(\"^v${MAJOR}\\.${MINOR}\\.[0-9]+$\")) | .tagName | ltrimstr(\"v\") | split(\".\")[2] | tonumber] | max // -1")
LAST_PATCH=${LAST_PATCH:--1}
NEXT_PATCH=$((LAST_PATCH + 1))

BRANCH="${{ inputs.branch }}"
SAFE_BRANCH=$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')

LAST_BETA=$(gh release list --exclude-drafts --limit 200 --json tagName \
--jq "[.[] | select(.tagName | test(\"^v${MAJOR}\\.${MINOR}\\.${NEXT_PATCH}-beta\\.[0-9]+\\.${SAFE_BRANCH}$\")) | .tagName | capture(\"beta\\.(?<n>[0-9]+)\") | .n | tonumber] | max // 0" 2>/dev/null)
LAST_BETA=${LAST_BETA:-0}
NEXT_BETA=$((LAST_BETA + 1))

echo "stable_version=${MAJOR}.${MINOR}.${NEXT_PATCH}" >> "$GITHUB_OUTPUT"
echo "beta_version=${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.${NEXT_BETA}.${SAFE_BRANCH}" >> "$GITHUB_OUTPUT"
echo "last_patch=${LAST_PATCH}" >> "$GITHUB_OUTPUT"
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,42 @@ jobs:

windows-parity:
uses: ./.github/workflows/_windows-parity.yml

version-preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Check for source changes
id: changes
run: |
BASE_SHA="${{ github.event.pull_request.base.sha }}"
if git diff --name-only "$BASE_SHA" HEAD | grep -qE '^src/|^[^/]+\.props$|^nuget-version\.yaml$'; then
echo "has_source_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_source_changes=false" >> "$GITHUB_OUTPUT"
fi

- name: Compute version preview
if: steps.changes.outputs.has_source_changes == 'true'
id: version
uses: ./.github/actions/compute-nuget-version
with:
branch: ${{ github.head_ref }}

- name: Write version preview summary
if: steps.changes.outputs.has_source_changes == 'true'
run: |
{
echo "## 📦 Version Preview"
echo ""
echo "| | Version |"
echo "|---|---|"
echo "| This PR (beta) | \`${{ steps.version.outputs.beta-version }}\` |"
echo "| Next stable release | \`${{ steps.version.outputs.stable-version }}\` |"
} >> "$GITHUB_STEP_SUMMARY"
80 changes: 61 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,54 +1,96 @@
name: Release

on:
push:
tags: [ 'v*' ]
workflow_dispatch:
inputs:
prerelease:
type: boolean
description: Publish as prerelease (beta)
default: false

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

jobs:
compute-version:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.compute.outputs.VERSION }}
prerelease: ${{ steps.compute.outputs.PRERELEASE }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Compute version
id: version
uses: ./.github/actions/compute-nuget-version
with:
branch: ${{ github.ref_name }}

- name: Warn if no source changes since last release
env:
GH_TOKEN: ${{ github.token }}
run: |
LAST_PATCH="${{ steps.version.outputs.last-patch }}"
MAJOR=$(grep -E '^major:' nuget-version.yaml | awk '{print $2}')
MINOR=$(grep -E '^minor:' nuget-version.yaml | awk '{print $2}')
if [[ "$LAST_PATCH" -ge 0 ]]; then
LAST_TAG="v${MAJOR}.${MINOR}.${LAST_PATCH}"
if ! git diff --name-only "${LAST_TAG}" HEAD | grep -qE '^src/|^[^/]+\.props$|^nuget-version\.yaml$'; then
echo "::warning::No source code changes found since ${LAST_TAG}. A version bump may not be needed."
fi
fi

- name: Pick version
id: compute
run: |
if [[ "${{ inputs.prerelease }}" == "true" || "${{ github.ref_name }}" != "main" ]]; then
VERSION="${{ steps.version.outputs.beta-version }}"
echo "PRERELEASE=true" >> "$GITHUB_OUTPUT"
else
VERSION="${{ steps.version.outputs.stable-version }}"
echo "PRERELEASE=false" >> "$GITHUB_OUTPUT"
fi
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
echo "📦 Publishing version: **${VERSION}**" >> "$GITHUB_STEP_SUMMARY"

test:
uses: ./.github/workflows/_build-and-test.yml

windows-parity:
uses: ./.github/workflows/_windows-parity.yml

release:
needs: [test, windows-parity]
needs: [compute-version, test, windows-parity]
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-dotnet-build
with:
build: false

- name: Extract version from tag
id: version
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
if [[ "$VERSION" == *-* ]]; then
echo "PRERELEASE=true" >> $GITHUB_OUTPUT
else
echo "PRERELEASE=false" >> $GITHUB_OUTPUT
fi

- name: Build
run: dotnet build CosmosDB.InMemoryEmulator.sln --configuration Release -p:Version=${{ steps.version.outputs.VERSION }}
run: dotnet build CosmosDB.InMemoryEmulator.sln --configuration Release -p:Version=${{ needs.compute-version.outputs.version }}

- name: Pack
run: dotnet pack CosmosDB.InMemoryEmulator.sln --configuration Release --no-build --output ./nupkgs -p:Version=${{ steps.version.outputs.VERSION }}
run: dotnet pack CosmosDB.InMemoryEmulator.sln --configuration Release --no-build --output ./nupkgs -p:Version=${{ needs.compute-version.outputs.version }}

- name: Push to NuGet
run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.compute-version.outputs.version }}
generate_release_notes: true
prerelease: ${{ steps.version.outputs.PRERELEASE == 'true' }}
make_latest: ${{ steps.version.outputs.PRERELEASE != 'true' }}
prerelease: ${{ needs.compute-version.outputs.prerelease == 'true' }}
make_latest: ${{ needs.compute-version.outputs.prerelease != 'true' }}
files: |
./nupkgs/*.nupkg
./nupkgs/*.snupkg
50 changes: 22 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ Every piece of behavioral logic in the source code — status codes, validation

## Versioning & Release

- After every session of bug fixes is complete and the full test suite has passed, increment the patch version in `src/Directory.Build.props` (the single `<Version>` property shared by all three packages).
- **On `main`:** Commit, create a git tag (`v{version}`), and push both the commit and the tag to origin.
- **On any other branch:** Commit and push the code changes and version bump only. Do not create or push a tag.
Version numbers are computed automatically from `nuget-version.yaml` at the repo root and the existing GitHub release history — **no manual version edits are needed for patch releases**.

- `nuget-version.yaml` contains only `major` and `minor`. The patch is auto-incremented from the latest stable GitHub release for that `major.minor` pair each time a release is triggered.
- For a **major or minor bump**, edit `nuget-version.yaml` and merge to `main` before triggering the next release.
- **Do not** edit `<Version>` in `src/Directory.Build.props` — it produces `0.0.0-local` for local builds, which is correct. The CI/release workflows always pass `-p:Version=X.Y.Z` at build time.
- After bug-fix sessions: **no version file changes are needed** — just commit and push your code changes.

## Test Classification Rules

Expand All @@ -81,40 +84,31 @@ The Integration project does **not** have `InternalsVisibleTo` access. If a test

## Publishing & Releases

Packages are published to NuGet via the `release.yml` GitHub Actions workflow, triggered by pushing a `v*` tag. The workflow runs the full test suite before publishing — if tests fail, nothing is published.

### Beta / Prerelease
Releases are published manually via the **Actions → Release** workflow (`workflow_dispatch`). The full test suite runs before anything is published. Releases are serialised — if two releases are triggered simultaneously, the second queues behind the first.

To publish a prerelease package for testing before merging:
### PR Version Preview

1. Ensure your fix is committed and pushed to a branch.
2. Create and push a tag with a prerelease suffix:
```bash
git tag v4.0.18-beta.1
git push origin v4.0.18-beta.1
```
3. The workflow extracts the version from the tag (strips the `v` prefix), passes it to `dotnet pack -p:Version=...`, and publishes to NuGet as a prerelease package.
4. Consumers install with: `dotnet add package CosmosDB.InMemoryEmulator --version 4.0.18-beta.1`
When a PR changes files in `src/`, `*.props`, or `nuget-version.yaml`, the CI `version-preview` job shows in the workflow step summary what the next beta and stable versions would be if published from that branch.

The tag can be on any branch — the workflow checks out the tagged commit.
### Stable Release (from `main`)

### Stable Release
1. Merge all desired changes to `main`. Update `CHANGELOG.md` if appropriate.
2. Go to **Actions → Release** and trigger the workflow from `main` with **Prerelease: unchecked** (the default).
3. The workflow auto-computes the next patch version, runs all tests, publishes to NuGet.org, and creates a GitHub Release with auto-generated notes.

After merging to `main`:
### Prerelease / Beta (from any branch)

1. Ensure `src/Directory.Build.props` has the correct `<Version>` (e.g. `4.0.18`).
2. Commit, create a tag, and push:
```bash
git tag v4.0.18
git push origin v4.0.18
```
3. The workflow publishes stable packages and creates a GitHub Release with auto-generated release notes.
1. Push your branch.
2. Go to **Actions → Release**, select your branch from the branch dropdown, and trigger with **Prerelease: checked** (beta is also automatic when triggering from any non-`main` branch).
3. The computed version will be `X.Y.Z-beta.N.sanitised-branch-name`, where N auto-increments.
4. Consumers install with: `dotnet add package CosmosDB.InMemoryEmulator --version X.Y.Z-beta.N.branch-name`

### Version Conventions

- `Directory.Build.props` `<Version>` is the target stable version — increment the patch after each release.
- The CI workflow **overrides** the version from the tag, so the `.props` value doesn't need to match beta suffixes.
- Prerelease format: `X.Y.Z-beta.N` (increment N for successive betas of the same version).
- Patch auto-increments: the release workflow queries the highest existing `vX.Y.*` GitHub release tag and adds 1.
- Beta format: `X.Y.Z-beta.N.sanitised-branch-name`
- Stable format: `X.Y.Z`
- To bump major or minor: edit `nuget-version.yaml` (`major`/`minor` fields) and merge to `main` before the next release.

## Documentation

Expand Down
2 changes: 2 additions & 0 deletions nuget-version.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major: 4
minor: 0
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Shared NuGet package metadata for all source (packable) projects -->
<PropertyGroup>
<IsPackable>true</IsPackable>
<Version>4.0.20</Version>
<Version Condition="'$(Version)' == ''">0.0.0-local</Version>
<Authors>lemonlion</Authors>
<Copyright>Copyright (c) 2026 lemonlion</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Loading