diff --git a/.github/actions/compute-nuget-version/action.yml b/.github/actions/compute-nuget-version/action.yml new file mode 100644 index 0000000..67164da --- /dev/null +++ b/.github/actions/compute-nuget-version/action.yml @@ -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\\.(?[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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c43658f..f1458be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0974549..ceeb632 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,63 @@ 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 @@ -12,33 +65,21 @@ jobs: 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 @@ -46,9 +87,10 @@ jobs: - 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 diff --git a/AGENTS.md b/AGENTS.md index 2d719de..542fc73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 `` 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 `` 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 @@ -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 `` (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` `` 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 diff --git a/nuget-version.yaml b/nuget-version.yaml new file mode 100644 index 0000000..fb1b15e --- /dev/null +++ b/nuget-version.yaml @@ -0,0 +1,2 @@ +major: 4 +minor: 0 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cc5f590..b57a64b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ true - 4.0.20 + 0.0.0-local lemonlion Copyright (c) 2026 lemonlion MIT