Skip to content

Commit 612adae

Browse files
committed
Adopt pure 3-segment SemVer versioning
- Remove BuildNumber append from VersionPrefix; use FileVersion for 4-segment build traceability instead - Resolve-version now checks tag existence for release idempotency: only bumped versions trigger a release, repeated pushes are CI-only - Simplify full_version to equal version (no run_number suffix) - Remove BuildNumber concatenation from NUKE GenerateReleaseManifest - Update docs and README to reflect SemVer tags (v0.2.0 not v0.2.0.42) Made-with: Cursor
1 parent 96fcef5 commit 612adae

6 files changed

Lines changed: 32 additions & 38 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,30 +59,29 @@ jobs:
5959
6060
SHA="$(git rev-parse HEAD)"
6161
62+
TAG="v$VERSION"
63+
6264
IS_RELEASE="false"
6365
if [ "${{ github.event_name }}" != "pull_request" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
64-
IS_RELEASE="true"
65-
fi
66-
67-
if [ "$IS_RELEASE" = "true" ]; then
68-
FULL_VERSION="${VERSION}.${{ github.run_number }}"
69-
else
70-
FULL_VERSION="$VERSION"
66+
git fetch --tags --force
67+
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then
68+
echo "::notice::Tag $TAG already exists. Version not bumped — skipping release."
69+
else
70+
IS_RELEASE="true"
71+
fi
7172
fi
72-
TAG="v$FULL_VERSION"
7373
7474
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
75-
echo "full_version=$FULL_VERSION" >> "$GITHUB_OUTPUT"
75+
echo "full_version=$VERSION" >> "$GITHUB_OUTPUT"
7676
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
7777
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
7878
echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
79-
echo "Resolved: version=$VERSION full_version=$FULL_VERSION tag=$TAG sha=$SHA is_release=$IS_RELEASE"
79+
echo "Resolved: version=$VERSION tag=$TAG sha=$SHA is_release=$IS_RELEASE"
8080
8181
- name: Version summary
8282
shell: bash
8383
run: |
8484
VERSION="${{ steps.resolve.outputs.version }}"
85-
FULL_VERSION="${{ steps.resolve.outputs.full_version }}"
8685
IS_RELEASE="${{ steps.resolve.outputs.is_release }}"
8786
SHA="${{ steps.resolve.outputs.sha }}"
8887
TAG="${{ steps.resolve.outputs.tag }}"
@@ -119,8 +118,7 @@ jobs:
119118
echo ""
120119
echo "| Property | Value |"
121120
echo "|----------|-------|"
122-
echo "| **Version** | \`$FULL_VERSION\` |"
123-
echo "| **Base Version** | \`$VERSION\` |"
121+
echo "| **Version** | \`$VERSION\` |"
124122
echo "| **Tag** | \`$TAG\` |"
125123
echo "| **Previous Tag** | \`$PREV_TAG\` |"
126124
echo "| **Bump Type** | **$BUMP** |"
@@ -167,7 +165,7 @@ jobs:
167165
runs-on: ${{ matrix.os }}
168166
timeout-minutes: 30
169167
env:
170-
BuildNumber: ${{ needs.resolve-version.outputs.is_release == 'true' && github.run_number || '' }}
168+
BuildNumber: ${{ github.event_name != 'pull_request' && github.run_number || '' }}
171169
strategy:
172170
fail-fast: false
173171
matrix: ${{ fromJSON(needs.resolve-version.outputs.build_matrix) }}

Directory.Build.props

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313

1414

1515
<!-- Explicit version: single source of truth for the entire solution.
16-
Bump this value via PR when preparing a new release. -->
16+
Bump this value via PR when preparing a new release.
17+
Release is triggered only when this version differs from the latest git tag. -->
1718
<PropertyGroup>
1819
<VersionPrefix>0.2.0</VersionPrefix>
19-
<VersionPrefix Condition="'$(BuildNumber)' != ''">$(VersionPrefix).$(BuildNumber)</VersionPrefix>
20+
<FileVersion Condition="'$(BuildNumber)' != ''">$(VersionPrefix).$(BuildNumber)</FileVersion>
2021
</PropertyGroup>
2122

2223
<!-- Local builds default to prerelease to avoid confusion with stable releases.

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ On **pull requests**, only a fast linux build + test runs. On **main**, the full
4141
| **NuGet Publishing** | Auto-push to NuGet.org with SHA256 manifest verification |
4242
| **Installer Packages** | Platform-specific zip archives attached to GitHub Releases |
4343
| **Version from Code** | `VersionPrefix` in `Directory.Build.props` is the single source of truth |
44-
| **Four-Part Release Tags** | `v0.2.0.42` format ensures unique tags per build |
44+
| **SemVer Release Tags** | `v0.2.0` format — release triggers only when version is bumped |
4545
| **CodeQL Security** | Automated vulnerability scanning on every push and weekly |
4646
| **Graceful Degradation** | Missing NuGet key? Skipped. No docs config? Skipped. Nothing breaks. |
4747

@@ -94,11 +94,12 @@ The pipeline runs automatically. When `build-and-test` completes, go to **Action
9494

9595
## Version Management
9696

97-
Versions follow a simple rule:
97+
Versions follow SemVer (3-segment `Major.Minor.Patch`):
9898

99-
- **CI appends the build number** on release: `0.2.0` becomes `0.2.0.42`
100-
- **Tags are created automatically**: `v0.2.0.42`
101-
- No manual tagging. No version input fields. The single source of truth is `VersionPrefix` in `Directory.Build.props`.
99+
- **`VersionPrefix` in `Directory.Build.props`** is the single source of truth (e.g., `0.2.0`)
100+
- **Tags are created automatically**: `v0.2.0` — release triggers only when the tag doesn't exist yet
101+
- **`FileVersion`** includes the CI build number for traceability (e.g., `0.2.0.42`), visible in DLL properties
102+
- No manual tagging. No version input fields. Bump `VersionPrefix` via PR to trigger a release.
102103

103104
### Version commands
104105

@@ -146,7 +147,7 @@ env:
146147

147148
## Concurrency
148149

149-
Multiple `CI and Release` runs execute **in parallel by default**. Each run gets a unique `run_number`, so versions never conflict.
150+
Multiple `CI and Release` runs execute **in parallel by default**.
150151

151152
To serialize runs on the same branch (only one active at a time), set repository variables in **Settings > Secrets and variables > Actions > Variables**:
152153

build/BuildTask.Targets.ReleaseManifest.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,7 @@ string ReadVersionPrefixFromProps()
5252
var match = Regex.Match(content, @"<VersionPrefix>\s*([^<]+?)\s*</VersionPrefix>", RegexOptions.Singleline);
5353
if (!match.Success)
5454
throw new InvalidOperationException($"<VersionPrefix> not found in {DirectoryBuildPropsFile}");
55-
var version = match.Groups[1].Value.Trim();
56-
57-
var buildNumber = Environment.GetEnvironmentVariable("BuildNumber");
58-
if (!string.IsNullOrWhiteSpace(buildNumber))
59-
version = $"{version}.{buildNumber}";
60-
61-
return version;
55+
return match.Groups[1].Value.Trim();
6256
}
6357

