Consume ARIEC reconciliation status in FAT live binding #1066
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build ARSAS | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| workflow_dispatch: | |
| jobs: | |
| build-windows: | |
| name: Build, test, validate and package Windows application | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout ARSAS application | |
| shell: powershell | |
| run: | | |
| $ref = if ($env:GITHUB_HEAD_REF) { $env:GITHUB_HEAD_REF } else { $env:GITHUB_REF_NAME } | |
| git clone --quiet --depth 1 --branch $ref "https://github.com/$env:GITHUB_REPOSITORY.git" ArIED61850Tester | |
| - name: Resolve immutable ARIEC61850 integration lock | |
| shell: powershell | |
| run: | | |
| $lockPath = ".\ArIED61850Tester\engines\ARIEC61850.lock.json" | |
| if (!(Test-Path $lockPath)) { | |
| throw "ARIEC61850 lock file was not found." | |
| } | |
| $lock = Get-Content $lockPath -Raw | ConvertFrom-Json | |
| if ($lock.repository -notmatch '^[^/]+/[^/]+$' -or | |
| $lock.ref -ne 'main' -or | |
| $lock.commit -notmatch '^[0-9a-f]{40}$') { | |
| throw "ARIEC61850 lock metadata is invalid." | |
| } | |
| "ARIEC61850_REPOSITORY=$($lock.repository)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| "ARIEC61850_REF=$($lock.ref)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| "ARIEC61850_COMMIT=$($lock.commit)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| - name: Verify source, website and license boundaries | |
| shell: powershell | |
| run: .\ArIED61850Tester\scripts\verify-source-clean.ps1 | |
| - name: Verify premium UX, GOOSE, SMV, SAS and release invariants | |
| shell: powershell | |
| run: | | |
| $app = Get-Content .\ArIED61850Tester\App.xaml -Raw | |
| $main = Get-Content .\ArIED61850Tester\MainWindow.xaml -Raw | |
| $code = Get-Content .\ArIED61850Tester\MainWindow.xaml.cs -Raw | |
| $save = Get-Content .\ArIED61850Tester\SaveSclWindow.xaml -Raw | |
| $saveCode = Get-Content .\ArIED61850Tester\SaveSclWindow.xaml.cs -Raw | |
| $project = Get-Content .\ArIED61850Tester\ArIED61850Tester.csproj -Raw | |
| $versionProps = Get-Content .\ArIED61850Tester\Directory.Build.props -Raw | |
| $versionFile = (Get-Content .\ArIED61850Tester\VERSION -Raw).Trim() | |
| $publish = Get-Content .\ArIED61850Tester\scripts\publish-windows-portable.ps1 -Raw | |
| $manifest = Get-Content .\ArIED61850Tester\app.manifest -Raw | |
| $sasPolicy = Get-Content .\ArIED61850Tester\Services\SasOperationalSignalPolicy.cs -Raw | |
| $sasUi = Get-Content .\ArIED61850Tester\SasOperationalUiPolicy.cs -Raw | |
| $sclMapper = Get-Content .\ArIED61850Tester\Services\SclWorkspaceSignalMapper.cs -Raw | |
| $gooseUi = Get-Content .\ArIED61850Tester\MainWindow.GooseSubscriber.cs -Raw | |
| $gooseRuntime = Get-Content .\ArIED61850Tester\Services\GooseSubscriberRuntime.cs -Raw | |
| $gooseModels = Get-Content .\ArIED61850Tester\Models\GooseSubscriberModels.cs -Raw | |
| $smvUi = Get-Content .\ArIED61850Tester\SmvViewerWindow.xaml -Raw | |
| $smvCode = Get-Content .\ArIED61850Tester\SmvViewerWindow.xaml.cs -Raw | |
| $smvHardening = Get-Content .\ArIED61850Tester\SmvViewerWindow.P0Hardening.cs -Raw | |
| $smvRuntime = Get-Content .\ArIED61850Tester\Services\SmvSnapshotCaptureService.cs -Raw | |
| $smvSafety = Get-Content .\ArIED61850Tester\Services\SmvSnapshotSafetyAssessment.cs -Raw | |
| $smvSelection = Get-Content .\ArIED61850Tester\Services\SmvSnapshotSelectionIdentity.cs -Raw | |
| $tests = Get-Content .\ArIED61850Tester\tests\ARSAS.Tests\SmvSnapshotSafetyTests.cs -Raw | |
| if ($save -notmatch 'x:Name="EditionIndicator"' -or | |
| $save -notmatch 'Content="Edition 2"' -or | |
| $save -notmatch 'Content="Edition 1"' -or | |
| $save -match '<ComboBox' -or | |
| $save -match 'Schema V3\.1' -or | |
| $save -match 'Schema V1\.6') { | |
| throw "Save SCL segmented Edition 2 / Edition 1 selector regressed." | |
| } | |
| $itemStyleMatch = [regex]::Match( | |
| $main, | |
| '<ListBox\.ItemContainerStyle>(?<body>.*?)</ListBox\.ItemContainerStyle>', | |
| [System.Text.RegularExpressions.RegexOptions]::Singleline) | |
| if (-not $itemStyleMatch.Success) { | |
| throw "IED card item-container style was not found." | |
| } | |
| $itemStyle = $itemStyleMatch.Groups['body'].Value | |
| if ($itemStyle -match 'ScaleTransform' -or $itemStyle -match 'RenderTransform') { | |
| throw "IED card interaction must not resize or transform the card." | |
| } | |
| if ($main -notmatch 'Content="\{Binding ControlPendingConfirmationLabel\}"' -or | |
| $main -match 'Content="Confirm Open"' -or | |
| $main -match 'Content="Confirm Close"' -or | |
| $main -notmatch 'ScrollViewer.HorizontalScrollBarVisibility="Disabled"' -or | |
| $main -notmatch 'CanUserResizeColumns="False"') { | |
| throw "Atomic Confirm or Command Panel overflow protection regressed." | |
| } | |
| if ($main -notmatch 'WorkflowNavShell" Width="760" Height="56"' -or | |
| $main -notmatch 'x:Name="NavGooseButton"' -or | |
| $main -notmatch '<TabItem Header="GOOSE Subscriber">' -or | |
| $main -notmatch 'ItemsSource="\{Binding SelectedGooseStream.Leaves\}"' -or | |
| $main -notmatch 'Header="confRev"' -or | |
| $main -notmatch 'Header="Flags"' -or | |
| $main -notmatch 'Header="Diagnostics" Binding="\{Binding DiagnosticsSummary\}"' -or | |
| $main -notmatch '<ColumnDefinition Width="56"/>' -or | |
| $main -notmatch '<Viewbox Width="50" Height="50" Stretch="Uniform"' -or | |
| $main -notmatch 'x:Name="MonitorStateBadge"' -or | |
| $main -match '<Control\.Effect><DropShadowEffect BlurRadius="5" ShadowDepth="1" Opacity="0\.22" Color="#26313B"/></Control\.Effect>' -or | |
| $main -notmatch 'LucideSquare.*Stroke="\{StaticResource Warning\}"' -or | |
| $main -notmatch 'LucideX.*Stroke="\{StaticResource Danger\}"' -or | |
| $app -notmatch 'x:Key="CommandOpenGradient"' -or | |
| $app -notmatch 'x:Key="CommandCloseGradient"' -or | |
| $app -notmatch 'x:Key="CommandActionButtonBase"' -or | |
| $app -notmatch '<Setter Property="Foreground" Value="White"/>' -or | |
| $app -notmatch 'x:Key="IedIconButton"[\s\S]*?<Setter Property="Background" Value="Transparent"/>' -or | |
| $app -notmatch 'x:Key="IedIconButton"[\s\S]*?<Setter Property="BorderBrush" Value="Transparent"/>') { | |
| throw "Direct navbar, IED-card action, relay-icon, command-action, or GOOSE-tab visual invariants regressed." | |
| } | |
| if ($gooseRuntime -notmatch 'NpcapProcessBusFrameSource' -or | |
| $gooseRuntime -notmatch 'ProcessBusStreamMonitor' -or | |
| $gooseRuntime -notmatch 'GooseFrameParser.TryParseEthernetFrame' -or | |
| $gooseRuntime -notmatch 'ether proto 0x88b8' -or | |
| $gooseUi -notmatch 'BuildGooseBindingCatalog' -or | |
| $gooseUi -notmatch 'SclWorkspace' -or | |
| $gooseUi -notmatch 'LiveDiscoveryModel' -or | |
| $gooseUi -notmatch 'Select\(\(entry, position\)' -or | |
| $gooseUi -notmatch 'Select\(\(member, position\)' -or | |
| $gooseUi -notmatch 'FirstNonEmpty' -or | |
| $gooseUi -notmatch 'GOOSE APPID differs from model' -or | |
| $gooseUi -notmatch 'GOOSE confRev differs from model' -or | |
| $gooseUi -match 'signal\?\.Category' -or | |
| $gooseModels -notmatch 'class GooseLeafValueRow' -or | |
| $gooseModels -notmatch 'public int Order' -or | |
| $gooseModels -notmatch 'FlagsText' -or | |
| $project -notmatch 'ArIec61850NpcapProject' -or | |
| $project -notmatch 'AR.Iec61850.Transports.Npcap') { | |
| throw "SCL/discovery-aware GOOSE Subscriber runtime, canonical leaf order, or semantic metadata binding regressed." | |
| } | |
| if ($smvUi -notmatch 'Content="Capture 2 cycles"' -or | |
| $smvUi -notmatch 'x:Name="StreamGrid"' -or | |
| $smvUi -notmatch 'ElementName=CaptureButton' -or | |
| $smvUi -notmatch 'P0StreamGrid_SelectionChanged' -or | |
| $smvUi -notmatch 'SnapshotContinuityEvidenceText' -or | |
| $smvUi -notmatch 'x:Name="WaveformCanvas"' -or | |
| $smvCode -notmatch 'SmvSnapshotCaptureService' -or | |
| $smvCode -notmatch 'smpCnt' -or | |
| $smvCode -notmatch 'Raw lanes remain semantically unresolved' -or | |
| $smvHardening -notmatch 'SmvSnapshotSafetyAssessment.IsCleanProof' -or | |
| $smvHardening -notmatch 'SmvSnapshotSelectionIdentity' -or | |
| $smvHardening -notmatch 'publisher restart' -or | |
| $smvSafety -notmatch 'RestartTransitions > 0' -or | |
| $smvSelection -notmatch 'public sealed record SmvSnapshotSelectionIdentity' -or | |
| $tests -notmatch 'AnyCounterDiscontinuity_BlocksCleanProof' -or | |
| $tests -notmatch 'SelectionIdentity_RejectsDifferentStream' -or | |
| $smvRuntime -notmatch 'SampledValuesFrameParser.TryParseEthernetFrame' -or | |
| $smvRuntime -notmatch 'SvGenericAsduInspector' -or | |
| $smvRuntime -notmatch 'SvSampleCounterTracker' -or | |
| $smvRuntime -notmatch 'SvTimebaseResolver' -or | |
| $smvRuntime -notmatch 'ether proto 0x88ba') { | |
| throw "Bounded two-cycle SMV P0 safety, selection lock, raw waveform, timebase, or continuity evidence regressed." | |
| } | |
| if ($sasPolicy -notmatch 'public static bool IsVisible' -or | |
| $sasPolicy -notmatch 'IsOperationalProtectionLeaf' -or | |
| $sasPolicy -notmatch 'IsFundamentalMeasurement' -or | |
| $sasPolicy -notmatch 'IsUnprovenSyntheticCandidate' -or | |
| $sasPolicy -notmatch 'dataObject is "mod" or "beh"') { | |
| throw "Typed SAS operational signal policy is incomplete." | |
| } | |
| if ($sasUi -notmatch 'ModuleInitializer' -or | |
| $sasUi -notmatch 'ApplyBallisticNavigation' -or | |
| $sasUi -notmatch 'legacyPill.Visibility = Visibility.Collapsed' -or | |
| $sasUi -notmatch 'shell.Width = 760' -or | |
| $sasUi -notmatch 'shell.Height = 56' -or | |
| $sasUi -notmatch 'NavGooseButton' -or | |
| $sasUi -notmatch '#D8E2F0' -or | |
| $sasUi -notmatch 'SasOperationalSignalPolicy.IsVisible' -or | |
| $sclMapper -notmatch 'SasOperationalSignalPolicy.IsVisible') { | |
| throw "Operator signal pruning or ballistic navigation policy is not installed." | |
| } | |
| [xml]$projectXml = $project | |
| [xml]$versionXml = $versionProps | |
| $releaseVersion = [string]$versionXml.Project.PropertyGroup.Version | |
| $assemblyVersion = [string]$versionXml.Project.PropertyGroup.AssemblyVersion | |
| $fileVersion = [string]$versionXml.Project.PropertyGroup.FileVersion | |
| $projectVersion = [string]$projectXml.Project.PropertyGroup.Version | |
| if ($releaseVersion -notmatch '^\d+\.\d+\.\d+$' -or | |
| $assemblyVersion -ne "$releaseVersion.0" -or | |
| $fileVersion -ne "$releaseVersion.0" -or | |
| $projectVersion -ne $releaseVersion -or | |
| $versionFile -ne $releaseVersion -or | |
| $publish -notmatch 'Directory.Build.props' -or | |
| $publish -notmatch 'PublishSingleFile' -or | |
| $publish -notmatch 'IncludeNativeLibrariesForSelfExtract=true' -or | |
| $publish -notmatch 'IncludeAllContentForSelfExtract=true' -or | |
| $publish -notmatch 'PublishTrimmed=false' -or | |
| $publish -notmatch 'portable.exe' -or | |
| $manifest -notmatch 'requestedExecutionLevel level="asInvoker"') { | |
| throw "Release metadata is not aligned to canonical version $releaseVersion." | |
| } | |
| "ARSAS_VERSION=$releaseVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| - name: Upload source snapshot | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ARSAS-source-snapshot | |
| path: | | |
| ArIED61850Tester/App.xaml | |
| ArIED61850Tester/MainWindow.xaml | |
| ArIED61850Tester/MainWindow.xaml.cs | |
| ArIED61850Tester/MainWindow.CommandPanelUx.cs | |
| ArIED61850Tester/MainWindow.ControlDiagnostics.cs | |
| ArIED61850Tester/MainWindow.GooseSubscriber.cs | |
| ArIED61850Tester/SmvViewerWindow.xaml | |
| ArIED61850Tester/SmvViewerWindow.xaml.cs | |
| ArIED61850Tester/SmvViewerWindow.P0Hardening.cs | |
| ArIED61850Tester/SaveSclWindow.xaml | |
| ArIED61850Tester/SaveSclWindow.xaml.cs | |
| ArIED61850Tester/GridUxBehavior.cs | |
| ArIED61850Tester/Directory.Build.props | |
| ArIED61850Tester/VERSION | |
| ArIED61850Tester/engines | |
| ArIED61850Tester/Models | |
| ArIED61850Tester/Services | |
| ArIED61850Tester/tests | |
| ArIED61850Tester/docs | |
| ArIED61850Tester/landing | |
| - name: Checkout immutable ARIEC61850 engine revision | |
| shell: powershell | |
| run: | | |
| git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ARIEC61850_REPOSITORY.git" ARIEC61850 | |
| git -C .\ARIEC61850 fetch --quiet --depth 1 origin $env:ARIEC61850_COMMIT | |
| git -C .\ARIEC61850 checkout --quiet --detach $env:ARIEC61850_COMMIT | |
| $actual = (git -C .\ARIEC61850 rev-parse HEAD).Trim() | |
| if ($actual -ne $env:ARIEC61850_COMMIT) { | |
| throw "ARIEC61850 pin mismatch. Expected $env:ARIEC61850_COMMIT, got $actual." | |
| } | |
| - name: Verify required ARIEC61850 APIs | |
| shell: powershell | |
| run: | | |
| $control = ".\ARIEC61850\src\AR.Iec61850\Control\Iec61850ControlService.cs" | |
| $models = ".\ARIEC61850\src\AR.Iec61850\Control\Iec61850ControlModels.cs" | |
| $workspace = ".\ARIEC61850\src\AR.Iec61850\Scl\Workspace\SclWorkspaceService.cs" | |
| $workspaceModels = ".\ARIEC61850\src\AR.Iec61850\Scl\Workspace\SclWorkspaceModels.cs" | |
| $sclModels = ".\ARIEC61850\src\AR.Iec61850\Scl\SclModels.cs" | |
| $sclExporter = ".\ARIEC61850\src\AR.Iec61850\Scl\Export\LiveIedSclExporter.cs" | |
| $schemaProfiles = ".\ARIEC61850\src\AR.Iec61850\Scl\Export\SclSchemaProfiles.cs" | |
| $interoperableConverter = ".\ARIEC61850\src\AR.Iec61850\Scl\Export\InteroperableSclConverter.cs" | |
| $gooseParser = ".\ARIEC61850\src\AR.Iec61850\Goose\GooseFrameParser.cs" | |
| $gooseDecoded = ".\ARIEC61850\src\AR.Iec61850\Goose\GooseDecodedValue.cs" | |
| $processBusMonitor = ".\ARIEC61850\src\AR.Iec61850\Monitoring\ProcessBusStreamMonitor.cs" | |
| $svParser = ".\ARIEC61850\src\AR.Iec61850\SampledValues\SampledValuesFrameParser.cs" | |
| $svInspector = ".\ARIEC61850\src\AR.Iec61850\SampledValues\Analysis\SvGenericAsduInspector.cs" | |
| $svCounter = ".\ARIEC61850\src\AR.Iec61850\SampledValues\Measurements\SvSampleCounterTracker.cs" | |
| $svTimebase = ".\ARIEC61850\src\AR.Iec61850\SampledValues\Measurements\SvTimebaseResolver.cs" | |
| $npcapSource = ".\ARIEC61850\src\AR.Iec61850.Transports.Npcap\NpcapProcessBusFrameSource.cs" | |
| $npcapProject = ".\ARIEC61850\src\AR.Iec61850.Transports.Npcap\AR.Iec61850.Transports.Npcap.csproj" | |
| if (!(Test-Path $control) -or !(Test-Path $models) -or | |
| !(Select-String -Path $models -Pattern "interface IIec61850ControlService" -Quiet) -or | |
| !(Select-String -Path $models -Pattern "CommandTerminationReceived" -Quiet)) { | |
| throw "Required ARIEC61850 Smart Control contract was not found." | |
| } | |
| if (!(Test-Path $workspace) -or !(Test-Path $workspaceModels) -or | |
| !(Select-String -Path $workspace -Pattern "CompareLive" -Quiet)) { | |
| throw "Required ARIEC61850 SCL Workspace API was not found." | |
| } | |
| if (!(Test-Path $sclModels) -or | |
| !(Select-String -Path $sclModels -Pattern "SclDataSetBindingStatus" -Quiet) -or | |
| !(Select-String -Path $sclModels -Pattern "class SclDataSetEntry" -Quiet) -or | |
| !(Select-String -Path $sclModels -Pattern "public int Index" -Quiet)) { | |
| throw "Typed SCL DataSet binding and ordered-entry contracts were not found." | |
| } | |
| if (!(Test-Path $sclExporter) -or | |
| !(Select-String -Path $sclExporter -Pattern "WriteFiles" -Quiet) -or | |
| !(Test-Path $interoperableConverter) -or | |
| !(Select-String -Path $interoperableConverter -Pattern "WriteFiles" -Quiet) -or | |
| !(Test-Path $schemaProfiles) -or | |
| !(Select-String -Path $schemaProfiles -Pattern "Edition2V31" -Quiet) -or | |
| !(Select-String -Path $schemaProfiles -Pattern "Edition1V16" -Quiet)) { | |
| throw "Required ARIEC61850 interoperable SCL export APIs were not found." | |
| } | |
| if (!(Test-Path $gooseParser) -or | |
| !(Test-Path $gooseDecoded) -or | |
| !(Test-Path $processBusMonitor) -or | |
| !(Test-Path $svParser) -or | |
| !(Test-Path $svInspector) -or | |
| !(Test-Path $svCounter) -or | |
| !(Test-Path $svTimebase) -or | |
| !(Test-Path $npcapSource) -or | |
| !(Test-Path $npcapProject) -or | |
| !(Select-String -Path $gooseParser -Pattern "TryParseEthernetFrame" -Quiet) -or | |
| !(Select-String -Path $gooseDecoded -Pattern "SignalReference" -Quiet) -or | |
| !(Select-String -Path $processBusMonitor -Pattern "GooseValues" -Quiet) -or | |
| !(Select-String -Path $processBusMonitor -Pattern "TimeAllowedToLiveMilliseconds" -Quiet) -or | |
| !(Select-String -Path $svParser -Pattern "TryParseEthernetFrame" -Quiet) -or | |
| !(Select-String -Path $svInspector -Pattern "SvGenericPayloadInspector" -Quiet) -or | |
| !(Select-String -Path $svCounter -Pattern "SvSampleCounterTransitionKind" -Quiet) -or | |
| !(Select-String -Path $svTimebase -Pattern "TrustedNominalFrequencyHz" -Quiet) -or | |
| !(Select-String -Path $npcapSource -Pattern "CaptureAsync" -Quiet)) { | |
| throw "Required ARIEC61850 live process-bus, SV parser/inspection/timebase/counter, or Npcap APIs were not found." | |
| } | |
| - name: Setup .NET 8 | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 8.0.x | |
| - name: Restore solution | |
| run: dotnet restore .\ArIED61850Tester\ArIED61850Tester.sln | |
| - name: Build solution | |
| run: dotnet build .\ArIED61850Tester\ArIED61850Tester.sln -c Release --no-restore | |
| - name: Run ARSAS application regression tests | |
| run: dotnet test .\ArIED61850Tester\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --logger "trx;LogFileName=arsas-tests.trx" --results-directory .\ArIED61850Tester\TestResults | |
| - name: Upload test evidence | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ARSAS-test-evidence | |
| path: ArIED61850Tester\TestResults\*.trx | |
| if-no-files-found: error | |
| - name: Publish real portable single EXE x64 | |
| shell: powershell | |
| run: | | |
| .\ArIED61850Tester\scripts\publish-windows-portable.ps1 ` | |
| -Version $env:ARSAS_VERSION ` | |
| -Runtime win-x64 ` | |
| -SingleFile $true ` | |
| -SelfContained $true ` | |
| -EngineProject "$env:GITHUB_WORKSPACE\ARIEC61850\src\AR.Iec61850\AR.Iec61850.csproj" ` | |
| -NpcapProject "$env:GITHUB_WORKSPACE\ARIEC61850\src\AR.Iec61850.Transports.Npcap\AR.Iec61850.Transports.Npcap.csproj" | |
| - name: Smoke-test real portable single EXE | |
| shell: powershell | |
| run: | | |
| $exe = ".\ArIED61850Tester\dist\ARSAS-$env:ARSAS_VERSION-win-x64-portable.exe" | |
| if (-not (Test-Path $exe -PathType Leaf)) { throw "Portable single EXE was not produced: $exe" } | |
| $env:DOTNET_BUNDLE_EXTRACT_BASE_DIR = Join-Path $env:RUNNER_TEMP "ARSAS-bundle-cache" | |
| $diagnostic = Join-Path ([System.IO.Path]::GetTempPath()) "ARSAS-portable-smoke-error.txt" | |
| Remove-Item $diagnostic -Force -ErrorAction SilentlyContinue | |
| $process = Start-Process -FilePath $exe -ArgumentList @("--portable-smoke-test") -PassThru | |
| if (-not $process.WaitForExit(30000)) { | |
| Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue | |
| throw "Portable single EXE smoke test timed out." | |
| } | |
| if ($process.ExitCode -ne 0) { | |
| if (Test-Path $diagnostic) { Get-Content $diagnostic | Write-Host } | |
| throw "Portable single EXE smoke test failed with exit code $($process.ExitCode)." | |
| } | |
| - name: Upload portable single EXE | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ARSAS-win-x64-portable-single-exe | |
| path: ArIED61850Tester\dist\ARSAS-*-win-x64-portable.exe | |
| if-no-files-found: error |