Skip to content

Consume ARIEC reconciliation status in FAT live binding #1066

Consume ARIEC reconciliation status in FAT live binding

Consume ARIEC reconciliation status in FAT live binding #1066

Workflow file for this run

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