6458
static string GetGitHeadSha()

docs/github-workflows-guide.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ For a quick release walkthrough, see: [Quick Start Release](quick-start-release.
2525

2626
### `CI and Release`
2727
- Triggers: `push` to `main`, `pull_request` targeting `main`, manual `workflow_dispatch`
28-
- Concurrency: **Parallel by default** — multiple pushes can run simultaneously (`run_number` is naturally unique, no version conflicts)
28+
- Concurrency: **Parallel by default** — multiple pushes can run simultaneously
2929
- For serial execution: Settings → Variables → set `CI_SERIAL=true` (queues by branch)
3030
- To cancel older runs: set `CI_CANCEL_IN_PROGRESS=true`
3131
- PR behavior: Runs Format Check + Build + Test + Coverage Report on ubuntu (with prerelease suffix)
32-
- main push behavior: Full-platform matrix Build + Test + Pack + Publish + PackageApp → approval → NuGet push + SBOM + Attestation + tag + GitHub Release → documentation deployment
32+
- main push behavior: If `VersionPrefix` has been bumped (tag doesn't exist yet), triggers full-platform matrix Build + Test + Pack + Publish + PackageApp → approval → NuGet push + SBOM + Attestation + tag + GitHub Release → documentation deployment. Otherwise, runs CI-only (build + test).
3333
- Artifacts: Test results (with PR annotations), coverage reports, NuGet packages (with release manifest), platform installer zips, SBOM
3434

3535
### `CodeQL`
@@ -42,9 +42,9 @@ For a quick release walkthrough, see: [Quick Start Release](quick-start-release.
4242
## 2) Job Details
4343

4444
### `resolve-version`
45-
- Reads `VersionPrefix` from `Directory.Build.props`, validates semver format
46-
- Determines if this is a release (main push = true, PR = false)
47-
- Computes the build matrix: PR uses ubuntu only, main push includes win/linux/osx
45+
- Reads `VersionPrefix` from `Directory.Build.props`, validates semver format (3-segment: `Major.Minor.Patch`)
46+
- Determines if this is a release: main push + tag `v{version}` does not exist yet = release; otherwise CI-only
47+
- Computes the build matrix: PR uses ubuntu only, release includes win/linux/osx
4848

4949
### `build-and-test`
5050
- Matrix build: each platform runs Build + Test
@@ -80,7 +80,7 @@ For a quick release walkthrough, see: [Quick Start Release](quick-start-release.
8080
Approve the `release` environment.
8181

8282
3. Check the Releases page:
83-
- A tag was created (e.g., `v0.2.0.42`)
83+
- A tag was created (e.g., `v0.2.0`)
8484
- The GitHub Release contains platform installer zips
8585
- A corresponding package version exists on NuGet.org (if `NUGET_API_KEY` is configured)
8686

@@ -121,7 +121,7 @@ The version comes from `VersionPrefix` in `Directory.Build.props`. CI does not a
121121
After making changes, merge to `main` via PR. CI automatically builds with the new version.
122122

123123
### Q3: Can the same version be re-published?
124-
Each main push generates a unique 4-segment version number (e.g., `0.2.0.42`), so the same push won't conflict. To publish a new version (e.g., `0.3.0`), modify `VersionPrefix` via PR.
124+
No. Each version can only be released once. If the tag `v{version}` already exists, the release is skipped and the run becomes CI-only. To publish a new version, bump `VersionPrefix` via PR (e.g., `0.2.0``0.3.0`).
125125

126126
### Q4: What is the release manifest?
127127
`release-manifest.json` records the SHA256 hash and version information for each NuGet package. During the release phase, package files are verified against the manifest to prevent tampering or corruption during artifact transfer.

docs/quick-start-release.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ After pushing to `main`:
4141

4242
After a successful run, you should see:
4343

44-
- A new tag (e.g., `v0.2.0.42`)
44+
- A new tag (e.g., `v0.2.0`)
4545
- A new GitHub Release
4646
- Release assets including platform installer zips (`app-linux-x64.zip`, `app-win-x64.zip`, etc.) and the SBOM file
4747
- A corresponding package version on NuGet.org (if `NUGET_API_KEY` is configured)
@@ -51,7 +51,7 @@ After a successful run, you should see:
5151

5252
## 4) Version Management (Must Read)
5353

54-
The version comes from `VersionPrefix` in `Directory.Build.props`. CI automatically appends the build number (e.g., `0.2.0.42`).
54+
The version comes from `VersionPrefix` in `Directory.Build.props` — a pure 3-segment SemVer (e.g., `0.2.0`). A release is triggered only when this version differs from the latest git tag.
5555

5656
```bash
5757
./build.sh ShowVersion # Show current version

0 commit comments

Comments
 (0)