Allow release.yml to promote release-line previews to nuget.org (#4574)

# Description

Adds a way to publish a numbered release-line preview (e.g.
`2.0.0-preview.6`) to nuget.org.

Since #4513, `nuget-publish.yml` pushes previews only to GitHub
Packages, and `release.yml` (the only nuget.org path) refused every
non-stable candidate. That left no way to continue the `2.0.0-preview.N`
series on nuget.org (last published there: `2.0.0-preview.5`).

## Changes

- **`release.yml`** gains a `channel` input (`stable` | `preview`,
default `stable`).
  - `stable` behaves exactly as before.
- `preview` accepts only a public release-line preview,
`M.m.p-preview.N` with no commit id, from the matching canonical
`release/M.m` branch. `master` / PR builds (`-preview.N.g<sha>`) are
rejected.
- The candidate manifest's recorded `channel` must equal the dispatched
one, so a stable promotion can never publish a preview by accident, or
the reverse.
- **`package-version-policy.ps1`**: new
`Test-PromotablePreviewPackageVersion`, and an `-AllowPreview` switch on
`Test-CanonicalReleaseBranchForPackageVersion`. Callers without the
switch (the stable gate in `nuget-publish.yml`) behave exactly as
before.
- **`docs/ReleaseProcess.md`**: new "Preview release" procedure,
including how to pick the preview number with `versionHeightOffset`.
- **Tests**: predicate cases, the `-AllowPreview` switch, and a pin that
`release.yml` defaults to `stable` and relaxes the gate only for
`channel=preview`.

## GitHub Packages preview feed is unchanged

- `nuget-publish.yml` is not touched. `master` keeps auto-publishing
`2.0.0-preview.<height>.g<sha>` to GitHub Packages, and release-branch
builds keep pushing their own `<base>-preview.N` there.
- The commit-id suffix means `master` versions can never collide with a
bare `2.0.0-preview.N`.
- For a preview, `release.yml`'s GitHub Packages push is a verified
no-op. The candidate's own `nuget-publish.yml` run already pushed those
bytes, and the existing identity preflight and post-push check prove
they match.

## Getting `2.0.0-preview.6` after merge

1. Cut `release/2.0` from `master` after this merges. `release.yml`
executes the policy script from the candidate commit, so that commit
must contain this change.
2. On `release/2.0`, commit a `versionHeightOffset` so the build lands
on `.6`. Verified locally with nbgv 3.10.94: at the current `master` tip
(height 24), one commit setting `-19` yields `2.0.0-preview.6` and the
next yields `.7`. Recompute for the actual cut point with `nbgv
get-version -v NuGetPackageVersion`.
3. Dispatch `release.yml --ref release/2.0 -f release_run_id=<run> -f
channel=preview`, with a dry run first.

`2.0.0-preview.6` is currently free: nuget.org has `.2`–`.5`, and the
retired `release/2.0.0` pushed up to `.5` to GitHub Packages.

## Related Issues

- Follow-up to #4513. Supersedes the `master` → `release/2.0.0` merge
attempts #4566 / #4570; `release/2.0.0` is retired and cannot publish.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

Locally: `PackageVersionPolicyTests` + `NuGetPackageSetValidationTests`
on net10.0, 96/96 passed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
romanett
2026-09-29 07:45:30 +02:00
committed by GitHub
parent d623007a3f
commit a430239fde
4 changed files with 269 additions and 26 deletions
+33 -3
View File
@@ -257,6 +257,27 @@ function Test-StablePackageVersion {
return $Version -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$'
}
function Test-PromotablePreviewPackageVersion {
<#
.SYNOPSIS
Returns $true when a package version is a public release-line
preview, exactly "major.minor.patch-preview.N", for example
"2.0.0-preview.6". This is the only preview shape release.yml may
promote to nuget.org.
.DESCRIPTION
A canonical release/M.m branch is a public release for
Nerdbank.GitVersioning, so its previews carry no commit id. Every
other shape is rejected: "-preview.N.gabc123" and "+gabc123" are
non-public builds (master, PR and feature branches), and anything
else ("-rc.1", "-preview", a four-component version) is not a
numbered preview this repository ever produces. The label is matched
case-sensitively (-cmatch): "-PREVIEW.6" is not the shape NBGV emits.
#>
param([Parameter(Mandatory)][string]$Version)
return $Version -cmatch '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-preview\.(0|[1-9]\d*)$'
}
function Test-CanonicalReleaseBranchRef {
<#
.SYNOPSIS
@@ -304,13 +325,22 @@ function Test-CanonicalReleaseBranchForPackageVersion {
comparison below can stay textual: "release/02.0" is not an alternate
spelling of the 2.0 line, it is simply not a release line. See
Test-CanonicalReleaseBranchRef for why case and leading zeroes matter.
-AllowPreview additionally accepts a public release-line preview
(Test-PromotablePreviewPackageVersion, e.g. "2.0.0-preview.6") under
the same major/minor rule. Only release.yml's explicit preview
promotion passes it; the stable gates in nuget-publish.yml and
release.yml never do.
#>
param(
[Parameter(Mandatory)][string]$Ref,
[Parameter(Mandatory)][string]$Version
[Parameter(Mandatory)][string]$Version,
[switch]$AllowPreview
)
if (-not (Test-StablePackageVersion -Version $Version)) {
$isStable = Test-StablePackageVersion -Version $Version
$isPreview = $AllowPreview -and (Test-PromotablePreviewPackageVersion -Version $Version)
if (-not ($isStable -or $isPreview)) {
return $false
}
@@ -321,7 +351,7 @@ function Test-CanonicalReleaseBranchForPackageVersion {
}
$versionMatch = [regex]::Match(
$Version, '^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?:0|[1-9]\d*)$')
$Version, '^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-preview\.(?:0|[1-9]\d*))?$')
return $versionMatch.Success -and
$branchMatch.Groups['major'].Value -eq $versionMatch.Groups['major'].Value -and
$branchMatch.Groups['minor'].Value -eq $versionMatch.Groups['minor'].Value
+62 -18
View File
@@ -4,13 +4,15 @@ name: Release to nuget.org
# nuget-publish.yml run on a canonical "release/<major>.<minor>" branch (e.g.
# release/2.0). The release environment provides the approval boundary (its
# deployment_branch_policy restricts execution to release/* branches) and the
# NuGet trusted publishing identity. This workflow additionally requires the
# candidate's base package version to be an exact stable release (no
# prerelease label or build metadata, e.g. "2.0.0") - a preview candidate is
# already published automatically by nuget-publish.yml and must never be
# promoted here. See docs/ReleaseProcess.md ("Approved promotion") for the
# full procedure, including how to pick a release_run_id and how to recover
# from a partial or failed promotion.
# NuGet trusted publishing identity. The "channel" input states what is being
# promoted and must match the candidate: "stable" (the default) requires an
# exact stable release (no prerelease label or build metadata, e.g. "2.0.0");
# "preview" requires a public release-line preview (e.g. "2.0.0-preview.6").
# nuget-publish.yml only ever pushes previews to GitHub Packages, so this is
# the one path that puts a preview on nuget.org. See docs/ReleaseProcess.md
# ("Approved promotion" and "Preview release") for the full procedures,
# including how to pick a release_run_id and how to recover from a partial or
# failed promotion.
#
# One-time nuget.org trusted publishing policy:
# Repository Owner: OPCFoundation
@@ -25,6 +27,14 @@ on:
description: Successful nuget-publish.yml run ID from a release/<major>.<minor> branch
required: true
type: string
channel:
description: What the candidate is - an exact stable release, or a release-line preview (e.g. 2.0.0-preview.6)
required: false
type: choice
options:
- stable
- preview
default: stable
dry_run:
description: Validate the selected package set without publishing
required: false
@@ -157,6 +167,7 @@ jobs:
RELEASE_RUN_ID: ${{ inputs.release_run_id }}
RELEASE_BRANCH: ${{ steps.source.outputs.branch }}
RELEASE_COMMIT: ${{ steps.source.outputs.commit }}
RELEASE_CHANNEL: ${{ inputs.channel }}
run: |
. ./.azurepipelines/package-version-policy.ps1
$manifestPath = 'promotion/release-manifest.json'
@@ -182,24 +193,53 @@ jobs:
$manifest.packageVersion
}
# This workflow exists solely to promote a stable release (e.g.
# "2.0.0", "2.0.1", "2.1.0") to nuget.org. In-development preview
# candidates are already published automatically by
# nuget-publish.yml and must never be promoted here. See
# docs/ReleaseProcess.md ("Approved promotion").
if (-not (Test-StablePackageVersion -Version $expectedBaseVersion)) {
# The dispatcher states which kind of release this is, and the
# candidate must be exactly that: a stable promotion can never
# publish a preview by accident, nor the reverse. A preview must be
# a public release-line build ("M.m.p-preview.N", no commit id), so
# master or feature-branch previews can never reach nuget.org. See
# docs/ReleaseProcess.md ("Approved promotion", "Preview release").
$channel = $env:RELEASE_CHANNEL
if ($channel -cne 'stable' -and $channel -cne 'preview') {
throw "channel must be 'stable' or 'preview', not '$channel'."
}
if ($channel -ceq 'stable' -and
-not (Test-StablePackageVersion -Version $expectedBaseVersion)) {
throw (
"Run $($env:RELEASE_RUN_ID) produced base version " +
"'$expectedBaseVersion', which is not an exact stable release. Only a " +
'stable candidate (no prerelease label or build metadata) may be ' +
'promoted through this workflow.')
"promoted with channel 'stable'; dispatch with channel 'preview' to " +
'publish a release-line preview.')
}
if ($channel -ceq 'preview' -and
-not (Test-PromotablePreviewPackageVersion -Version $expectedBaseVersion)) {
throw (
"Run $($env:RELEASE_RUN_ID) produced base version " +
"'$expectedBaseVersion', which is not a release-line preview. Only an " +
"'M.m.p-preview.N' candidate without a commit id may be promoted with " +
"channel 'preview'.")
}
# schemaVersion 1 manifests predate the recorded channel. They stay
# promotable as stable, but a preview must prove its channel.
if ($channel -ceq 'preview' -and $manifest.schemaVersion -ne 2) {
throw (
"The promotion manifest is schemaVersion $($manifest.schemaVersion), which " +
"records no channel. Only a schemaVersion 2 candidate may be promoted " +
"with channel 'preview'.")
}
if ($manifest.schemaVersion -eq 2 -and $manifest.channel -cne $channel) {
throw (
"The promotion manifest records channel '$($manifest.channel)', but this " +
"promotion was dispatched with channel '$channel'.")
}
if (-not (Test-CanonicalReleaseBranchForPackageVersion `
-Ref $expectedRef `
-Version $expectedBaseVersion)) {
-Version $expectedBaseVersion `
-AllowPreview:($channel -ceq 'preview'))) {
throw (
"Candidate version '$expectedBaseVersion' does not match release branch " +
"'$expectedRef'. A stable M.m.p candidate must be promoted only from " +
"'$expectedRef'. An M.m.p candidate must be promoted only from " +
'the matching release/M.m branch.')
}
@@ -309,7 +349,7 @@ jobs:
} |
Sort-Object Name)
if ($packages.Count -eq 0) {
throw 'The stable candidate contains no non-Debug packages to push to nuget.org.'
throw 'The candidate contains no non-Debug packages to push to nuget.org.'
}
Write-Host (
"Publishing $($packages.Count) non-Debug package(s) to nuget.org. " +
@@ -342,7 +382,9 @@ jobs:
# any feed automatically (see its "Skip feed publication" step); this
# is the one place a stable release also gets mirrored to GitHub
# Packages, gated behind the same approval and dry-run switch as the
# nuget.org push above.
# nuget.org push above. A preview candidate was already pushed here by
# its own nuget-publish.yml run, so this loop is a verified no-op for
# it: the preflight and post-push checks prove those are its bytes.
if: ${{ !inputs.dry_run }}
shell: pwsh
env:
@@ -378,6 +420,7 @@ jobs:
env:
RELEASE_RUN_ID: ${{ inputs.release_run_id }}
RELEASE_BRANCH: ${{ steps.source.outputs.branch }}
RELEASE_CHANNEL: ${{ inputs.channel }}
RELEASE_DRY_RUN: ${{ inputs.dry_run }}
run: |
$manifestPath = 'promotion/release-manifest.json'
@@ -400,6 +443,7 @@ jobs:
- Source run: ``$($env:RELEASE_RUN_ID)``
- Source branch: ``$($env:RELEASE_BRANCH)``
- Channel: ``$($env:RELEASE_CHANNEL)``
- Version: ``$version``
- Packages: ``$count``
- Dry run: ``$($env:RELEASE_DRY_RUN)``
+82 -5
View File
@@ -145,6 +145,7 @@ Every procedure below starts here.
| 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) |
@@ -159,7 +160,10 @@ Every procedure below starts here.
(`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`. At the exact commit approved for release, its root version is
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
@@ -175,8 +179,9 @@ Every procedure below starts here.
`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 or a
non-stable candidate), and the `release` GitHub Environment's
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
@@ -454,8 +459,9 @@ maintainer can subsequently approve [Approved promotion](#approved-promotion).
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 - check the shape alone with
`Test-CanonicalReleaseBranchRef -Ref $manifest.ref` in that case.
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
@@ -592,6 +598,77 @@ fails the promotion instead of being silently skipped.
[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.
## Failed or partial release
Recovers from an error anywhere in [Candidate and dry run](#candidate-and-dry-run)
@@ -280,6 +280,98 @@ namespace Opc.Ua.Tools.Tests
Assert.That(result.GetProperty("actual").GetBoolean(), Is.EqualTo(expectedMatch));
}
[TestCase("2.0.0-preview.6", true, Description = "Public release-line preview")]
[TestCase("2.0.1-preview.0", true, Description = "Zero preview number")]
[TestCase("2.1.0-preview.12", true, Description = "Multi-digit preview number")]
[TestCase("2.0.0", false, Description = "Stable is not a preview")]
[TestCase("2.0.0-preview.1.gabc123def0", false, Description = "Non-public build carries a commit id")]
[TestCase("2.0.0-preview.6+gabc123def0", false, Description = "Build metadata")]
[TestCase("2.0.0-PREVIEW.6", false, Description = "Upper-case label")]
[TestCase("2.0.0-Preview.6", false, Description = "Mixed-case label")]
[TestCase("2.0.0-preview", false, Description = "Unnumbered preview")]
[TestCase("2.0.0-preview.06", false, Description = "Leading zero in the preview number")]
[TestCase("2.0.0-rc.1", false, Description = "Other prerelease label")]
[TestCase("2.0.0.0-preview.1", false, Description = "Four-component version")]
[TestCase("2.0-preview.6", false, Description = "Two-component version")]
public async Task TestPromotablePreviewPackageVersionAsync(string version, bool expectedPreview)
{
JsonElement result = await RunPolicyScriptAsync(
$$"""
. '{{PolicyScriptPath}}'
@{ actual = (Test-PromotablePreviewPackageVersion -Version '{{version}}') } | ConvertTo-Json
""").ConfigureAwait(false);
Assert.That(result.GetProperty("actual").GetBoolean(), Is.EqualTo(expectedPreview));
}
[TestCase("refs/heads/release/2.0", "2.0.0-preview.6", true)]
[TestCase("refs/heads/release/2.0", "2.0.1-preview.3", true)]
[TestCase("refs/heads/release/2.0", "2.0.0", true, Description = "Stable still accepted")]
[TestCase("refs/heads/release/2.0", "2.1.0-preview.1", false, Description = "Other minor line")]
[TestCase("refs/heads/release/2.0", "2.0.0-PREVIEW.6", false, Description = "Upper-case label")]
[TestCase("refs/heads/release/2.0", "2.0.0-preview.6.gabc123def0", false, Description = "Non-public build")]
[TestCase("refs/heads/release/2.0", "2.0.0-preview.6+gabc123def0", false, Description = "Build metadata")]
[TestCase("refs/heads/release/2.0.0", "2.0.0-preview.6", false, Description = "Retired three-component branch")]
[TestCase("refs/heads/master", "2.0.0-preview.6", false, Description = "master never promotes")]
[TestCase("refs/heads/Release/2.0", "2.0.0-preview.6", false, Description = "Case-sensitive like nbgv")]
public async Task TestCanonicalReleaseBranchForPreviewPackageVersionAsync(
string ruleRef,
string version,
bool expectedMatch)
{
JsonElement result = await RunPolicyScriptAsync(
$$"""
. '{{PolicyScriptPath}}'
@{
allowed = (Test-CanonicalReleaseBranchForPackageVersion -Ref '{{ruleRef}}' -Version '{{version}}' -AllowPreview)
stableOnly = (Test-CanonicalReleaseBranchForPackageVersion -Ref '{{ruleRef}}' -Version '{{version}}')
} | ConvertTo-Json
""").ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(result.GetProperty("allowed").GetBoolean(), Is.EqualTo(expectedMatch));
Assert.That(
result.GetProperty("stableOnly").GetBoolean(),
Is.EqualTo(expectedMatch && !version.Contains('-', StringComparison.Ordinal)),
"Without -AllowPreview the stable gate must keep rejecting every preview.");
});
}
[Test]
public void ReleaseWorkflowPromotesAPreviewOnlyWhenDispatchedAsOne()
{
// nuget-publish.yml never pushes to nuget.org, so release.yml is
// the only path there for a preview too. It must default to the
// stable channel, accept a preview only when the dispatcher asked
// for one, and require the candidate's recorded channel to agree.
string workflow = File.ReadAllText(ReleaseWorkflowPath);
Assert.Multiple(() =>
{
Assert.That(
workflow,
Does.Match(@"channel:\s*\n(?:\s+.*\n)*?\s+default: stable\s*\n"),
"The channel input must default to 'stable'.");
Assert.That(
workflow,
Does.Contain("-AllowPreview:($channel -ceq 'preview')"),
"Only a preview dispatch may relax the release-branch version gate.");
Assert.That(
workflow,
Does.Contain("Test-PromotablePreviewPackageVersion -Version $expectedBaseVersion"),
"A preview dispatch must require a public release-line preview version.");
Assert.That(
workflow,
Does.Contain("$manifest.channel -cne $channel"),
"The candidate's recorded channel must match the dispatched channel.");
Assert.That(
workflow,
Does.Contain("$channel -ceq 'preview' -and $manifest.schemaVersion -ne 2"),
"A preview must come from a manifest that records its channel.");
});
}
[Test]
public async Task GetPreviewPackageBuildNumberMatchesCommittedPropsFileAsync()
{