Summary
The release feed infrastructure has no concept of a prerelease channel. Today, any tag pushed — including a semver prerelease like 0.19.0-beta1 — becomes the de-facto "latest" everywhere: the install scripts, the Docker :latest tag, and the GitHub "Latest" release. There is no supported way to publish a beta for opt-in testers without it leaking to every new user.
This was discovered while preparing a 0.19.0-beta1 beta; we abandoned the beta and shipped a normal 0.18.1 patch instead because the pipeline could not safely publish a prerelease.
Why this matters
We want to be able to ship x.y.z-beta.n builds that:
- are installable by testers who explicitly opt in,
- do not become the default for fresh
curl | sh installs,
- do not move Docker
:latest,
- show up as a GitHub prerelease, not the repo's "Latest" release.
Concrete gaps
feeds/scripts/generate-release-manifest.sh hardcodes "latest": "${VERSION}". Any tag — prerelease included — becomes manifest.json latest.
scripts/install.sh / scripts/install.ps1 read .latest from the manifest when no explicit version is given. With (1), a fresh curl | sh install would silently pull the prerelease.
.github/workflows/publish_release_binaries.yml → publish-docker unconditionally tags :latest (and :<major.minor>). A prerelease tag would move :latest and docker pull ghcr.io/netclaw-dev/netclaw would return the beta.
publish_release_binaries.yml → create-release hardcodes prerelease: false. A prerelease tag would create a GitHub release marked as the repo's "Latest".
UpdateCheckService.IsNewerVersion (src/Netclaw.Configuration/Feeds/UpdateCheckService.cs) compares versions with System.Version.TryParse, which cannot parse semver prerelease strings (0.19.0-beta1). It currently fails safe (parse failure → "no update"), so prerelease versions can never be offered by the update check — and version comparison is not semver-correct in general.
- CI gate
Validate tag matches VersionPrefix compares the git tag verbatim against the <VersionPrefix> MSBuild node. A prerelease tag forces <VersionPrefix> to hold the full 0.19.0-beta1 string, even though the semver suffix properly belongs in <VersionSuffix>.
Proposed approach
generate-release-manifest.sh: if the version contains a - (prerelease), do not overwrite latest — only append the entry to releases[]. Optionally add a separate latestPrerelease field for an opt-in prerelease channel.
publish-docker: omit :latest (and probably :<major.minor>) for prerelease tags; publish only :<full-version>.
create-release: set prerelease: dynamically — ${{ contains(github.ref_name, '-') }}.
- Update check: switch to a semver-aware comparison (
NuGet.Versioning.NuGetVersion / SemanticVersion) and explicitly decide prerelease policy — prereleases should only be offered to clients that opt into a prerelease channel, never to stable users.
- CI version gate: either teach the gate to reconstruct
VersionPrefix + VersionSuffix, or accept storing the full prerelease string in <VersionPrefix> (the .NET SDK's GetAssemblyVersion strips the suffix for AssemblyVersion/FileVersion, so the build itself survives).
- Install scripts: document the
NETCLAW_VERSION opt-in for prereleases; optionally support a latestPrerelease channel selector.
Acceptance criteria
A x.y.z-beta.n tag can be published such that:
Summary
The release feed infrastructure has no concept of a prerelease channel. Today, any tag pushed — including a semver prerelease like
0.19.0-beta1— becomes the de-facto "latest" everywhere: the install scripts, the Docker:latesttag, and the GitHub "Latest" release. There is no supported way to publish a beta for opt-in testers without it leaking to every new user.This was discovered while preparing a
0.19.0-beta1beta; we abandoned the beta and shipped a normal0.18.1patch instead because the pipeline could not safely publish a prerelease.Why this matters
We want to be able to ship
x.y.z-beta.nbuilds that:curl | shinstalls,:latest,Concrete gaps
feeds/scripts/generate-release-manifest.shhardcodes"latest": "${VERSION}". Any tag — prerelease included — becomesmanifest.jsonlatest.scripts/install.sh/scripts/install.ps1read.latestfrom the manifest when no explicit version is given. With (1), a freshcurl | shinstall would silently pull the prerelease..github/workflows/publish_release_binaries.yml→publish-dockerunconditionally tags:latest(and:<major.minor>). A prerelease tag would move:latestanddocker pull ghcr.io/netclaw-dev/netclawwould return the beta.publish_release_binaries.yml→create-releasehardcodesprerelease: false. A prerelease tag would create a GitHub release marked as the repo's "Latest".UpdateCheckService.IsNewerVersion(src/Netclaw.Configuration/Feeds/UpdateCheckService.cs) compares versions withSystem.Version.TryParse, which cannot parse semver prerelease strings (0.19.0-beta1). It currently fails safe (parse failure → "no update"), so prerelease versions can never be offered by the update check — and version comparison is not semver-correct in general.Validate tag matches VersionPrefixcompares the git tag verbatim against the<VersionPrefix>MSBuild node. A prerelease tag forces<VersionPrefix>to hold the full0.19.0-beta1string, even though the semver suffix properly belongs in<VersionSuffix>.Proposed approach
generate-release-manifest.sh: if the version contains a-(prerelease), do not overwritelatest— only append the entry toreleases[]. Optionally add a separatelatestPrereleasefield for an opt-in prerelease channel.publish-docker: omit:latest(and probably:<major.minor>) for prerelease tags; publish only:<full-version>.create-release: setprerelease:dynamically —${{ contains(github.ref_name, '-') }}.NuGet.Versioning.NuGetVersion/SemanticVersion) and explicitly decide prerelease policy — prereleases should only be offered to clients that opt into a prerelease channel, never to stable users.VersionPrefix+VersionSuffix, or accept storing the full prerelease string in<VersionPrefix>(the .NET SDK'sGetAssemblyVersionstrips the suffix forAssemblyVersion/FileVersion, so the build itself survives).NETCLAW_VERSIONopt-in for prereleases; optionally support alatestPrereleasechannel selector.Acceptance criteria
A
x.y.z-beta.ntag can be published such that:manifest.jsonlatestis unchanged (still points at the latest stable);:latestis unchanged;curl | sh/install.ps1install is unaffected;NETCLAW_VERSION=x.y.z-beta.n) and/or a documented prerelease channel;