Release Process
CVOYA graph releases are tag-triggered and fully automated by
.github/workflows/release.yml. There is
no manual publish step and no CI stamping of version numbers: releases are
tag-authoritative — the tag is the version. release.yml only ever reads
it, and rejects any tag that doesn’t match the version scheme below.
./scripts/release.sh is the operator entry point and the only supported way to
cut a tag. It computes the version, runs the pre-flight safety checks, pushes the
tag, watches the workflow to completion, and verifies the published packages are
actually resolvable on nuget.org.
Version scheme
MAJOR.MINOR.PATCH[-(alpha|beta|rc).YYYYMMDD[.N]]
Stable releases are plain MAJOR.MINOR.PATCH. Pre-releases are date-anchored, and
a second pre-release cut on the same day gets a .1, .2, … counter — so
1.0.0-alpha.20260716 is followed by 1.0.0-alpha.20260716.1. These order
correctly under SemVer pre-release precedence. This matches the scheme Spring
Voyage uses.
Cutting a release
-
Preview the plan.
--plancomputes the tag and prints what would happen without pushing anything../scripts/release.sh 1.2.3 --pre alpha --plan -
Cut it. One command does the rest — the base version is positional, and pre-release suffixes come from
--pre, never typed by hand../scripts/release.sh 1.2.3 # stable -> v1.2.3 ./scripts/release.sh 1.2.3 --pre alpha # alpha -> v1.2.3-alpha.20260716 ./scripts/release.sh 1.2.3 --pre rc # rc -> v1.2.3-rc.20260716Before pushing anything, the script verifies its tools and GitHub authentication, checks that both fetch and push URLs for
originresolve tocvoya-com/graph, requires a clean checkout exactly at the currentorigin/maincommit, and performs a dry-run push. Existing tags are immutable: rerun their workflow after a transient failure, or choose a new version. - Pushing the tag triggers the release workflow, which:
- Reads the version off the tag and verifies it matches the scheme above (the workflow never writes a version anywhere — that is what prevents the double-date-suffix class of bugs).
- Runs the full release-relevant test suite, including the Neo4j and Apache
AGE provider tests against service containers (the same pattern as
ci.yml). - Packs the provider and shared packages, including
Cvoya.Graph.Neo4j,Cvoya.Graph.Age,Cvoya.Graph.InMemory,Cvoya.Graph,Cvoya.Graph.Cypher, serialization, analyzer, code-generation, and compatibility-test packages. - Verifies the exact package inventory and inspects every packaged
Cvoya.Graph*.dll: the NuGet manifest, filename,AssemblyVersion,FileVersion, andInformationalVersionmust all match the tag-derived version before anything can publish. - Verifies a clean example consumer can restore and build against the exact package set assembled for publication.
- Generates a build provenance attestation for every package, symbol
package, and the source archive (
actions/attest-build-provenance), verifiable withgh attestation verify <file> --repo cvoya-com/graph. - Publishes to nuget.org using Trusted Publishing (OIDC) — no API key is stored anywhere, long-lived or otherwise.
- Creates a fixed-name
cvoya-graph-source.ziparchive from the tagged commit and attaches it to the GitHub Release alongside the NuGet packages. - Creates a CVOYA-branded GitHub Release for the tag, prepending the product, publisher, download, and package-ID migration information to GitHub’s auto-generated notes.
release.shthen verifies the release is real. A green workflow only meansdotnet nuget pushwas accepted — nuget.org indexes asynchronously, and a package can be accepted but held by validation. The script reads the.nupkgasset names off the GitHub Release and polls nuget.org until every one of those package IDs resolves anonymously at that exact version. Reading the IDs off the release rather than hardcoding them means this can never drift from whatrelease.ymlpacks.
Dry runs
Running release.yml manually via workflow_dispatch (Actions tab → Release
→ Run workflow, on any branch) is always a dry run: it executes the full
build/test/pack/attest pipeline but always skips the NuGet publish and GitHub
Release steps. Use it to validate the pipeline end-to-end — including on a
throwaway branch — without touching nuget.org or creating a release. Real
publishing only ever happens from an actual v* tag push.
With no tag to read, a dispatched run versions itself from the VERSION file’s
development default.
Promoting a prerelease to Latest
release.yml marks any semver prerelease as a GitHub prerelease, which keeps it
off releases/latest. During a prerelease line there is still a “current” build
that public download links should resolve to, so pass --latest to promote it:
./scripts/release.sh 1.2.3 --pre alpha --latest
Once the workflow succeeds and the packages verify, the script clears the
prerelease flag on the GitHub Release record and marks it Latest. The tag and
the NuGet package version remain semantic prereleases — only the GitHub Release
badge moves. --latest is redundant for a stable release, which is always Latest.
Afterwards, confirm that
https://github.com/cvoya-com/graph/releases/latest/download/cvoya-graph-source.zip
downloads the tagged archive before updating public links.
NuGet Trusted Publishing — one-time portal setup
Trusted Publishing exchanges a short-lived GitHub OIDC token for a temporary
(~1 hour) nuget.org API key at publish time, so no NuGet API key is ever
stored as a GitHub secret. This must be configured once on nuget.org by an
owner of the Cvoya.Graph* package IDs (or, before the first publish,
the account/org that will own them):
- Sign in to nuget.org, open your username menu, and choose Trusted Publishing.
- Add a new trusted publishing policy with:
- Repository Owner:
cvoya-com - Repository:
graph - Workflow File:
release.yml(the file name only, not the.github/workflows/path) - Environment: leave empty (the workflow does not use a GitHub Environment)
- Repository Owner:
- Repeat step 2 for each package owner scope if the package IDs aren’t all covered by a single policy (a policy applies to all packages owned by the selected owner).
- The policy is valid for 7 days pending its first successful publish (this is a GitHub anti-resurrection safeguard for the repository identity), after which it’s tied to the repository permanently.
- Set the
NUGET_TRUSTED_PUBLISHING_USERrepository variable (Settings → Secrets and variables → Actions → Variables — not a secret, since it’s just a username) to the nuget.org account or organization name that owns the policy.release.ymlreads it as theuserinput toNuGet/login@v1.
No further workflow changes are needed — release.yml already requests
id-token: write and uses NuGet/login@v1 to exchange the OIDC token for the
push.
Design decisions
- The tag is authoritative; nothing stamps a version in CI. An old,
disabled
release.ymlproduced the mangled1.0.0-alpha.20251014.0.20251014.0version because a workflow step appended a date suffix to an already-suffixedVERSION. No step writes a version now:release.shcomputes it once, the tag carries it, andrelease.ymlonly reads and validates it. VERSIONis a development default, not the release source of truth. Untagged local and CI builds version themselves from it.release.ymlpasses the tag’s version to the pack job asGRAPH_RELEASE_VERSION, whichDirectory.Build.propsprefers over the file. EditingVERSIONdoes not cut a release, and cutting a release does not editVERSION.AssemblyVersion/FileVersionare derived, not tracked. They must be four numeric parts and cannot carry a semver prerelease suffix, soDirectory.Build.propsstrips the suffix and appends.0(1.2.3-rc.20260716→1.2.3.0). This replaced a checked-inVERSION.ASSEMBLYfile holding aMajor.Minor.YYDDD.HHMMstamp, which embedded the build minute — so rebuilding one tag produced a differentFileVersion. The full version, prerelease suffix and commit sha included, still ships viaInformationalVersion(e.g.1.2.3-rc.20260716+cba4d4f…), which is what to inspect for provenance.- Release builds and packing are separate.
dotnet pack -c Releasefollows the standard SDK contract and builds before packing; only an explicit--no-buildreuses earlier output. The release workflow deliberately builds once and then packs with--no-build, while the package verifier checks the DLL metadata so stale or mismatched output cannot publish. - No
CHANGELOG.md. A short CVOYA-branded release introduction is prepended to GitHub’s auto-generated notes (derived from merged PR titles since the previous tag). - No NuGet API key anywhere, cleartext or secret-stored — Trusted Publishing is the only supported publish mechanism.