From 951b8fe643f763108a712b2296ab857eaa5d03a6 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 30 Aug 2026 21:30:11 +0200 Subject: [PATCH 1/2] Registry sample: retarget net10.0, MSTest 4.3.3, Live-named registry tests, CI Live filter --- .github/workflows/pr-build.yml | 2 +- .../ReadWriteWindowsRegistryInCSharp.csproj | 2 +- .../RegistryDemo.cs | 50 +++++++------ .../Tests/RegistryDemoTests.cs | 75 ++++--------------- .../Tests/Tests.csproj | 10 +-- 5 files changed, 49 insertions(+), 90 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 155122e2ce..63f2ce1360 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -122,7 +122,7 @@ jobs: echo "Building $sln" dotnet build "$sln" --configuration Release echo "Testing $sln (no-op for folders with no test project)" - dotnet test "$sln" --configuration Release --no-build + dotnet test "$sln" --configuration Release --no-build --filter "FullyQualifiedName!~Live" # Stable, folder-independent name for branch protection to require -- the # build-and-test job's displayed name varies with the matrix (one leg per diff --git a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp.csproj b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp.csproj index f02677bf64..dfb40caafc 100644 --- a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp.csproj +++ b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net10.0 enable enable diff --git a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/RegistryDemo.cs b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/RegistryDemo.cs index c2e251a157..872f2046b9 100644 --- a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/RegistryDemo.cs +++ b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/ReadWriteWindowsRegistryInCSharp/RegistryDemo.cs @@ -42,7 +42,7 @@ public static string ReadAndWriteRegistryValueUsingRegistryClass() return string.Empty; } - var subKeyToWrite = Path.Combine(Registry.CurrentUser.Name, CodeMazeRegistryDemoSubKey); + var subKeyToWrite = $@"{Registry.CurrentUser.Name}\{CodeMazeRegistryDemoSubKey}"; Registry.SetValue(subKeyToWrite, CodeMazeRegistryDemoName, CodeMazeRegistryDemoValue); var writtenValue = Registry.GetValue(subKeyToWrite, CodeMazeRegistryDemoName, string.Empty); @@ -64,9 +64,9 @@ public static string ReadAndWriteRegistryValueUsingRegistryKeyClass() using var subKey = baseKey.OpenSubKey(CodeMazeRegistryDemoSubKey, true) ?? baseKey.CreateSubKey(CodeMazeRegistryDemoSubKey); - subKey?.SetValue(CodeMazeRegistryDemoName, CodeMazeRegistryDemoValue); - var writtenValue = subKey?.GetValue(CodeMazeRegistryDemoName); - subKey?.DeleteValue(CodeMazeRegistryDemoName); + subKey.SetValue(CodeMazeRegistryDemoName, CodeMazeRegistryDemoValue); + var writtenValue = subKey.GetValue(CodeMazeRegistryDemoName); + subKey.DeleteValue(CodeMazeRegistryDemoName); baseKey.DeleteSubKey(CodeMazeRegistryDemoSubKey); @@ -77,37 +77,37 @@ public static string[] GetSubKeyNames() { if (!OperatingSystem.IsWindows()) { - return Array.Empty(); + return []; } using var subKey = Registry.CurrentUser.CreateSubKey(CodeMazeRegistryDemoSubKey); - subKey?.CreateSubKey("SubKey1"); - subKey?.CreateSubKey("SubKey2"); + subKey.CreateSubKey("SubKey1"); + subKey.CreateSubKey("SubKey2"); - var subKeyNames = subKey?.GetSubKeyNames(); + var subKeyNames = subKey.GetSubKeyNames(); Registry.CurrentUser.DeleteSubKeyTree(CodeMazeRegistryDemoSubKey); - return subKeyNames ?? Array.Empty(); + return subKeyNames; } public static string[] GetValueNames() { if (!OperatingSystem.IsWindows()) { - return Array.Empty(); + return []; } using var subKey = Registry.CurrentUser.CreateSubKey(CodeMazeRegistryDemoSubKey); - using var subKey1 = subKey?.CreateSubKey("SubKey1"); - subKey1?.SetValue("Name1", "Value1"); - subKey1?.SetValue("Name2", "Value2"); + using var subKey1 = subKey.CreateSubKey("SubKey1"); + subKey1.SetValue("Name1", "Value1"); + subKey1.SetValue("Name2", "Value2"); - var subKeyNames = subKey1?.GetValueNames(); + var subKeyNames = subKey1.GetValueNames(); Registry.CurrentUser.DeleteSubKeyTree(CodeMazeRegistryDemoSubKey); - return subKeyNames ?? Array.Empty(); + return subKeyNames; } public static string GetValueKind() @@ -118,14 +118,14 @@ public static string GetValueKind() } using var subKey = Registry.CurrentUser.CreateSubKey(CodeMazeRegistryDemoSubKey); - using var subKey1 = subKey?.CreateSubKey("SubKey1"); - subKey1?.SetValue("Name1", "Value1"); + using var subKey1 = subKey.CreateSubKey("SubKey1"); + subKey1.SetValue("Name1", "Value1"); - var valueKind = subKey1?.GetValueKind("Name1"); + var valueKind = subKey1.GetValueKind("Name1"); Registry.CurrentUser.DeleteSubKeyTree(CodeMazeRegistryDemoSubKey); - return valueKind.ToString() ?? string.Empty; + return valueKind.ToString(); } public static bool SetRegistryKeyAccessPermissions() @@ -135,7 +135,7 @@ public static bool SetRegistryKeyAccessPermissions() return false; } - var user = Path.Combine(Environment.UserDomainName, Environment.UserName); + var user = $@"{Environment.UserDomainName}\{Environment.UserName}"; var registrySecurity = new RegistrySecurity(); var accessRule = new RegistryAccessRule(user, @@ -180,10 +180,16 @@ public static bool OpenRemoteBaseKey(string machineName) try { - var remoteBaseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.CurrentUser, machineName); + using var remoteBaseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.CurrentUser, machineName); + return true; } - catch + catch (ArgumentException) + { + // An unreachable machine and a stopped Remote Registry service both surface here. + return false; + } + catch (IOException) { return false; } diff --git a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/RegistryDemoTests.cs b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/RegistryDemoTests.cs index 791d1bd589..0bcf327aed 100644 --- a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/RegistryDemoTests.cs +++ b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/RegistryDemoTests.cs @@ -1,81 +1,54 @@ namespace Tests { + // Every test in this class reads and writes the registry of the machine that runs it, + // so it can only pass on Windows. The names carry "Live" and CI excludes them with + // --filter "FullyQualifiedName!~Live". Run them locally on Windows. [TestClass] public class RegistryDemoTests { [TestMethod] - public void WhenGetCurrentUserRootKeyName_ResultIsRootKeyCurrentUserName() + public void WhenGetCurrentUserRootKeyName_ResultIsRootKeyCurrentUserName_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var currentUserRegistryName = RegistryDemo.GetCurrentUserRootKeyName(); Assert.AreEqual(currentUserRegistryName, Registry.CurrentUser.Name); } [TestMethod] - public void WhenGetCurrentUserRootKeyNameWithPlatformCheck_ResultIsRootKeyCurrentUserName() + public void WhenGetCurrentUserRootKeyNameWithPlatformCheck_ResultIsRootKeyCurrentUserName_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var currentUserRegistryName = RegistryDemo.GetCurrentUserRootKeyNameWithPlatformCheck(); Assert.AreEqual(currentUserRegistryName, Registry.CurrentUser.Name); } [TestMethod] - public void WhenGetCurrentUserRootKeySubkeyCount_ResultIsGreaterThanZero() + public void WhenGetCurrentUserRootKeySubkeyCount_ResultIsGreaterThanZero_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var subKeyCount = RegistryDemo.GetCurrentUserRootKeySubkeyCount(); Assert.IsTrue(subKeyCount > 0); } [TestMethod] - public void WhenReadAndWriteRegistryValueUsingRegistryClass_ResultIsCodeMazeRegistryDemoValue() + public void WhenReadAndWriteRegistryValueUsingRegistryClass_ResultIsCodeMazeRegistryDemoValue_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var writtenValue = RegistryDemo.ReadAndWriteRegistryValueUsingRegistryClass(); Assert.AreEqual(writtenValue, RegistryDemo.CodeMazeRegistryDemoValue); } [TestMethod] - public void WhenReadAndWriteRegistryValueUsingRegistryKeyClass_ResultIsCodeMazeRegistryDemoValue() + public void WhenReadAndWriteRegistryValueUsingRegistryKeyClass_ResultIsCodeMazeRegistryDemoValue_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var writtenValue = RegistryDemo.ReadAndWriteRegistryValueUsingRegistryKeyClass(); Assert.AreEqual(writtenValue, RegistryDemo.CodeMazeRegistryDemoValue); } [TestMethod] - public void WhenGetSubKeyNames_ResultAreTwoSpecificNames() + public void WhenGetSubKeyNames_ResultAreTwoSpecificNames_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var subKeyNames = RegistryDemo.GetSubKeyNames(); Assert.IsTrue(subKeyNames.Length == 2); @@ -84,13 +57,8 @@ public void WhenGetSubKeyNames_ResultAreTwoSpecificNames() } [TestMethod] - public void WhenGetValueNames_ResultAreTwoSpecificValues() + public void WhenGetValueNames_ResultAreTwoSpecificValues_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var valueNames = RegistryDemo.GetValueNames(); Assert.IsTrue(valueNames.Length == 2); @@ -99,42 +67,27 @@ public void WhenGetValueNames_ResultAreTwoSpecificValues() } [TestMethod] - public void WhenGetValueKind_ResultIsString() + public void WhenGetValueKind_ResultIsString_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var valueKind = RegistryDemo.GetValueKind(); Assert.IsTrue(valueKind.Equals("String")); } [TestMethod] - public void WhenSetRegistryKeyAccessPermissions_ResultIsTrue() + public void WhenSetRegistryKeyAccessPermissions_ResultIsTrue_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var setPermission = RegistryDemo.SetRegistryKeyAccessPermissions(); Assert.IsTrue(setPermission); } [TestMethod] - public void WhenOpenRemoteBaseKey_ResultIsFalse() + public void WhenOpenRemoteBaseKey_ResultIsFalse_Live() { - if (!OperatingSystem.IsWindows()) - { - return; - } - var openRemote = RegistryDemo.OpenRemoteBaseKey("machineName"); Assert.IsFalse(openRemote); } } -} \ No newline at end of file +} diff --git a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/Tests.csproj b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/Tests.csproj index 45f2533ac4..b6b1b17c18 100644 --- a/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/Tests.csproj +++ b/dotnet-platform-specific/ReadWriteWindowsRegistryInCSharp/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 enable enable @@ -10,10 +10,10 @@ - - - - + + + + From a6f9b6030d107f5e79983dd22d7260128d41c340 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sat, 12 Sep 2026 12:07:52 +0200 Subject: [PATCH 2/2] Restore pr-build.yml to main: the Live-test filter already merged in #2162 --- .github/workflows/pr-build.yml | 61 ++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 63f2ce1360..e046ff6f5b 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -1,8 +1,11 @@ # Advisory only. This check is never marked as a required status check, and main # has no branch protection rule referencing it. It builds and tests exactly the -# article folder(s) a PR touches -- nothing else. Folders in .github/ci-skip-folders.txt -# are filtered out before the matrix is built, so they never show up as a run, -# passing or failing (edit that file, not this one, to change what's excluded). +# article folder(s) a PR touches -- nothing else. A folder is classified once: +# "dotnet" when it holds a .sln, "npm" when it holds a package.json at its root +# or in an immediate subfolder (the layout an Angular series folder uses). +# Folders in .github/ci-skip-folders.txt are filtered out before the matrix is +# built, so they never show up as a run, passing or failing (edit that file, not +# this one, to change what's excluded). name: PR Build on: @@ -69,13 +72,23 @@ jobs: continue fi - # Folder may have been deleted in this PR -- skip if no .sln remains at HEAD. - if ! compgen -G "$f"/*.sln > /dev/null 2>&1; then + # Classify the folder. A .sln makes it a .NET folder; otherwise a + # package.json at the folder root or one level down makes it an npm + # folder. Anything else has nothing to build. + kind="" + if compgen -G "$f"/*.sln > /dev/null 2>&1; then + kind="dotnet" + elif [ -f "$f/package.json" ] || compgen -G "$f"/*/package.json > /dev/null 2>&1; then + kind="npm" + fi + + # Folder may have been deleted in this PR -- skip if nothing buildable remains at HEAD. + if [ -z "$kind" ]; then echo "::notice::Skipping '$f' -- no .sln at HEAD (deleted or restructured)." continue fi - entries=$(jq -c --arg folder "$f" '. + [{folder: $folder}]' <<<"$entries") + entries=$(jq -c --arg folder "$f" --arg kind "$kind" '. + [{folder: $folder, kind: $kind}]' <<<"$entries") done echo "folders=$entries" >> "$GITHUB_OUTPUT" @@ -100,6 +113,7 @@ jobs: sparse-checkout-cone-mode: true - name: Setup .NET SDKs + if: matrix.kind == 'dotnet' uses: actions/setup-dotnet@v4 with: dotnet-version: | @@ -110,6 +124,7 @@ jobs: 10.0.x - name: Build and test + if: matrix.kind == 'dotnet' working-directory: ${{ matrix.folder }} shell: bash run: | @@ -124,6 +139,40 @@ jobs: echo "Testing $sln (no-op for folders with no test project)" dotnet test "$sln" --configuration Release --no-build --filter "FullyQualifiedName!~Live" + - name: Setup Node + if: matrix.kind == 'npm' + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + cache-dependency-path: ${{ matrix.folder }}/**/package-lock.json + + - name: Build (npm) + if: matrix.kind == 'npm' + working-directory: ${{ matrix.folder }} + shell: bash + run: | + set -euo pipefail + # Every directory at depth 0 or 1 that holds a package.json is its own + # project: one folder may carry a single application or a set of them. + mapfile -t projects < <( + find . -maxdepth 2 -name package.json -not -path "*/node_modules/*" -printf '%h\n' \ + | sort -u + ) + if [ ${#projects[@]} -eq 0 ]; then + echo "::error::No package.json found in ${{ matrix.folder }}." + exit 1 + fi + for p in "${projects[@]}"; do + echo "::group::${{ matrix.folder }}/${p#./} -- npm ci && npm run build" + ( + cd "$p" + npm ci + npm run build + ) + echo "::endgroup::" + done + # Stable, folder-independent name for branch protection to require -- the # build-and-test job's displayed name varies with the matrix (one leg per # changed folder), so it can't be set as a required check directly. This