mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
e9313ef957
## Description Two related changes to how the 2.0 line is built and shipped. ### 1. Release-branch-only package publication - `master` produces development previews only. - Stable packages can only be built from a canonical `release/<major>.<minor>` branch and are promoted manually from the exact signed candidate artifact. - Core packages use explicit `major.minor.patch` versions (`2.0.0`, `2.0.1`, then `2.1.0`); git height never changes a stable package patch. - XRegistry, WoT Connectivity, Vision, Robotics, Redundancy, Positioning, OpenUSD, ISA95, AI, DI, the relevant MCP extensions, and OpenUSD connector tools remain `-preview.N` while following the core numeric version. - nuget.org receives only non-`.Debug` package IDs; GitHub Packages retains the complete signed Release/Debug candidate. - `docs/ReleaseProcess.md` is a reproducible runbook for developers and agents, with each step stating its completion evidence. Promotion is source-bound and byte-checked: `release.yml` checks out nothing until the candidate SHA is known, and `assert-published-packages-match.ps1` proves that any version already on a feed holds *this* candidate's content before `--skip-duplicate` is allowed to skip it. Canonical release-branch naming is matched case-sensitively with no leading zeroes, and `version.json`'s `publicReleaseRefSpec`, both policy helpers and the copy `release.yml` inlines are pinned to one expression by tests. ### 2. PR CI moved to GitHub-hosted runners - `.github/workflows/buildandtest.yml` (**CI**) now owns pull-request validation outright, covering the Windows Debug/Release net48 + net10.0 solution builds and fast tests that only Azure Pipelines ran, alongside the existing Linux/macOS, all-target and NativeAOT legs. `build-and-test summary` remains the stable required check name. - `.github/workflows/nightly.yml` is a new **manual-only** (`workflow_dispatch`) full-scope workflow covering the Azure nightly workload: all solutions x seven target profiles x Debug/Release, the net472/netstandard profiles, Windows + Linux NativeAOT, and the long-running and durable tiers. - Shared deterministic discovery/batching lives in `.github/scripts/get-ci-matrix.ps1` with a local composite action, so both entry points expand the same workload manifest instead of duplicating a large embedded script. - `azure-pipelines.yml` is set to `pr: none`. - The standalone pump sample image workflow is folded into the consolidated **Images CI** workflow, which now builds all ten images and publishes an `images summary` rollup suitable as a required check. ## Related Issues No related issue provided. ## 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. - [x] 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. - [x] I have addressed **all** PR feedback received. ## Validation - `Opc.Ua.Tools.Tests` full `net10.0` suite: **959 passed, 1 skipped, 0 failed**. - Version/package-policy regression subset (`PackageVersionPolicyTests`): **60 passed, 0 failed**. - GitHub Actions **CI** run 35719807620: **149/149 jobs green** on GitHub-hosted runners. - Canonical release-branch rule verified against the pinned nbgv 3.7.115 by building throwaway branches: `release/2.0`, `release/0.9`, `release/10.42` -> `PublicRelease=True`; `release/02.0`, `release/2.00`, `Release/2.0` -> `False`. - Package identity gate verified against a live nuget.org package: served packages carry a repository `.signature.p7s`, so equality is decided on content excluding that part. Re-signing and zip reordering produce an identical digest; a one-byte content change does not. - Synthetic Release-only, Debug-only, combined, preview, duplicate, orphaned-symbol, mismatched-root and accidental-stable-family package sets exercise the shared validator. - Stable-release rehearsal in an isolated `release/2.0` clone: `Opc.Ua.Di` packed as `2.0.0-preview.6` depending on stable Core `2.0.0`; `Opc.Ua.Di.Client` likewise against stable Client `2.0.0`. - All workflow YAML, `version.json`, the package-policy PowerShell scripts and the internal release-runbook anchors parse. ## Known validation limits - Full `UA.slnx` `net48` and `net10.0` local validation remains blocked by pre-existing `NETSDK1005` and dependency-version failures in unmodified fuzz/sample projects; the corresponding checklist items intentionally stay open. - The TRX-driven test verdict added for the intermittent at-exit host stall is proven by `CiTestVerdictTests`; the stall itself has not recurred since. - `nightly.yml` has not yet been run against a full-scope Azure run for tuple-level parity — see below. ## Operational follow-up This remains a preparation PR targeting `master`. It does **not** authorize a release. Release sequence: 1. Merge this PR. 2. Create canonical `release/2.0`, advance `master` to `2.1.0-preview.{height}`, and open a separate `2.0.0` version PR against `release/2.0`. 3. Follow [`docs/ReleaseProcess.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/ReleaseProcess.md) for candidate dry-run, environment approval, promotion, tag/release finalization, and next-patch preview setup. CI migration steps still needing administrator access are tracked in [`docs/DeveloperGuide.md#migration-status`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DeveloperGuide.md#migration-status): proving `nightly.yml` parity on a trusted SHA with the private fuzz corpus provisioned, turning off definition 14's service-side PR override, retiring the Azure full-scope schedules and push trigger, and retiring the duplicate `azure-pipelines-preview.yml` publisher. > **Before cutting `release/2.0`:** the `Release` ruleset requires the Azure context for *every* `refs/heads/release/*`, but a new 2.x branch inherits `pr: none`, so that context would never report and would block every PR into the new line. Exclude the new ref from that ruleset, or add a 2.x ruleset requiring `build-and-test summary`, as part of creating the branch. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
207 lines
9.9 KiB
C#
207 lines
9.9 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
#if NET10_0
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
|
|
namespace Opc.Ua.Tools.Tests
|
|
{
|
|
/// <summary>
|
|
/// Contract tests for .github/scripts/get-path-relevance.ps1, which decides
|
|
/// whether a pull request's changes are worth building.
|
|
///
|
|
/// This rule fails dangerously in one direction only. Calling a real change
|
|
/// irrelevant skips the build, test and NativeAOT jobs while the required
|
|
/// 'build-and-test summary' still reports success, so a broken change merges
|
|
/// behind a green check. Calling an irrelevant change relevant only wastes
|
|
/// runner time. The cases below therefore concentrate on inputs that are
|
|
/// easy to forget: generator inputs, test fixtures and build configuration.
|
|
/// </summary>
|
|
[TestFixture]
|
|
public sealed class CiPathRelevanceTests
|
|
{
|
|
/// <summary>
|
|
/// Anything that is not documentation has to be built. These are the
|
|
/// build inputs an extension allow-list would have missed.
|
|
/// </summary>
|
|
[TestCase("src/Opc.Ua.Types/BuiltIn/NodeId.cs", TestName = "Source")]
|
|
[TestCase("src/Opc.Ua.WotCon/Design/Opc.Ua.WotCon.NodeSet2.xml", TestName = "GeneratorNodeSet")]
|
|
[TestCase("src/Opc.Ua.WotCon/Design/Opc.Ua.WotCon.NodeSet2.csv", TestName = "GeneratorIdentifiers")]
|
|
[TestCase("tests/Opc.Ua.Types.Tests/Wot/Assets/example.jsonld", TestName = "TestFixture")]
|
|
[TestCase(".editorconfig", TestName = "EditorConfigEnforcedAtBuildTime")]
|
|
[TestCase("Directory.Packages.props", TestName = "CentralPackageVersions")]
|
|
[TestCase("version.json", TestName = "PackageVersion")]
|
|
[TestCase(".github/workflows/images.yml", TestName = "Workflow")]
|
|
[TestCase(".github/scripts/get-ci-matrix.ps1", TestName = "MatrixExpander")]
|
|
[TestCase("fuzzing/Common/Fuzz.Tests/FuzzTargetTestsBase.cs", TestName = "Fuzz")]
|
|
[TestCase("docs.txt", TestName = "FileNamedLikeTheDocsDirectory")]
|
|
[TestCase("src/readme.markdown", TestName = "NotMarkdownByExtension")]
|
|
public async Task ChangeIsBuildRelevantAsync(string path)
|
|
{
|
|
Assert.That(await EvaluateAsync(path).ConfigureAwait(false), Is.True);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The only skippable class is documentation: Markdown anywhere, and the
|
|
/// docs/ tree, which holds nothing but Markdown and images.
|
|
/// </summary>
|
|
[TestCase("README.md", TestName = "RootMarkdown")]
|
|
[TestCase("docs/ReleaseProcess.md", TestName = "DocsMarkdown")]
|
|
[TestCase("docs/images/architecture.png", TestName = "DocsImage")]
|
|
[TestCase("src/Opc.Ua.Client/README.md", TestName = "NestedMarkdown")]
|
|
[TestCase("CONTRIBUTING.MD", TestName = "UppercaseExtension")]
|
|
public async Task ChangeIsSkippableAsync(string path)
|
|
{
|
|
Assert.That(await EvaluateAsync(path).ConfigureAwait(false), Is.False);
|
|
}
|
|
|
|
/// <summary>
|
|
/// One build-relevant file among documentation changes still has to
|
|
/// build; the rule stops at the first file that matters.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OneRelevantFileAmongDocumentationStillBuildsAsync()
|
|
{
|
|
bool relevant = await EvaluateAsync("README.md", "docs/DeveloperGuide.md", "src/Opc.Ua.Types/Guid.cs")
|
|
.ConfigureAwait(false);
|
|
|
|
Assert.That(relevant, Is.True);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An empty diff changes nothing, so there is nothing to rebuild.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task EmptyChangeSetIsNotRelevantAsync()
|
|
{
|
|
Assert.That(await EvaluateAsync().ConfigureAwait(false), Is.False);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The workflow has to call this script rather than carry a second copy
|
|
/// of the rule, because only this one is tested.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task WorkflowDelegatesTheDecisionToThisScriptAsync()
|
|
{
|
|
string workflow = Path.Combine(FindRepositoryRoot(), ".github", "workflows", "buildandtest.yml");
|
|
string source = await File.ReadAllTextAsync(workflow).ConfigureAwait(false);
|
|
|
|
Assert.That(source, Does.Contain("./.github/scripts/get-path-relevance.ps1 -ChangedFile $changed"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// An unavailable base ref must fail the gate rather than look like an
|
|
/// empty, documentation-only diff. Both fixed-name required summaries
|
|
/// depend on these internal relevance checks.
|
|
/// </summary>
|
|
[TestCase("buildandtest.yml", "$relevantChanges = [bool]")]
|
|
[TestCase("docker-image.yml", "$relevantChanges = $false")]
|
|
public async Task WorkflowFailsClosedWhenGitDiffFailsAsync(string workflowName, string relevanceDecision)
|
|
{
|
|
string workflow = Path.Combine(FindRepositoryRoot(), ".github", "workflows", workflowName);
|
|
string source = await File.ReadAllTextAsync(workflow).ConfigureAwait(false);
|
|
int diff = source.IndexOf("$changed = @(& git diff --name-only", StringComparison.Ordinal);
|
|
int exitCheck = source.IndexOf("if ($LASTEXITCODE -ne 0)", diff, StringComparison.Ordinal);
|
|
int decision = source.IndexOf(relevanceDecision, diff, StringComparison.Ordinal);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(diff, Is.GreaterThanOrEqualTo(0));
|
|
Assert.That(exitCheck, Is.GreaterThan(diff), "git diff must be checked immediately.");
|
|
Assert.That(
|
|
decision,
|
|
Is.GreaterThan(exitCheck),
|
|
"Relevance must not be evaluated after a failed git diff.");
|
|
Assert.That(
|
|
source[exitCheck..decision],
|
|
Does.Contain("throw"),
|
|
"A failed diff must fail the gate rather than become an empty change set.");
|
|
});
|
|
}
|
|
|
|
private static async Task<bool> EvaluateAsync(params string[] changedFiles)
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
using var process = new Process();
|
|
process.StartInfo.FileName = "pwsh";
|
|
process.StartInfo.WorkingDirectory = root;
|
|
process.StartInfo.RedirectStandardOutput = true;
|
|
process.StartInfo.RedirectStandardError = true;
|
|
PowerShellScriptOutput.ConfigureDeterministicOutput(process.StartInfo);
|
|
process.StartInfo.ArgumentList.Add("-NoProfile");
|
|
process.StartInfo.ArgumentList.Add("-File");
|
|
process.StartInfo.ArgumentList.Add(Path.Combine(root, ".github", "scripts", "get-path-relevance.ps1"));
|
|
if (changedFiles.Length != 0)
|
|
{
|
|
// 'pwsh -File' binds one argument per parameter, so an array has
|
|
// to arrive as a single comma-separated value. Omitting the
|
|
// parameter entirely is how an empty diff is expressed.
|
|
process.StartInfo.ArgumentList.Add("-ChangedFile");
|
|
process.StartInfo.ArgumentList.Add(string.Join(',', changedFiles));
|
|
}
|
|
|
|
Assert.That(process.Start(), Is.True);
|
|
Task<string> standardOutput = process.StandardOutput.ReadToEndAsync();
|
|
Task<string> standardError = process.StandardError.ReadToEndAsync();
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2));
|
|
await process.WaitForExitAsync(timeout.Token).ConfigureAwait(false);
|
|
|
|
string output = PowerShellScriptOutput.Normalize(await standardOutput.ConfigureAwait(false));
|
|
string error = await standardError.ConfigureAwait(false);
|
|
Assert.That(process.ExitCode, Is.Zero, output + error);
|
|
|
|
string verdict = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Last().Trim();
|
|
return bool.Parse(verdict);
|
|
}
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
string? current = TestContext.CurrentContext.TestDirectory;
|
|
while (!string.IsNullOrWhiteSpace(current))
|
|
{
|
|
if (File.Exists(Path.Combine(current, ".github", "scripts", "get-path-relevance.ps1")))
|
|
{
|
|
return current;
|
|
}
|
|
current = Directory.GetParent(current)?.FullName;
|
|
}
|
|
|
|
throw new InvalidOperationException("Could not find the repository root.");
|
|
}
|
|
}
|
|
}
|
|
#endif
|