mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
fa06c66dbb
## Summary Removes the **DI** (`Opc.Ua.Di*`) and **ISA95** (`Opc.Ua.ISA95*`) package families from the preview-only set, so they ship with the stable root version (e.g. `2.0.1` instead of `2.0.1-preview.N`). - `version.targets`: drop the two `_IsPreviewPackage` conditions. - `.azurepipelines/package-version-policy.ps1`: drop `ISA95|Di` from `Test-PreviewPackageId`. It must match `version.targets`, because the package-set validator fails if the two disagree. - `PackageVersionPolicyTests`: ISA95/Di IDs are now expected to be non-preview, they are removed from the classifier-agreement token list, and the "already-preview base version" case now uses Robotics. - Docs (`DeveloperGuide.md`, `ReleaseProcess.md`, `docs/README.md`): the preview-family list no longer includes DI or ISA95, and the "preview package" label is removed from the DI, Software Update (part of the DI packages), and ISA-95 entries. ## Test `dotnet test tests/Opc.Ua.Tools.Tests --filter PackageVersionPolicy` (net10.0): 87/87 passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
841 lines
43 KiB
Markdown
841 lines
43 KiB
Markdown
# Release process
|
|
|
|
This is the authoritative procedure for every release operation on this
|
|
repository: cutting a release line, shipping a patch or minor version,
|
|
backporting a fix, promoting a stable candidate, and recovering from a
|
|
failed or partial release. Follow it exactly as written; it is designed to
|
|
be followed by a developer or an agent working from the repository alone,
|
|
without any other context.
|
|
|
|
> **Read this before**: changing a release version, cutting or retiring a
|
|
> release branch, backporting a fix, running `release.yml`, or recovering
|
|
> from a failed release.
|
|
|
|
## Contents
|
|
|
|
- [Terms](#terms)
|
|
- [Common preflight](#common-preflight)
|
|
- [Which procedure do I need?](#which-procedure-do-i-need)
|
|
- [Branch and channel model](#branch-and-channel-model)
|
|
- [First stable release](#first-stable-release)
|
|
- [Patch release](#patch-release)
|
|
- [Minor release](#minor-release)
|
|
- [Routine backport](#routine-backport)
|
|
- [Emergency hotfix](#emergency-hotfix)
|
|
- [Candidate and dry run](#candidate-and-dry-run)
|
|
- [Approved promotion](#approved-promotion)
|
|
- [Failed or partial release](#failed-or-partial-release)
|
|
- [Post-release verification](#post-release-verification)
|
|
- [Release handoff template](#release-handoff-template)
|
|
- [Historical `release/2.0.0` branch](#historical-release200-branch)
|
|
- [Maintaining this document](#maintaining-this-document)
|
|
|
|
## Terms
|
|
|
|
- **Root version** - the package version computed from the committed
|
|
`version.json` for the commit being built (see
|
|
[Versioning](DeveloperGuide.md#versioning)). Every package in the solution
|
|
shares the same root version except the **preview-only families**.
|
|
- **Preview-only families** - the XRegistry, WoT Connectivity, Vision,
|
|
Robotics, Redundancy, Positioning, OpenUSD, and AI package
|
|
families, the Robotics/Vision MCP extensions, and the OpenUSD connector
|
|
tools. `version.targets` keeps these on a `-preview.N` suffix even when the
|
|
root version is an exact stable release. The full, single source of truth
|
|
for which package IDs this covers is `_IsPreviewPackage` in
|
|
`version.targets` (build-time) and `Test-PreviewPackageId` in
|
|
`.azurepipelines/package-version-policy.ps1` (validation-time) - both must
|
|
always list the same families.
|
|
- **Channel** - `stable` when the root version has no prerelease label or
|
|
build metadata (e.g. `2.0.0`); `preview` otherwise (e.g. `2.0.0-preview.6`,
|
|
`2.0.1-preview.4+gabc123`). `validate-nuget-package-set.ps1` writes this as
|
|
`channel` in its manifest; `Test-StablePackageVersion` in
|
|
`package-version-policy.ps1` is the single source of truth.
|
|
- **Canonical release branch** - a branch named exactly
|
|
`release/<major>.<minor>` (e.g. `release/2.0`, `release/2.1`) - the only
|
|
branch shape `version.json`'s `publicReleaseRefSpec` and
|
|
`Test-CanonicalReleaseBranchRef` recognize as eligible to produce a
|
|
`stable` channel package set. The name is matched **case-sensitively**
|
|
(`Release/2.0` is not a release line) and each component must be a plain
|
|
decimal number with no leading zero (`release/02.0` is not a second
|
|
spelling of `release/2.0`, it is simply not a release line). The
|
|
historical three-component naming (`release/2.0.0`) is retired and not
|
|
canonical; see
|
|
[Historical `release/2.0.0` branch](#historical-release200-branch).
|
|
- **Candidate** - a signed, validated package set produced by a
|
|
`nuget-publish.yml` run, retained as its `opcua-packages-<run id>` GitHub
|
|
Actions artifact.
|
|
|
|
## Common preflight
|
|
|
|
Every procedure below starts here.
|
|
|
|
1. **Resolve the repository and remotes.**
|
|
```powershell
|
|
git rev-parse --show-toplevel
|
|
git remote -v
|
|
```
|
|
Confirm you are in a clone of `OPCFoundation/UA-.NETStandard` (or a fork
|
|
with `origin`/`upstream` pointing at it) before continuing.
|
|
|
|
2. **Require a clean worktree.**
|
|
```powershell
|
|
git status --short
|
|
```
|
|
*Completion evidence*: no output. Commit, stash, or discard changes before
|
|
continuing - every procedure below assumes a clean starting point.
|
|
|
|
3. **Fetch and confirm the source/target branches exist.**
|
|
```powershell
|
|
git fetch origin --prune
|
|
git branch --all --list 'origin/release/*'
|
|
```
|
|
*Completion evidence*: the branch(es) the procedure you are about to run
|
|
names are present (or, for a branch-creation step, deliberately absent).
|
|
|
|
4. **Confirm required tooling and authentication.**
|
|
```powershell
|
|
dotnet --version
|
|
gh auth status
|
|
```
|
|
*Completion evidence*: a .NET 10 SDK version, and `gh` reports an
|
|
authenticated account with access to `OPCFoundation/UA-.NETStandard`.
|
|
Promotion (`release.yml`) additionally requires the `release` GitHub
|
|
Environment's approval; you cannot grant that to yourself; a repository
|
|
maintainer with environment-reviewer access must approve the run.
|
|
|
|
5. **Discover the currently published preview numbers before touching
|
|
`preview-version.props`.** Every procedure that changes the root version
|
|
to an exact stable release must confirm the committed
|
|
`PreviewPackageBuildNumber` (see `preview-version.props`) is still greater
|
|
than every already-published `-preview.N` (or legacy `-preview.YYYYMMDD`)
|
|
identifier for the preview-only families, on **both** feeds:
|
|
```powershell
|
|
Invoke-RestMethod 'https://api.nuget.org/v3-flatcontainer/opcfoundation.netstandard.opc.ua.xregistry/index.json' |
|
|
Select-Object -ExpandProperty versions
|
|
gh api '/orgs/OPCFoundation/packages/nuget/OPCFoundation.NetStandard.Opc.Ua.XRegistry/versions' --paginate |
|
|
ConvertFrom-Json | ForEach-Object name
|
|
```
|
|
Repeat for a representative sample of the preview-only families (at
|
|
minimum XRegistry, Robotics, and one MCP/connector package); they are versioned
|
|
together but published independently, so check more than one. If any
|
|
published number is greater than or equal to the committed
|
|
`PreviewPackageBuildNumber`, raise it (in the same PR that changes the
|
|
root version to stable) before proceeding.
|
|
|
|
This manual sampling is only a headroom estimate. The authoritative check
|
|
is `.azurepipelines/validate-preview-package-ordering.ps1`, which queries
|
|
**every** preview-family package ID in a candidate's manifest against both
|
|
feeds. `nuget-publish.yml` and `release.yml` both run it automatically for
|
|
stable candidates, and both fail if any already-published version is not
|
|
strictly below the candidate's `-preview.N`. Note that ordinary `master`
|
|
development keeps publishing `<base>-preview.{height}` packages with a
|
|
growing height, so a number that was correct last week can go stale on its
|
|
own - always re-run the check against the **actual merged candidate**
|
|
([Candidate and dry run](#candidate-and-dry-run) step 3) before approving
|
|
a promotion.
|
|
|
|
## Which procedure do I need?
|
|
|
|
| Situation | Procedure |
|
|
| --- | --- |
|
|
| Shipping the very first stable `2.0.0` | [First stable release](#first-stable-release) |
|
|
| Shipping the next patch on an existing line (e.g. `2.0.1`, `2.0.2`) | [Patch release](#patch-release) |
|
|
| Shipping the next minor line (e.g. `2.1.0`) | [Minor release](#minor-release) |
|
|
| Porting an already-reviewed `master` fix onto a release branch | [Routine backport](#routine-backport) |
|
|
| A production-only bug needs a fix that did not start on `master` | [Emergency hotfix](#emergency-hotfix) |
|
|
| You have a `nuget-publish.yml` run and want to inspect it before/without publishing | [Candidate and dry run](#candidate-and-dry-run) |
|
|
| A maintainer has approved a stable candidate for publication | [Approved promotion](#approved-promotion) |
|
|
| Publishing a numbered preview (e.g. `2.0.0-preview.6`) to nuget.org | [Preview release](#preview-release) |
|
|
| A release or promotion attempt errored, or you are unsure what published | [Failed or partial release](#failed-or-partial-release) |
|
|
| You just promoted a release and want to confirm everything is consistent | [Post-release verification](#post-release-verification) |
|
|
|
|
## Branch and channel model
|
|
|
|
- `master` always carries a `-preview.{height}` root version for the next
|
|
minor release under development (e.g. `2.0.0-preview.{height}` while `2.0`
|
|
is being prepared, `2.1.0-preview.{height}` once `2.0` has shipped).
|
|
Every `master` build is `channel: preview` and publishes automatically to
|
|
GitHub Packages; it never produces a stable release.
|
|
- Each maintained minor line lives on its own canonical release branch
|
|
(`release/2.0`, `release/2.1`, ...). While stabilizing, its root version is
|
|
also `-preview.{height}` (e.g. `2.0.1-preview.{height}` between the `2.0.0`
|
|
and `2.0.1` releases) and publishes automatically to GitHub Packages, same
|
|
as `master`. Such a release-line preview (`M.m.p-preview.N`, no commit id)
|
|
reaches nuget.org only through the explicitly dispatched
|
|
[Preview release](#preview-release) procedure; `master` previews carry a
|
|
commit id and never can. At the exact commit approved for release, its root version is
|
|
set to the plain `<major>.<minor>.<patch>` (e.g. `2.0.0`, `2.0.1`, `2.1.0`)
|
|
- `channel: stable` - and that specific package set is only ever published
|
|
by the [Approved promotion](#approved-promotion) procedure, never
|
|
automatically.
|
|
- Patch numbers only ever increase by exactly one per release on their line
|
|
(`2.0.0` → `2.0.1` → `2.0.2`, ...); a minor bump resets the patch to zero
|
|
(`2.1.0`, not `2.1.<height>`). Nothing about git height, commit count, or
|
|
CI retries changes these numbers - they are explicit, committed decisions
|
|
in `version.json`.
|
|
- A release is **never** produced from `master`, a tag, a PR branch, or any
|
|
branch other than its own canonical `release/<major>.<minor>` line. This is
|
|
enforced in multiple independent places: `version.json`'s
|
|
`publicReleaseRefSpec` (so only that ref shape ever computes
|
|
`NBGV_PublicRelease == 'True'`), the `Aggregate and validate packages` step
|
|
in `nuget-publish.yml` (refuses to build a stable set from a non-canonical
|
|
ref), `release.yml` (refuses to promote from a non-canonical ref, and
|
|
refuses any candidate whose version does not match the dispatched `channel`
|
|
input, which defaults to `stable`), and the `release` GitHub Environment's
|
|
`deployment_branch_policy` (restricts which branch may even execute the
|
|
`release.yml` job).
|
|
- The branch and stable package version must agree: `release/2.0` may
|
|
release `2.0.p` only, while `release/2.1` may release `2.1.p` only.
|
|
`Test-CanonicalReleaseBranchForPackageVersion` enforces this in both the
|
|
candidate and promotion workflows, so a valid-looking `2.1.0` package set
|
|
cannot be released from `release/2.0`.
|
|
- Container image tags follow the same line model, enforced by the
|
|
`Determine release line precedence` step in
|
|
`.github/workflows/docker-image.yml`. Every build gets its exact version
|
|
tag; a stable release additionally gets `<major>.<minor>` and the plain
|
|
`<major>.<minor>.<patch>`. The unqualified `latest` and `release` aliases
|
|
are claimed **only** by the newest canonical `release/<major>.<minor>`
|
|
branch that exists on `origin`, so a later `2.0.x` maintenance release
|
|
publishes `latest-2.0` instead and cannot move `latest`/`release` back
|
|
from an already published `2.1.x`. The check fails closed: if the newest
|
|
line cannot be determined, the build is treated as superseded and only
|
|
publishes line-specific aliases. Non-release builds keep their existing
|
|
branch-suffixed `latest-<branch>` alias, and the master-only pump image
|
|
publishes `preview` rather than `latest`.
|
|
|
|
## First stable release
|
|
|
|
Ships the very first stable version on a brand-new minor line (in this
|
|
repository's history, `2.0.0`).
|
|
|
|
**Owner**: a repository maintainer with permission to create protected
|
|
branches and merge to `master`.
|
|
|
|
**Preconditions**: the [common preflight](#common-preflight) is complete; the
|
|
preparation work for the release (dependency/version-model changes, this
|
|
document) is already merged to `master`; `master`'s root version is
|
|
`2.0.0-preview.{height}`.
|
|
|
|
1. Confirm `master`'s current committed version:
|
|
```powershell
|
|
git show HEAD:version.json
|
|
```
|
|
*Completion evidence*: `"version": "2.0.0-preview.{height}"`.
|
|
|
|
2. Choose the exact `master` commit to branch from (normally its current
|
|
`HEAD`, after its required checks have passed) and create the canonical
|
|
release branch from it:
|
|
```powershell
|
|
git fetch origin master
|
|
git branch release/2.0 origin/master
|
|
git push origin release/2.0
|
|
```
|
|
*Completion evidence*: `gh api repos/OPCFoundation/UA-.NETStandard/branches/release/2.0`
|
|
returns the expected commit SHA.
|
|
|
|
3. Immediately advance `master` to the next minor line's preview version so
|
|
no further commit is mistaken for `2.0` servicing work. Open a PR (do not
|
|
push directly to the protected branch):
|
|
```powershell
|
|
git switch -c bump-version-2.1-preview origin/master
|
|
```
|
|
Edit `version.json`'s `"version"` to `"2.1.0-preview.{height}"`. Commit,
|
|
push, and open a PR titled something like "Advance master to 2.1.0
|
|
preview". Merge once required checks pass.
|
|
*Completion evidence*: `master`'s `version.json` shows
|
|
`2.1.0-preview.{height}`.
|
|
|
|
4. On `release/2.0`, open a **second** PR that sets the stable version and
|
|
completes the release preflight (do not combine this with step 3 - they
|
|
target different branches):
|
|
```powershell
|
|
git switch -c release-2.0.0 origin/release/2.0
|
|
```
|
|
Edit `version.json`'s `"version"` to `"2.0.0"` (no prerelease label).
|
|
Re-run the preview-number check from
|
|
[Common preflight](#common-preflight) step 5 against the actual current
|
|
published state and adjust `preview-version.props` if needed. Commit,
|
|
push, and open a PR against `release/2.0`.
|
|
*Completion evidence*: the PR's diff changes only `version.json` (and, if
|
|
needed, `preview-version.props`); required checks pass.
|
|
|
|
5. Merge the PR from step 4 once its checks pass and it has the required
|
|
approval. This is the release commit.
|
|
|
|
6. Continue with [Candidate and dry run](#candidate-and-dry-run) using the
|
|
`nuget-publish.yml` run triggered by that merge, then
|
|
[Approved promotion](#approved-promotion).
|
|
|
|
7. After promotion, reopen `release/2.0` for the next patch's development by
|
|
repeating step 3's pattern on `release/2.0` itself, setting `version.json`
|
|
to `"2.0.1-preview.{height}"` via a PR.
|
|
|
|
## Patch release
|
|
|
|
Ships the next patch on an already-shipped line (for example `2.0.1` after
|
|
`2.0.0`, or `2.0.2` after `2.0.1`).
|
|
|
|
**Owner**: a repository maintainer.
|
|
|
|
**Preconditions**: the [common preflight](#common-preflight) is complete;
|
|
`release/<major>.<minor>` already exists and its current committed
|
|
`version.json` is the next patch's `-preview.{height}` version (see step 7 of
|
|
[First stable release](#first-stable-release) or step 3 below, from a prior
|
|
run of this same procedure).
|
|
|
|
1. Confirm the last released version on this line and the next patch number:
|
|
```powershell
|
|
gh api repos/OPCFoundation/UA-.NETStandard/releases --paginate --jq '.[].tag_name' |
|
|
Where-Object { $_ -match '^2\.0\.\d+$' } | Sort-Object { [version]$_ }
|
|
```
|
|
The next patch is exactly one more than the highest result - never skip a
|
|
number and never reuse one.
|
|
|
|
2. Collect and backport the fixes intended for this patch (see
|
|
[Routine backport](#routine-backport) for each one) before continuing.
|
|
|
|
3. Open a PR against `release/<major>.<minor>` that sets `version.json`'s
|
|
`"version"` to the exact next patch (e.g. `"2.0.1"`, no prerelease label),
|
|
re-verifying the preview-number check from
|
|
[Common preflight](#common-preflight) step 5. Merge once checks pass. This
|
|
is the release commit.
|
|
|
|
4. Continue with [Candidate and dry run](#candidate-and-dry-run), then
|
|
[Approved promotion](#approved-promotion).
|
|
|
|
5. After promotion, open a PR against the same release branch advancing
|
|
`version.json` to the *following* patch's preview version (e.g.
|
|
`"2.0.2-preview.{height}"`) before accepting further servicing work.
|
|
|
|
## Minor release
|
|
|
|
Ships the next minor line (for example `2.1.0` after `2.0.x`).
|
|
|
|
**Owner**: a repository maintainer.
|
|
|
|
**Preconditions**: the [common preflight](#common-preflight) is complete;
|
|
`master`'s current committed root version is the new minor's preview version
|
|
(e.g. `2.1.0-preview.{height}`, set when the *previous* minor line was cut -
|
|
see step 3 of [First stable release](#first-stable-release)).
|
|
|
|
This procedure is identical to [First stable release](#first-stable-release)
|
|
with two substitutions: create `release/<new-minor>` (e.g. `release/2.1`)
|
|
instead of `release/2.0`, and advance `master` to the minor *after* that
|
|
(e.g. `2.2.0-preview.{height}`) instead of `2.1.0-preview.{height}`. Every
|
|
older maintained line (e.g. `release/2.0`) is unaffected and continues to
|
|
receive its own patches independently through
|
|
[Patch release](#patch-release); do not merge the new minor's branch into or
|
|
out of an older line.
|
|
|
|
## Routine backport
|
|
|
|
Ports an already-merged, already-tested fix from `master` onto a release
|
|
branch for an upcoming patch.
|
|
|
|
**Owner**: any contributor.
|
|
|
|
**Preconditions**: the fix already has a merged commit on `master`; the
|
|
[common preflight](#common-preflight) is complete; you know the target
|
|
`release/<major>.<minor>` branch and the commit SHA to port.
|
|
|
|
1. Branch from the release line, not from `master`:
|
|
```powershell
|
|
git fetch origin release/2.0
|
|
git switch -c backport-<short-description> origin/release/2.0
|
|
```
|
|
|
|
2. Cherry-pick the fix with traceability to its source commit. Never
|
|
cherry-pick a version-only or release-preparation commit (that would
|
|
collide with the target branch's own version state):
|
|
```powershell
|
|
git cherry-pick -x <source-commit-sha>
|
|
```
|
|
Resolve conflicts so only the intended fix lands - do not pull in
|
|
unrelated `master`-only changes, and do not alter `version.json` or
|
|
`preview-version.props` in this PR.
|
|
|
|
3. Push and open a PR **against the release branch**
|
|
(`release/<major>.<minor>`, not `master`). Reference the original `master`
|
|
PR/commit in the description.
|
|
*Completion evidence*: required checks pass on the release branch build;
|
|
the PR is reviewed and merged.
|
|
|
|
4. Confirm the fix is included in the next
|
|
[Patch release](#patch-release) for that line.
|
|
|
|
## Emergency hotfix
|
|
|
|
For a production-only defect that cannot wait for the normal
|
|
`master`-first flow (for example a security fix needed on an older
|
|
maintained line before it can land anywhere else).
|
|
|
|
**Owner**: a repository maintainer, in coordination with whoever authors the
|
|
fix.
|
|
|
|
**Preconditions**: the [common preflight](#common-preflight) is complete; you
|
|
have identified every maintained release line the defect affects (there may
|
|
be more than one) and the current state of `master`.
|
|
|
|
1. Start on the **oldest** affected maintained release branch. Branch, fix,
|
|
and open a PR against that release branch exactly as in
|
|
[Routine backport](#routine-backport) steps 1-3, except the fix itself is
|
|
authored fresh here rather than cherry-picked from an existing commit.
|
|
|
|
2. Once merged there, forward-port the same fix (cherry-pick with `-x` from
|
|
the release-branch commit this time) into every newer affected maintained
|
|
release line, in ascending order, each through its own PR against that
|
|
line.
|
|
|
|
3. Forward-port the same fix into `master` last, through an ordinary PR.
|
|
|
|
4. Track the forward-port chain to completion explicitly (for example in the
|
|
original issue or PR description): list every affected line, and do not
|
|
consider the hotfix done until each one has a merged PR. A hotfix that
|
|
never reaches `master` will keep reappearing on every future release of
|
|
the older line.
|
|
|
|
5. Include the fix in the next [Patch release](#patch-release) for each
|
|
affected line.
|
|
|
|
## Candidate and dry run
|
|
|
|
Produces (or locates) a validated, signed package set from `nuget-publish.yml`
|
|
and inspects it without publishing anything. Every release and every dry run
|
|
starts here.
|
|
|
|
**Owner**: any contributor can run this read-only inspection; only a
|
|
maintainer can subsequently approve [Approved promotion](#approved-promotion).
|
|
|
|
**Preconditions**: the [common preflight](#common-preflight) is complete.
|
|
|
|
1. Find the `nuget-publish.yml` run for the commit you care about:
|
|
```powershell
|
|
gh run list --workflow nuget-publish.yml --branch release/2.0 --limit 5
|
|
```
|
|
*Completion evidence*: a `run id` with `status: completed` and
|
|
`conclusion: success`. If none exists yet for your commit, either wait for
|
|
the automatic run (pushes to `master` and canonical release branches
|
|
trigger it) or start one manually:
|
|
```powershell
|
|
gh workflow run nuget-publish.yml --ref release/2.0
|
|
```
|
|
|
|
2. Download that run's `opcua-packages-<run id>` artifact and inspect its
|
|
manifest:
|
|
```powershell
|
|
gh run download <run-id> --name opcua-packages-<run-id> --dir ./candidate
|
|
Get-Content ./candidate/release-manifest.json | ConvertFrom-Json |
|
|
Select-Object basePackageVersion, channel, packageCount, debugPackageCount, ref, commit
|
|
```
|
|
*Completion evidence*: `basePackageVersion`, `channel`, and `ref` match
|
|
what you expect (a stable version only from a canonical `release/*`
|
|
branch; a preview version from `master` or an in-progress release
|
|
branch). `debugPackageCount` is greater than zero and less than
|
|
`packageCount` (both Release and Debug package IDs are present).
|
|
|
|
3. To prove the set is eligible for promotion **without publishing anything**,
|
|
run the exact validation `release.yml` itself performs:
|
|
```powershell
|
|
. ./.azurepipelines/package-version-policy.ps1
|
|
$manifest = Get-Content ./candidate/release-manifest.json | ConvertFrom-Json
|
|
./.azurepipelines/validate-nuget-package-set.ps1 `
|
|
-PackageDirectory ./candidate `
|
|
-ManifestPath ./candidate/current-package-set.json `
|
|
-ExpectedVersion $manifest.basePackageVersion `
|
|
-RequireDebug `
|
|
-VerifySignatures
|
|
Test-CanonicalReleaseBranchForPackageVersion `
|
|
-Ref $manifest.ref -Version $manifest.basePackageVersion
|
|
```
|
|
*Completion evidence*: the validation script succeeds with no thrown
|
|
error, and `Test-CanonicalReleaseBranchForPackageVersion` prints `True`
|
|
for a release you intend to promote.
|
|
|
|
Use the version-aware predicate rather than the branch-shape-only
|
|
`Test-CanonicalReleaseBranchRef`: `release.yml` calls both, and only this
|
|
one rejects the mismatch that a shape check cannot see - a stable `2.1.0`
|
|
candidate built from `release/2.0`. A `False` here means either the ref is
|
|
not a canonical `release/<major>.<minor>` branch, the version is not an
|
|
exact stable `major.minor.patch`, or the two disagree; print `$manifest.ref`
|
|
and `$manifest.basePackageVersion` to see which. For a **preview**
|
|
candidate this predicate returns `False` by design, because a preview
|
|
version is not a stable release version. For a preview you intend to
|
|
publish with [Preview release](#preview-release), pass `-AllowPreview`,
|
|
exactly as `release.yml` does for a `channel: preview` dispatch.
|
|
|
|
For a stable candidate, also prove that the preview-only families in the
|
|
set still sort strictly above everything already published, using the same
|
|
script both workflows run:
|
|
```powershell
|
|
$env:GITHUB_TOKEN = (gh auth token)
|
|
./.azurepipelines/validate-preview-package-ordering.ps1 `
|
|
-ManifestPath ./candidate/current-package-set.json `
|
|
-GitHubPackagesOwner OPCFoundation
|
|
```
|
|
*Completion evidence*: the script prints that every preview-family package
|
|
ID sorts above its published versions and exits `0`. It returns early
|
|
(also `0`) when the manifest's `channel` is not `stable`, because a
|
|
preview candidate carries no synthesized `-preview.N` number to compare.
|
|
A non-zero exit lists every offending package ID and published version -
|
|
see [Failed or partial release](#failed-or-partial-release) step 2 for
|
|
how to recover.
|
|
|
|
Also prove that no version this candidate occupies is already held by a
|
|
different build, using the gate `release.yml` runs before either push:
|
|
```powershell
|
|
$env:GITHUB_TOKEN = (gh auth token)
|
|
./.azurepipelines/assert-published-packages-match.ps1 `
|
|
-PackageDirectory ./candidate `
|
|
-ManifestPath ./candidate/current-package-set.json `
|
|
-Feed 'nuget.org' -ExcludeDebugPackages
|
|
./.azurepipelines/assert-published-packages-match.ps1 `
|
|
-PackageDirectory ./candidate `
|
|
-ManifestPath ./candidate/current-package-set.json `
|
|
-Feed 'GitHub Packages'
|
|
```
|
|
*Completion evidence*: each invocation prints how many already-published
|
|
packages are byte-identical to this candidate and how many are not yet
|
|
published, and exits `0`. On a first release every package is "not yet
|
|
published"; on a recovery re-run the already-pushed ones must report as
|
|
identical. A non-zero exit means the version is taken by other bytes -
|
|
see [Failed or partial release](#failed-or-partial-release) step 3.
|
|
|
|
4. Alternatively, dry-run the actual promotion workflow itself (still no
|
|
publication occurs):
|
|
```powershell
|
|
gh workflow run release.yml --ref release/<major>.<minor> `
|
|
-f release_run_id=<run-id> -f dry_run=true
|
|
```
|
|
`--ref` is required and must name the same canonical release branch the
|
|
candidate was built from: `release.yml` refuses to run when its own
|
|
`GITHUB_REF` differs from the candidate run's branch. The job deliberately
|
|
checks out nothing until it has resolved the candidate, then performs its
|
|
one and only checkout at that run's exact commit - so no repository code
|
|
runs with the `release` environment's credentials before the workspace is
|
|
bound to the bytes being promoted, and the validation policy and promotion
|
|
code are source-bound to them even if the release branch has advanced since
|
|
the candidate was built. Without `--ref`, `gh` dispatches from the default
|
|
branch and the run stops at "Validate selected publish run".
|
|
|
|
*Completion evidence*: the run succeeds through "Validate promotion
|
|
manifest and package bytes", "Verify preview package ordering" and
|
|
"Verify already-published package identity" - the last two are read-only,
|
|
so a dry run proves the same feed-state preconditions the real promotion
|
|
will re-check; the summary reports the expected version and package count;
|
|
no "Push to nuget.org" or "Push to GitHub Packages" step ran (both are
|
|
skipped under `dry_run`).
|
|
|
|
## Approved promotion
|
|
|
|
Publishes an already-built, already-signed stable candidate to nuget.org and
|
|
GitHub Packages. nuget.org receives only non-`.Debug` package IDs (and their
|
|
symbol packages); GitHub Packages receives the complete candidate, including
|
|
`.Debug` package IDs. This is the **only** path that ever publishes a stable
|
|
release; nothing else in this repository's CI does so automatically.
|
|
|
|
**Owner**: a repository maintainer with `release` GitHub Environment
|
|
reviewer access.
|
|
|
|
**Preconditions**: [Candidate and dry run](#candidate-and-dry-run) is
|
|
complete and its manifest shows `channel: stable` and the exact expected
|
|
version; the run's source branch is the canonical `release/<major>.<minor>`
|
|
line you intend to release; the change has the required approvals.
|
|
|
|
`release.yml` re-runs `validate-preview-package-ordering.ps1` itself, before
|
|
it authenticates to any feed, so an ordering violation that appeared after
|
|
your dry run stops the promotion before the first public write rather than
|
|
midway through it. It then runs `assert-published-packages-match.ps1` against
|
|
both feeds before **and after** publication: `dotnet nuget push
|
|
--skip-duplicate` treats an existing id/version as a successful no-op without
|
|
looking at it, so the preflight proves that every version the candidate already
|
|
occupies holds this candidate's own content. The mandatory post-push check waits
|
|
for feed indexing and then proves every expected package is present with the
|
|
candidate's content, closing the race where another publisher claims an
|
|
immutable id/version between the preflight and the push. Comparison ignores the
|
|
`.signature.p7s` part,
|
|
because nuget.org repository-signs packages at ingestion and the bytes it
|
|
serves therefore legitimately differ from the bytes that were pushed;
|
|
everything else must match exactly. A version claimed by a different build
|
|
fails the promotion instead of being silently skipped.
|
|
|
|
1. Trigger the promotion for real, from the same canonical release branch the
|
|
candidate was built from:
|
|
```powershell
|
|
gh workflow run release.yml --ref release/<major>.<minor> `
|
|
-f release_run_id=<run-id> -f dry_run=false
|
|
```
|
|
2. Approve the pending deployment to the `release` environment when
|
|
prompted (in the GitHub UI, or `gh run watch <new-run-id>` from the CLI).
|
|
*Completion evidence*: the run proceeds past "Push to nuget.org" and
|
|
"Push to GitHub Packages" without error. Each step ends by reporting that
|
|
its post-push publication is byte-identical to the candidate. The nuget.org
|
|
log explicitly says it is publishing only non-Debug package IDs; the GitHub
|
|
Packages loop covers the complete candidate, including its `.Debug`
|
|
packages.
|
|
|
|
3. Confirm the non-Debug packages are live on nuget.org:
|
|
```powershell
|
|
Invoke-RestMethod 'https://api.nuget.org/v3-flatcontainer/opcfoundation.netstandard.opc.ua.core/index.json' |
|
|
Select-Object -ExpandProperty versions | Select-Object -Last 3
|
|
```
|
|
*Completion evidence*: the expected exact stable version (e.g. `2.0.0`)
|
|
appears. Do not expect any `.Debug` package ID on nuget.org; inspect
|
|
GitHub Packages for those IDs instead.
|
|
|
|
4. The successful promotion automatically creates the version tag and GitHub
|
|
Release, both bound to the exact source SHA recorded in the promotion
|
|
manifest as `commit`. On a recovery re-run, it verifies that any existing
|
|
tag and Release already point to that same candidate, and fails rather than
|
|
moving either one.
|
|
|
|
5. Record the [release handoff](#release-handoff-template) in the GitHub
|
|
Release description or the tracking issue.
|
|
|
|
6. Return to the calling procedure ([First stable release](#first-stable-release),
|
|
[Patch release](#patch-release), or [Minor release](#minor-release)) to
|
|
complete its final "advance to the next preview version" step.
|
|
|
|
## Preview release
|
|
|
|
Publishes a numbered preview of a release line (e.g. `2.0.0-preview.6`) to
|
|
nuget.org. `nuget-publish.yml` only ever pushes previews to GitHub Packages,
|
|
so this is the only way a preview reaches nuget.org. It reuses the
|
|
[Approved promotion](#approved-promotion) machinery, dispatched with
|
|
`channel=preview`.
|
|
|
|
**Owner**: a repository maintainer with `release` GitHub Environment
|
|
reviewer access.
|
|
|
|
**Preconditions**: the [common preflight](#common-preflight) is complete; the
|
|
canonical `release/<major>.<minor>` branch exists; nothing on nuget.org or
|
|
GitHub Packages already holds the preview number you intend to publish.
|
|
|
|
What stays the same for the GitHub Packages preview feed:
|
|
|
|
- `master` keeps publishing `<base>-preview.<height>.g<commit>` to GitHub
|
|
Packages automatically. Those versions carry a commit id, so they can never
|
|
collide with a bare release-line `<base>-preview.N`, and `release.yml`
|
|
rejects them for `channel=preview`.
|
|
- Every build of the release branch already pushed its own
|
|
`<base>-preview.N` to GitHub Packages. `release.yml`'s GitHub Packages step
|
|
is therefore a verified no-op for a preview: the identity preflight and the
|
|
post-push check prove the feed holds this candidate's exact bytes.
|
|
|
|
1. **Choose the preview number.** A canonical release branch is a public
|
|
release for Nerdbank.GitVersioning, so its previews are
|
|
`<base>-preview.<height + versionHeightOffset>`, and a branch freshly cut
|
|
from `master` inherits `master`'s height. To make the next build carry a
|
|
specific number (for example to continue after `2.0.0-preview.5`), commit
|
|
a `versionHeightOffset` change on the release branch through a normal PR,
|
|
and check the result on that commit before merging:
|
|
```powershell
|
|
nbgv get-version -v NuGetPackageVersion
|
|
```
|
|
*Completion evidence*: it prints exactly the intended version, e.g.
|
|
`2.0.0-preview.6`. The offset is negative when `master`'s height is already
|
|
past the target. Each later commit on the branch increases the number by
|
|
one, so publish the build of the exact commit you verified.
|
|
|
|
2. Find that commit's successful `nuget-publish.yml` run and inspect it as
|
|
in [Candidate and dry run](#candidate-and-dry-run) steps 1-2.
|
|
*Completion evidence*: `channel` is `preview`, `basePackageVersion` is the
|
|
intended version with no `.g<commit>` suffix, and `ref` is the release
|
|
branch.
|
|
|
|
3. Dry-run, then publish, from the same release branch:
|
|
```powershell
|
|
gh workflow run release.yml --ref release/<major>.<minor> `
|
|
-f release_run_id=<run-id> -f channel=preview -f dry_run=true
|
|
gh workflow run release.yml --ref release/<major>.<minor> `
|
|
-f release_run_id=<run-id> -f channel=preview -f dry_run=false
|
|
```
|
|
Approve the `release` environment deployment when prompted.
|
|
*Completion evidence*: the run passes "Validate promotion manifest and
|
|
package bytes", reports its nuget.org publication byte-identical to the
|
|
candidate, and the summary shows `Channel: preview`. A candidate whose
|
|
version does not match the dispatched channel fails in the validation
|
|
step, before anything is published.
|
|
|
|
4. Confirm the preview is live on nuget.org:
|
|
```powershell
|
|
Invoke-RestMethod 'https://api.nuget.org/v3-flatcontainer/opcfoundation.netstandard.opc.ua.core/index.json' |
|
|
Select-Object -ExpandProperty versions | Select-Object -Last 3
|
|
```
|
|
*Completion evidence*: the intended preview version appears. Recover a
|
|
partial preview publication exactly as in
|
|
[Failed or partial release](#failed-or-partial-release), keeping
|
|
`channel=preview` on the re-run.
|
|
|
|
5. Confirm the promotion created a prerelease GitHub Release and version tag
|
|
for the exact candidate commit:
|
|
```powershell
|
|
git rev-parse 2.0.0-preview.6
|
|
gh release view 2.0.0-preview.6 --json targetCommitish,isPrerelease
|
|
```
|
|
|
|
## Failed or partial release
|
|
|
|
Recovers from an error anywhere in [Candidate and dry run](#candidate-and-dry-run)
|
|
or [Approved promotion](#approved-promotion).
|
|
|
|
**Owner**: a repository maintainer.
|
|
|
|
1. **Determine whether any public write already happened.** Check nuget.org
|
|
for a non-Debug package and GitHub Packages for the complete candidate
|
|
(including its `.Debug` package IDs):
|
|
```powershell
|
|
Invoke-RestMethod 'https://api.nuget.org/v3-flatcontainer/opcfoundation.netstandard.opc.ua.core/index.json' |
|
|
Select-Object -ExpandProperty versions
|
|
gh api '/orgs/OPCFoundation/packages/nuget/OPCFoundation.NetStandard.Opc.Ua.Core/versions' --paginate |
|
|
ConvertFrom-Json | ForEach-Object name
|
|
```
|
|
|
|
2. **If nothing published yet** (the failure was in validation, signing, or
|
|
before any `dotnet nuget push`): fix the underlying problem (a code fix
|
|
via [Routine backport](#routine-backport)/[Emergency hotfix](#emergency-hotfix),
|
|
or a version/config fix via a normal PR against the release branch), which
|
|
produces a **new** `nuget-publish.yml` run and a new candidate. Start over
|
|
at [Candidate and dry run](#candidate-and-dry-run) with that new run - do
|
|
not attempt to reuse or repair the failed one.
|
|
|
|
Two failures are specific to the preview-ordering gate, and both stop the
|
|
run before any public write:
|
|
|
|
| Symptom | Cause and safe next action |
|
|
| --- | --- |
|
|
| `validate-preview-package-ordering.ps1` lists package IDs whose published versions are not below the candidate's `-preview.N` | Ordinary `master` development published a higher `<base>-preview.{height}` while this release was being prepared. Raise `PreviewPackageBuildNumber` in `preview-version.props` above every listed version, via a normal PR against the release branch, then build and validate a **new** candidate. Never lower a published version or delete packages to make room. |
|
|
| The script throws while querying nuget.org or GitHub Packages (for example `401`/`403`, or any non-`404` error status) | The gate deliberately fails closed: it cannot prove ordering, so it refuses to let the release proceed. Do **not** bypass it. Confirm the workflow passes a token with `packages: read`/`packages: write` for the owner being queried, and that the feed is reachable, then re-run the same workflow. A genuinely unpublished package ID returns `404`, which the script treats as "nothing to compare" and allows. |
|
|
|
|
3. **If nuget.org already has some non-Debug packages, or GitHub Packages has
|
|
some expected packages** for the version (a partial push - `dotnet nuget
|
|
push` is per-package and can fail partway through): re-run
|
|
[Approved promotion](#approved-promotion) against the **same**
|
|
`release_run_id`. `--skip-duplicate` makes re-pushing the packages that
|
|
already succeeded a no-op; only the missing ones are actually written.
|
|
`.Debug` package IDs are intentionally absent from nuget.org, so their
|
|
absence there is not a partial-release symptom.
|
|
|
|
The re-run is safe because `assert-published-packages-match.ps1` runs
|
|
before and after each push. The preflight proves that every version this
|
|
candidate already occupies really holds this candidate's own bytes; the
|
|
post-push check proves that every package now present is still this
|
|
candidate, including a duplicate accepted by `--skip-duplicate` (see the
|
|
gate table below). Because that proof exists,
|
|
`validate-preview-package-ordering.ps1` deliberately does **not** treat
|
|
the candidate's own already-published `-preview.N` as a violation -
|
|
otherwise the recovery in this step would be unreachable. Any *higher*
|
|
or more-qualified preview version still blocks.
|
|
|
|
Never build a new candidate for a version that already has *any* packages
|
|
published - that would risk two different byte sequences under the same
|
|
immutable version. Investigate why the previous attempt stopped before
|
|
retrying.
|
|
|
|
| Symptom | Cause and safe next action |
|
|
| --- | --- |
|
|
| `assert-published-packages-match.ps1` reports a feed that "already serves ... with different content" | A different build permanently claimed this candidate's immutable version. NuGet versions cannot be overwritten, so this candidate can never be published under it. Do **not** bypass the gate. Move to step 4 and release a new version. |
|
|
| The script throws while downloading a package for comparison (any non-`404` error status) | It fails closed: it cannot decide whether the version is taken, so it refuses to push. Confirm feed reachability and the token, then re-run. A genuinely unpublished version returns `404`, which the script treats as "nothing to prove" and allows. |
|
|
|
|
4. **If the version needs to change entirely** (e.g. the approved candidate
|
|
turned out to be wrong before any push happened): do not reuse the
|
|
version number. Treat this as returning to the relevant release procedure
|
|
([First stable release](#first-stable-release)/[Patch release](#patch-release)/[Minor release](#minor-release))
|
|
with a corrected `version.json`, producing a fresh candidate for a
|
|
version nothing has ever published under.
|
|
|
|
5. **If you cannot determine what happened** (for example the workflow log
|
|
is inconclusive about whether a push completed): stop and escalate to
|
|
another maintainer before taking any further action. Never guess at a
|
|
feed's state by re-running a push blind - check it first as in step 1.
|
|
|
|
## Post-release verification
|
|
|
|
Confirms a completed [Approved promotion](#approved-promotion) left the
|
|
repository and both feeds consistent.
|
|
|
|
1. Confirm nuget.org has the exact expected non-Debug version and GitHub
|
|
Packages has the complete candidate (repeat
|
|
[Approved promotion](#approved-promotion) step 3 for GitHub Packages too):
|
|
```powershell
|
|
gh api '/orgs/OPCFoundation/packages/nuget/OPCFoundation.NetStandard.Opc.Ua.Core/versions' --paginate |
|
|
ConvertFrom-Json | ForEach-Object name | Select-Object -Last 3
|
|
```
|
|
2. Confirm the release tag and GitHub Release point at the exact commit
|
|
recorded in the promotion manifest:
|
|
```powershell
|
|
git rev-parse 2.0.0
|
|
gh release view 2.0.0 --json targetCommitish
|
|
```
|
|
3. Confirm the preview-only families sorted above every previously published
|
|
preview for the same base version (spot-check at least one, e.g.
|
|
XRegistry):
|
|
```powershell
|
|
Invoke-RestMethod 'https://api.nuget.org/v3-flatcontainer/opcfoundation.netstandard.opc.ua.xregistry/index.json' |
|
|
Select-Object -ExpandProperty versions | Select-Object -Last 5
|
|
```
|
|
4. Confirm the release branch was advanced to the next servicing preview
|
|
version (the final step of [First stable release](#first-stable-release)/[Patch release](#patch-release)/[Minor release](#minor-release)):
|
|
```powershell
|
|
git show origin/release/2.0:version.json
|
|
```
|
|
5. Confirm the stable container image aliases (if this release includes
|
|
image-tagged samples - see [Container support](ContainerReferenceServer.md))
|
|
now point at the released commit. For the newest line expect `latest`,
|
|
`release`, `<major>.<minor>` and the exact version; for a superseded
|
|
maintenance line expect `latest-<major>.<minor>`, `<major>.<minor>` and
|
|
the exact version, with `latest`/`release` still on the newer minor (see
|
|
[Branch and channel model](#branch-and-channel-model)).
|
|
6. Confirm every backport tracked for this release (see
|
|
[Routine backport](#routine-backport)/[Emergency hotfix](#emergency-hotfix))
|
|
shows as merged on every line it was required for.
|
|
|
|
*Completion evidence*: all of the above match; if any do not, treat it as a
|
|
[failed or partial release](#failed-or-partial-release) and escalate rather
|
|
than silently continuing.
|
|
|
|
## Release handoff template
|
|
|
|
Record this in the GitHub Release description, the tracking issue, or the
|
|
release PR - not in an untracked local note - whenever a release is prepared,
|
|
promoted, or handed off between people (or between an agent and a
|
|
maintainer).
|
|
|
|
```markdown
|
|
## Release handoff: <version>
|
|
|
|
- Release branch: release/<major>.<minor>
|
|
- Source commit: <sha>
|
|
- Candidate run: nuget-publish.yml run <run id>
|
|
- Promotion run: release.yml run <run id> (or: not yet promoted)
|
|
- Approvals: <who approved the release environment deployment>
|
|
- Backports included: <PR links, or "none required">
|
|
- Gates completed: [ ] candidate validated [ ] dry run [ ] promoted
|
|
[ ] tagged [ ] GitHub Release published
|
|
[ ] next preview version PR opened
|
|
- Blockers: <none, or describe>
|
|
- Next allowed action: <e.g. "approve release.yml run 12345">
|
|
```
|
|
|
|
## Historical `release/2.0.0` branch
|
|
|
|
The three-component `release/2.0.0` branch predates this release model and is
|
|
retired: `version.json`'s `publicReleaseRefSpec` no longer matches that
|
|
naming, so it can never again produce a `channel: stable` package set, and
|
|
`nuget-publish.yml`/`release.yml` both refuse to publish from or promote it.
|
|
It is kept, unmodified, as historical record of the earlier `2.0.0-preview.*`
|
|
line; do not delete it, force-push to it, or attempt to bring it back into
|
|
the maintained-line model above. All current and future `2.0.x` work happens
|
|
on `release/2.0`.
|
|
|
|
## Maintaining this document
|
|
|
|
Update this file, and the worked examples above, in the same pull request as
|
|
any change to `version.json`, `version.props`, `version.targets`,
|
|
`preview-version.props`, `.azurepipelines/package-version-policy.ps1`,
|
|
`.azurepipelines/validate-nuget-package-set.ps1`,
|
|
`.azurepipelines/validate-preview-package-ordering.ps1`,
|
|
`.azurepipelines/assert-published-packages-match.ps1`,
|
|
`.github/workflows/nuget-publish.yml`, `.github/workflows/release.yml`, or
|
|
the Docker/container tagging workflows. A release procedure that no longer
|
|
matches the scripts it describes is worse than no documentation at all.
|