Novolis Docs
novolis-governance / release-policy.md

Release policy

dotnetgovernancenovolis

Version format

All NuGet and GitHub Package versions use four numeric segments only (no text, no prerelease labels):

YEAR.MAJOR.MINOR.BUILD

Example: 2026.1.1.351 — merge run 351 and release run 351 both use that exact shape.

SegmentSourceWhen it changes
YEARbuild/version.jsonPlatform generation (e.g. settled .NET baseline year)
MAJORbuild/version.jsonBreaking public API change (reset minor to 0)
MINORbuild/version.jsonManual bump before intentional release line
BUILDgithub.run_number in CIEvery workflow run; never committed

Human-owned intent: `build/version.json`.

MSBuild projection: build/version.props (run scripts/sync-version-props.ps1 after editing JSON).

Local pack without CI: YEAR.MAJOR.MINOR.1 via NovolisLocalBuild (override with /p:NovolisLocalBuild=42).

Cross-repo references: floating `2026.1.1.*` in Directory.Packages.props.

Registries

RegistryWhoTriggerVersion
GitHub Packages and nuget.orgLibrariesPush to main (merge.yml)2026.1.1.{run}
GitHub Packages and nuget.orgnovolis-tools, novolis-analyzersGitHub Release published (release.yml)2026.1.1.{run}

Library merge.yml pushes the same pack to GitHub Packages and nuget.org. Library release.yml does not push nuget.org. novolis-tools and novolis-analyzers push both feeds from release.yml. nuget.org trust is one policy per repository, owned by the NuGet organization Novolis, named for the repository, matching GitHub owner Novolis-Platform and package glob Novolis.*. NuGet/login (user frankhaugen) runs in that repository's own workflow. A reusable workflow's job_workflow_ref names novolis-workflows, which does not match the policy.

Workflows

FileTriggerPurpose
pull-request.ymlPR to mainBuild + test (cancels superseded PR runs)
merge.ymlPush to mainLibraries: build + test; pack/publish to GitHub Packages and nuget.org when the package surface changes. Tools and analyzers: build + test only
release.ymlRelease publishedPack, push to GitHub Packages, and attach the packages to the GitHub Release. Tools and analyzers also push nuget.org. Library releases do not. App and utility hosts stay on their own release workflows

Executable repositories

Executable publishing is organized by host grain rather than by the library that implements the domain:

RepositoryRelease artifactTriggerInstaller
novolis-toolsNuGet PackAsTool packagesA published release pushes to GitHub Packages and nuget.orgNone
novolis-utilitiesSelected framework-dependent executable zips and SHA-256 manifestManual selected-utility releaseNone
novolis-appsSelected app channels such as Inno or APKManual selected-app releaseProduct channel only
novolis-labNoneChanged-lab validation onlyNone

Library repositories publish libraries only. They must not publish a domain CLI or a tool package. Existing command names and package IDs are preserved when their hosts move into novolis-tools.

Merge short-circuit (libraries)

Reusable dotnet-merge-publish.yml classifies the push:

Change setCIGPR publish
src/**, *.csproj, Directory.Packages.props, …yesyes
tests/**, benchmarks/**, tools/**, Directory.Build.props (policy/warnings), scripts/**yesno
docs / **.md / version bump props (paths-ignore)skippedskipped

Direct pushes to main remain supported. Cross-repo “herds” still run in parallel; same-repo re-pushes cancel in-progress merge runs.

Apps product releases (novolis-apps)

novolis-apps does not publish installers or APKs on every push to main.

WorkflowBehavior
pull-request.yml / merge.ymlChanged-app matrices from build/apps.json (scoped solutions + tests; Android compile only when declared). No packaging.
release.ymlManual workflow_dispatch with no inputs. Every app, every channel declared in ship.

Channels (phase one):

ChannelArtifactNotes
windows-innoPer-user Inno + portable zip + SHA-256Install under %LocalAppData%\Programs\Novolis\…
android-apkInstallable APK + SHA-256Persistent keystore preferred; adhoc signing allowed for sideload testing; monotonic versionCode
Linux—Local/PR capability only until a named product needs a Linux channel

Catalog source of truth: novolis-apps/build/apps.json. See apps-repos.md and installer-data-lifecycle.md.

Google Play delivery is a separate product distribution path rather than a ship channel. An app opts in with release.googlePlay.enabled=true in novolis-apps/build/apps.json. The novolis-apps/.github/workflows/play-store.yml workflow starts from an existing GitHub Release tag, builds a signed Android App Bundle, and uploads it to a selected Play testing or production track. It does not remove or replace the GitHub Release APK. Play uploads require a persistent app upload key and the GOOGLE_PLAY_SERVICE_ACCOUNT_JSON secret in the selected GitHub Environment; the adhoc APK fallback is forbidden for Play.

Older GitHub Releases are pruned to the newest 5 after a successful release (scripts/prune-github-releases.ps1).

Shared host setup and GitHub Release publish live in novolis-workflows composites: prepare-novolis-build, publish-built-release, install-inno-setup, write-sha256sums, and ensure-github-release. App catalog publish stays in novolis-apps scripts. Google Play (upload-google-play-bundle, play-store.yml) is separate. nuget.org (publish-nuget-org) runs from library merge.yml (dotnet-merge-publish), not from dotnet-release-publish.

Utility releases (novolis-utilities)

Utilities are one-executable technical hosts. They do not use Inno, APK packaging, Start Menu registration, an AppId, or an uninstall lifecycle.

  • Pull requests and merges restore/build/test changed utilities only.
  • A manual release selects one utility or explicitly selects All.
  • Each selected utility is published as a framework-dependent executable

artifact using the supported .NET runtime/SDK and placed in a zip.

  • Each zip is accompanied by SHA256SUMS.txt.
  • Utility release assets are attached directly to a GitHub Release and older

releases are pruned using the same retention policy as apps.

  • dotnet publish --self-contained true is not a utility classification rule.

A utility may use a framework-dependent publish because SDK/runtime requirements are allowed.

Tool releases (novolis-tools)

novolis-tools is the sole publisher of PackAsTool commands. Tool hosts use dotnet tool install; they never receive an installer or utility zip.

  • Changed tool hosts and their package libraries are validated in PR/merge CI.
  • Merge does not publish packages.
  • A published release pushes the packages to GitHub Packages and nuget.org, and attaches them to

the GitHub Release.

  • novolis-analyzers uses that same release publish. Library repositories publish

GitHub Packages and nuget.org from merge.yml.

  • Command names and package IDs must remain stable across host moves.

Release tag: vYEAR.MAJOR.MINOR.BUILD from build/version.json plus github.run_number.

Permissions

permissions:
  contents: read
  packages: write
  id-token: write

Library nuget.org pushes use Trusted Publishing (id-token: write). Android APK product releases prefer persistent ANDROID_KEYSTORE_* secrets; adhoc keys are allowed for sideload testing and are not upgrade-safe. Google Play releases additionally require app-specific upload-key secrets and a Play Console service account with app-level release permissions.

Local development

dotnet pack -c Release /p:NovolisLocalPack=true
# Produces 2026.1.1.0 (or set /p:NovolisLocalBuild=42)