530 lines
23 KiB
PowerShell
530 lines
23 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Campaign LA11 display-free, connection-free automated preflight.
|
|
|
|
.DESCRIPTION
|
|
Runs the exact Release and portability ladder used before the launcher
|
|
user gate. It never starts App/Headless in connected mode, never opens a
|
|
window, never reads credentials, and never bakes retail DATs. All logs and
|
|
publishes are contained beneath one logs/campaign-la-gate-<timestamp>
|
|
directory. Use -DryRun to emit the complete command matrix without
|
|
executing it.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$Repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path,
|
|
[Parameter(Mandatory = $true)][string]$AllowedOutputRoot,
|
|
[string]$OutputDirectory,
|
|
[switch]$DryRun,
|
|
[switch]$IncludeInstalledDat,
|
|
[string]$InstalledDatDirectory
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
|
throw 'Campaign LA preflight requires PowerShell 7 or newer.'
|
|
}
|
|
|
|
$Repository = [IO.Path]::TrimEndingDirectorySeparator(
|
|
[IO.Path]::GetFullPath($Repository))
|
|
if (-not (Test-Path -LiteralPath (Join-Path $Repository 'AcDream.slnx') -PathType Leaf)) {
|
|
throw "Repository does not contain AcDream.slnx: $Repository"
|
|
}
|
|
|
|
function Assert-NoReparseAncestry([string]$Path, [string]$Description) {
|
|
$cursor = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath($Path))
|
|
while (-not (Test-Path -LiteralPath $cursor)) {
|
|
$parent = [IO.Path]::GetDirectoryName($cursor)
|
|
if ([string]::IsNullOrEmpty($parent) -or $parent -ceq $cursor) { break }
|
|
$cursor = $parent
|
|
}
|
|
while (-not [string]::IsNullOrEmpty($cursor)) {
|
|
$item = Get-Item -LiteralPath $cursor -Force
|
|
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw "$Description has a reparse point in its ancestry."
|
|
}
|
|
$parent = [IO.Directory]::GetParent($cursor)
|
|
if ($null -eq $parent) { break }
|
|
$cursor = $parent.FullName
|
|
}
|
|
}
|
|
|
|
function Test-SameOrDescendant([string]$Path, [string]$Ancestor) {
|
|
$comparison = if ($IsWindows) {
|
|
[StringComparison]::OrdinalIgnoreCase
|
|
} else { [StringComparison]::Ordinal }
|
|
if ([string]::Equals($Path, $Ancestor, $comparison)) { return $true }
|
|
return $Path.StartsWith(
|
|
$Ancestor + [IO.Path]::DirectorySeparatorChar,
|
|
$comparison)
|
|
}
|
|
|
|
if (-not [IO.Path]::IsPathFullyQualified($AllowedOutputRoot)) {
|
|
throw '-AllowedOutputRoot must be absolute.'
|
|
}
|
|
$AllowedOutputRoot = [IO.Path]::TrimEndingDirectorySeparator(
|
|
[IO.Path]::GetFullPath($AllowedOutputRoot))
|
|
if (-not (Test-Path -LiteralPath $AllowedOutputRoot -PathType Container)) {
|
|
throw '-AllowedOutputRoot must be an existing campaign gate/log directory.'
|
|
}
|
|
Assert-NoReparseAncestry $AllowedOutputRoot 'Allowed output root'
|
|
$comparison = if ($IsWindows) {
|
|
[StringComparison]::OrdinalIgnoreCase
|
|
} else { [StringComparison]::Ordinal }
|
|
$homeDirectory = [IO.Path]::TrimEndingDirectorySeparator(
|
|
[IO.Path]::GetFullPath([Environment]::GetFolderPath(
|
|
[Environment+SpecialFolder]::UserProfile)))
|
|
if ([string]::Equals($AllowedOutputRoot, $Repository, $comparison) -or
|
|
[string]::Equals($AllowedOutputRoot, $homeDirectory, $comparison)) {
|
|
throw '-AllowedOutputRoot cannot be the repository root or user home.'
|
|
}
|
|
$repositoryLogs = [IO.Path]::TrimEndingDirectorySeparator(
|
|
[IO.Path]::GetFullPath((Join-Path $Repository 'logs')))
|
|
$allowedLeaf = [IO.Path]::GetFileName($AllowedOutputRoot)
|
|
$allowedInRepository = Test-SameOrDescendant $AllowedOutputRoot $Repository
|
|
if ($allowedInRepository -and
|
|
-not (Test-SameOrDescendant $AllowedOutputRoot $repositoryLogs)) {
|
|
throw '-AllowedOutputRoot inside the repository must be below its logs directory.'
|
|
}
|
|
if (-not [string]::Equals($AllowedOutputRoot, $repositoryLogs, $comparison) -and
|
|
-not $allowedLeaf.StartsWith('campaign-la-', [StringComparison]::Ordinal)) {
|
|
throw '-AllowedOutputRoot must be the repository logs root or a campaign-la-* gate root.'
|
|
}
|
|
if ($IncludeInstalledDat) {
|
|
if ([string]::IsNullOrWhiteSpace($InstalledDatDirectory) -or
|
|
-not [IO.Path]::IsPathFullyQualified($InstalledDatDirectory)) {
|
|
throw '-IncludeInstalledDat requires an absolute -InstalledDatDirectory.'
|
|
}
|
|
$InstalledDatDirectory = [IO.Path]::TrimEndingDirectorySeparator(
|
|
[IO.Path]::GetFullPath($InstalledDatDirectory))
|
|
foreach ($file in @(
|
|
'client_portal.dat',
|
|
'client_cell_1.dat',
|
|
'client_highres.dat',
|
|
'client_local_English.dat')) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $InstalledDatDirectory $file) -PathType Leaf)) {
|
|
throw "Installed DAT directory is missing $file."
|
|
}
|
|
}
|
|
}
|
|
|
|
$stamp = [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss')
|
|
if ([string]::IsNullOrWhiteSpace($OutputDirectory)) {
|
|
$OutputDirectory = Join-Path $AllowedOutputRoot "campaign-la-preflight-$stamp"
|
|
}
|
|
elseif (-not [IO.Path]::IsPathFullyQualified($OutputDirectory)) {
|
|
throw '-OutputDirectory must be absolute when supplied.'
|
|
}
|
|
$OutputDirectory = [IO.Path]::TrimEndingDirectorySeparator(
|
|
[IO.Path]::GetFullPath($OutputDirectory))
|
|
if (-not (Test-SameOrDescendant $OutputDirectory $AllowedOutputRoot) -or
|
|
[string]::Equals($OutputDirectory, $AllowedOutputRoot, $comparison)) {
|
|
throw '-OutputDirectory must be a strict descendant of -AllowedOutputRoot.'
|
|
}
|
|
if ([string]::Equals($OutputDirectory, $Repository, $comparison) -or
|
|
[string]::Equals($OutputDirectory, $homeDirectory, $comparison)) {
|
|
throw '-OutputDirectory cannot be the repository root or user home.'
|
|
}
|
|
if (Test-Path -LiteralPath $OutputDirectory) {
|
|
throw '-OutputDirectory must be fresh and must not already exist.'
|
|
}
|
|
Assert-NoReparseAncestry $OutputDirectory 'Output directory'
|
|
$logsDirectory = Join-Path $OutputDirectory 'commands'
|
|
$publishDirectory = Join-Path $OutputDirectory 'publish'
|
|
$null = New-Item -ItemType Directory -Path $logsDirectory
|
|
|
|
$commandResults = [Collections.Generic.List[object]]::new()
|
|
$failures = [Collections.Generic.List[string]]::new()
|
|
$startedUtc = [DateTime]::UtcNow
|
|
|
|
function Protect-Text([string]$Text) {
|
|
if ($null -eq $Text) { return '' }
|
|
$protected = $Text
|
|
$protected = [Text.RegularExpressions.Regex]::Replace(
|
|
$protected,
|
|
'(?i)(--password|-password)(\s+|=)([^\s"'']+)',
|
|
'$1$2<redacted>')
|
|
$protected = [Text.RegularExpressions.Regex]::Replace(
|
|
$protected,
|
|
'(?i)\b(password|passwd|secret|token|credential|api[_-]?key)(\s*[:=]\s*)([^\s,;]+)',
|
|
'$1$2<redacted>')
|
|
$protected = [Text.RegularExpressions.Regex]::Replace(
|
|
$protected,
|
|
'(?i)(https?://)[^/\s:@]+:[^@\s/]+@',
|
|
'$1<redacted>@')
|
|
$protected = [Text.RegularExpressions.Regex]::Replace(
|
|
$protected,
|
|
'(?i)([?&](?:token|secret|password|credential|api[_-]?key)=)[^&\s]+',
|
|
'$1<redacted>')
|
|
$protected = [Text.RegularExpressions.Regex]::Replace(
|
|
$protected,
|
|
'(?i)("(?:password|credential|secret|token)"\s*:\s*")[^"]*(")',
|
|
'$1<redacted>$2')
|
|
return $protected
|
|
}
|
|
|
|
function Format-Command([string]$FilePath, [string[]]$Arguments) {
|
|
$parts = [Collections.Generic.List[string]]::new()
|
|
$parts.Add($FilePath)
|
|
foreach ($argument in $Arguments) {
|
|
if ($argument -match '[\s"]') {
|
|
$parts.Add('"' + $argument.Replace('"', '\"') + '"')
|
|
}
|
|
else { $parts.Add($argument) }
|
|
}
|
|
return $parts -join ' '
|
|
}
|
|
|
|
function Add-PlannedCommand(
|
|
[string]$Name,
|
|
[string]$FilePath,
|
|
[string[]]$Arguments,
|
|
[Collections.IDictionary]$Environment = @{}) {
|
|
$commandResults.Add([ordered]@{
|
|
name = $Name
|
|
command = Format-Command $FilePath $Arguments
|
|
status = 'planned'
|
|
startedUtc = $null
|
|
durationSeconds = 0
|
|
exitCode = $null
|
|
stdout = $null
|
|
stderr = $null
|
|
environmentKeys = @($Environment.Keys | Sort-Object)
|
|
})
|
|
}
|
|
|
|
function Invoke-GateCommand(
|
|
[string]$Name,
|
|
[string]$FilePath,
|
|
[string[]]$Arguments,
|
|
[Collections.IDictionary]$Environment = @{}) {
|
|
if ($DryRun) {
|
|
Add-PlannedCommand $Name $FilePath $Arguments $Environment
|
|
return
|
|
}
|
|
|
|
$safeName = $Name -replace '[^A-Za-z0-9_.-]', '-'
|
|
$stdoutRelative = "commands/$safeName.out.log"
|
|
$stderrRelative = "commands/$safeName.err.log"
|
|
$stdoutPath = Join-Path $OutputDirectory $stdoutRelative
|
|
$stderrPath = Join-Path $OutputDirectory $stderrRelative
|
|
$begin = [DateTime]::UtcNow
|
|
$watch = [Diagnostics.Stopwatch]::StartNew()
|
|
$exitCode = 74
|
|
try {
|
|
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
|
$startInfo.FileName = $FilePath
|
|
$startInfo.WorkingDirectory = $Repository
|
|
$startInfo.UseShellExecute = $false
|
|
$startInfo.CreateNoWindow = $true
|
|
$startInfo.RedirectStandardOutput = $true
|
|
$startInfo.RedirectStandardError = $true
|
|
foreach ($argument in $Arguments) { $startInfo.ArgumentList.Add($argument) }
|
|
foreach ($key in @($startInfo.Environment.Keys)) {
|
|
if ($key.StartsWith('ACDREAM_', [StringComparison]::OrdinalIgnoreCase)) {
|
|
$startInfo.Environment.Remove($key)
|
|
}
|
|
}
|
|
foreach ($entry in $Environment.GetEnumerator()) {
|
|
$startInfo.Environment[[string]$entry.Key] = [string]$entry.Value
|
|
}
|
|
$process = [Diagnostics.Process]::new()
|
|
$process.StartInfo = $startInfo
|
|
if (-not $process.Start()) { throw "Could not start $FilePath." }
|
|
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
|
$stderrTask = $process.StandardError.ReadToEndAsync()
|
|
$process.WaitForExit()
|
|
$stdout = $stdoutTask.GetAwaiter().GetResult()
|
|
$stderr = $stderrTask.GetAwaiter().GetResult()
|
|
$exitCode = $process.ExitCode
|
|
$process.Dispose()
|
|
[IO.File]::WriteAllText($stdoutPath, (Protect-Text $stdout))
|
|
[IO.File]::WriteAllText($stderrPath, (Protect-Text $stderr))
|
|
}
|
|
catch {
|
|
[IO.File]::WriteAllText($stderrPath, (Protect-Text ($_ | Out-String)))
|
|
}
|
|
finally {
|
|
$watch.Stop()
|
|
$commandResults.Add([ordered]@{
|
|
name = $Name
|
|
command = Format-Command $FilePath $Arguments
|
|
status = if ($exitCode -eq 0) { 'passed' } else { 'failed' }
|
|
startedUtc = $begin.ToString('O')
|
|
durationSeconds = [Math]::Round($watch.Elapsed.TotalSeconds, 3)
|
|
exitCode = $exitCode
|
|
stdout = $stdoutRelative
|
|
stderr = $stderrRelative
|
|
environmentKeys = @($Environment.Keys | Sort-Object)
|
|
})
|
|
}
|
|
if ($exitCode -ne 0) {
|
|
throw "Preflight command '$Name' failed with exit code $exitCode."
|
|
}
|
|
}
|
|
|
|
function Add-InternalCheck([string]$Name, [scriptblock]$Action) {
|
|
if ($DryRun) {
|
|
$commandResults.Add([ordered]@{
|
|
name = $Name; command = '<internal contract check>'; status = 'planned'
|
|
startedUtc = $null; durationSeconds = 0; exitCode = $null
|
|
stdout = $null; stderr = $null; environmentKeys = @()
|
|
})
|
|
return
|
|
}
|
|
$begin = [DateTime]::UtcNow
|
|
$watch = [Diagnostics.Stopwatch]::StartNew()
|
|
$exitCode = 0
|
|
try { & $Action }
|
|
catch { $exitCode = 1; throw }
|
|
finally {
|
|
$watch.Stop()
|
|
$commandResults.Add([ordered]@{
|
|
name = $Name; command = '<internal contract check>'
|
|
status = if ($exitCode -eq 0) { 'passed' } else { 'failed' }
|
|
startedUtc = $begin.ToString('O')
|
|
durationSeconds = [Math]::Round($watch.Elapsed.TotalSeconds, 3)
|
|
exitCode = $exitCode; stdout = $null; stderr = $null; environmentKeys = @()
|
|
})
|
|
}
|
|
}
|
|
|
|
function Invoke-DotNet([string]$Name, [string[]]$Arguments) {
|
|
Invoke-GateCommand $Name 'dotnet' $Arguments
|
|
}
|
|
|
|
$portableBuildProjects = @(
|
|
'src/AcDream.Platform/AcDream.Platform.csproj',
|
|
'src/AcDream.Launcher.Core/AcDream.Launcher.Core.csproj',
|
|
'src/AcDream.Bake/AcDream.Bake.csproj',
|
|
'src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj',
|
|
'src/AcDream.Core/AcDream.Core.csproj',
|
|
'src/AcDream.Core.Net/AcDream.Core.Net.csproj',
|
|
'src/AcDream.Content/AcDream.Content.csproj',
|
|
'src/AcDream.Runtime/AcDream.Runtime.csproj',
|
|
'src/AcDream.Headless/AcDream.Headless.csproj'
|
|
)
|
|
$portableTestProjects = @(
|
|
'tests/AcDream.Platform.Tests/AcDream.Platform.Tests.csproj',
|
|
'tests/AcDream.Launcher.Core.Tests/AcDream.Launcher.Core.Tests.csproj',
|
|
'tests/AcDream.Bake.Tests/AcDream.Bake.Tests.csproj',
|
|
'tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj',
|
|
'tests/AcDream.Content.Tests/AcDream.Content.Tests.csproj',
|
|
'tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj',
|
|
'tests/AcDream.Headless.Tests/AcDream.Headless.Tests.csproj'
|
|
)
|
|
|
|
try {
|
|
Invoke-DotNet 'release-build' @(
|
|
'build', 'AcDream.slnx', '-c', 'Release', '--nologo', '-m:1')
|
|
Invoke-GateCommand 'campaign-la-gate-helper-contracts' `
|
|
([Environment]::ProcessPath ??
|
|
$(throw 'The PowerShell process path is unavailable.')) `
|
|
@(
|
|
'-NoProfile',
|
|
'-File', 'tools/test-campaign-la-gate-helpers.ps1',
|
|
'-Repository', $Repository,
|
|
'-OutputDirectory', (Join-Path $OutputDirectory 'helper-contracts'))
|
|
Invoke-GateCommand 'campaign-la-script-safety-contracts' `
|
|
([Environment]::ProcessPath ??
|
|
$(throw 'The PowerShell process path is unavailable.')) `
|
|
@(
|
|
'-NoProfile',
|
|
'-File', 'tools/test-campaign-la-script-safety.ps1',
|
|
'-Repository', $Repository,
|
|
'-OutputDirectory', (Join-Path $OutputDirectory 'script-safety'))
|
|
Invoke-DotNet 'release-tests-serial' @(
|
|
'test', 'AcDream.slnx', '-c', 'Release', '--no-build', '--nologo', '-m:1',
|
|
'--', 'RunConfiguration.MaxCpuCount=1')
|
|
Invoke-DotNet 'focused-launcher-updater-core' @(
|
|
'test', 'tests/AcDream.Launcher.Core.Tests/AcDream.Launcher.Core.Tests.csproj',
|
|
'-c', 'Release', '--no-build', '--nologo',
|
|
'--filter', 'FullyQualifiedName~Updates')
|
|
Invoke-DotNet 'focused-launcher-updater-ui' @(
|
|
'test', 'tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj',
|
|
'-c', 'Release', '--no-build', '--nologo',
|
|
'--filter', 'FullyQualifiedName~LauncherUpdateViewModelTests|FullyQualifiedName~LauncherStartupOptionsTests')
|
|
|
|
foreach ($project in $portableBuildProjects) {
|
|
$leaf = [IO.Path]::GetFileNameWithoutExtension($project)
|
|
Invoke-DotNet "portable-build-$leaf" @(
|
|
'build', $project, '-c', 'Release', '--no-restore', '--nologo', '-m:1')
|
|
}
|
|
foreach ($project in $portableTestProjects) {
|
|
$leaf = [IO.Path]::GetFileNameWithoutExtension($project)
|
|
Invoke-DotNet "portable-test-$leaf" @(
|
|
'test', $project, '-c', 'Release', '--no-build', '--nologo',
|
|
'--', 'RunConfiguration.MaxCpuCount=1')
|
|
}
|
|
|
|
$headlessValidationConfig = Join-Path $OutputDirectory 'headless-k0.json'
|
|
Add-InternalCheck 'portable-headless-write-empty-config' {
|
|
[IO.File]::WriteAllText(
|
|
$headlessValidationConfig,
|
|
'{"version":1,"sessions":[]}',
|
|
[Text.UTF8Encoding]::new($false))
|
|
}
|
|
Invoke-DotNet 'portable-headless-help-no-connect' @(
|
|
'run', '--project', 'src/AcDream.Headless/AcDream.Headless.csproj',
|
|
'-c', 'Release', '--no-build', '--', '--help')
|
|
Invoke-DotNet 'portable-headless-validate-empty-no-connect' @(
|
|
'run', '--project', 'src/AcDream.Headless/AcDream.Headless.csproj',
|
|
'-c', 'Release', '--no-build', '--',
|
|
'validate', '--config', $headlessValidationConfig)
|
|
Add-InternalCheck 'portable-headless-native-permission' {
|
|
if (-not $IsWindows) {
|
|
$headlessExecutable = Join-Path `
|
|
$Repository 'src/AcDream.Headless/bin/Release/net10.0/acdream-headless'
|
|
if (-not (Test-Path -LiteralPath $headlessExecutable -PathType Leaf)) {
|
|
throw 'The native Headless build output is missing.'
|
|
}
|
|
$mode = [IO.File]::GetUnixFileMode($headlessExecutable)
|
|
if (($mode -band [IO.UnixFileMode]::UserExecute) -eq 0) {
|
|
throw 'The native Headless build output is not executable.'
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($rid in @('win-x64', 'linux-x64')) {
|
|
$destination = Join-Path $publishDirectory $rid
|
|
Invoke-DotNet "publish-launcher-$rid" @(
|
|
'publish', 'src/AcDream.Launcher/AcDream.Launcher.csproj',
|
|
'-c', 'Release', '-r', $rid, '--self-contained', 'true',
|
|
'-p:PublishSingleFile=true', '-o', $destination, '--nologo')
|
|
Add-InternalCheck "publish-contract-$rid" {
|
|
$suffix = if ($rid.StartsWith('win-', [StringComparison]::Ordinal)) { '.exe' } else { '' }
|
|
foreach ($name in @("acdream-launcher$suffix", "acdream-bake$suffix")) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $destination $name) -PathType Leaf)) {
|
|
throw "$rid publish is missing $name."
|
|
}
|
|
}
|
|
if (Test-Path -LiteralPath (Join-Path $destination 'acdream-launcher.dll')) {
|
|
throw "$rid launcher publish is not single-file."
|
|
}
|
|
if (Test-Path -LiteralPath (Join-Path $destination 'acdream-bake.dll')) {
|
|
throw "$rid bake publish is not single-file."
|
|
}
|
|
if (-not $IsWindows -and $rid -eq 'linux-x64') {
|
|
$mode = [IO.File]::GetUnixFileMode((Join-Path $destination 'acdream-launcher'))
|
|
if (($mode -band [IO.UnixFileMode]::UserExecute) -eq 0) {
|
|
throw 'linux-x64 launcher is not executable.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$nativeRid = if ($IsWindows) { 'win-x64' } else { 'linux-x64' }
|
|
$nativeSuffix = if ($IsWindows) { '.exe' } else { '' }
|
|
$nativeRoot = Join-Path $publishDirectory $nativeRid
|
|
$bogusRoot = if ($IsWindows) { 'Z:\definitely-not-installed' } else { '/definitely-not-installed' }
|
|
$bogusEnvironment = @{
|
|
DOTNET_ROOT = $bogusRoot
|
|
DOTNET_ROOT_X64 = $bogusRoot
|
|
DOTNET_MULTILEVEL_LOOKUP = '0'
|
|
}
|
|
Invoke-GateCommand 'native-launcher-bogus-dotnet-root' `
|
|
(Join-Path $nativeRoot "acdream-launcher$nativeSuffix") `
|
|
@('--verify-publish') $bogusEnvironment
|
|
Invoke-GateCommand 'native-bake-bogus-dotnet-root' `
|
|
(Join-Path $nativeRoot "acdream-bake$nativeSuffix") `
|
|
@('--help') $bogusEnvironment
|
|
|
|
if ($IncludeInstalledDat) {
|
|
$datEnvironment = @{
|
|
ACDREAM_DAT_DIR = $InstalledDatDirectory
|
|
ACDREAM_PROBE_LIVE_MOUNT = '1'
|
|
}
|
|
$datResults = Join-Path $OutputDirectory 'installed-dat-results'
|
|
Invoke-GateCommand 'installed-dat-character-management-readonly' 'dotnet' @(
|
|
'test', 'tests/AcDream.App.Tests/AcDream.App.Tests.csproj',
|
|
'-c', 'Release', '--no-build', '--nologo',
|
|
'--filter', 'FullyQualifiedName~CharacterManagementLiveDatTests',
|
|
'--results-directory', $datResults,
|
|
'--logger', 'trx;LogFileName=character-management.trx') $datEnvironment
|
|
Add-InternalCheck 'installed-dat-character-management-require-pass' {
|
|
$trx = Join-Path $datResults 'character-management.trx'
|
|
if (-not (Test-Path -LiteralPath $trx -PathType Leaf)) {
|
|
throw 'CharacterManagementLiveDatTests did not produce a TRX result.'
|
|
}
|
|
[xml]$result = Get-Content -LiteralPath $trx -Raw
|
|
$outcomes = @($result.TestRun.Results.UnitTestResult | ForEach-Object { $_.outcome })
|
|
if ($outcomes.Count -eq 0 -or $outcomes -ccontains 'NotExecuted' -or
|
|
@($outcomes | Where-Object { $_ -cne 'Passed' }).Count -gt 0) {
|
|
throw "CharacterManagementLiveDatTests must pass (not skip): $($outcomes -join ',')."
|
|
}
|
|
}
|
|
Invoke-GateCommand 'installed-dat-action-map-readonly' 'dotnet' @(
|
|
'test', 'tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj',
|
|
'-c', 'Release', '--no-build', '--nologo',
|
|
'--filter', 'FullyQualifiedName~RetailActionMapReader_LiveDatTests') $datEnvironment
|
|
Invoke-GateCommand 'installed-dat-portal-assets-readonly' 'dotnet' @(
|
|
'test', 'tests/AcDream.App.Tests/AcDream.App.Tests.csproj',
|
|
'-c', 'Release', '--no-build', '--nologo',
|
|
'--filter', 'FullyQualifiedName~PortalTunnelAssetTests.InstalledDat_ResolvesRetailPortalSetupAndAnimation') $datEnvironment
|
|
}
|
|
}
|
|
catch {
|
|
$failures.Add((Protect-Text ($_ | Out-String)).Trim())
|
|
}
|
|
finally {
|
|
$finishedUtc = [DateTime]::UtcNow
|
|
$head = (& git -C $Repository rev-parse HEAD).Trim()
|
|
$dirtyLines = @(& git -C $Repository status --porcelain=v1 --untracked-files=all)
|
|
$artifacts = @()
|
|
if (-not $DryRun) {
|
|
[string[]]$artifactPaths = @(Get-ChildItem -LiteralPath $OutputDirectory -File -Recurse |
|
|
Where-Object { $_.FullName -ne (Join-Path $OutputDirectory 'report.json') } |
|
|
ForEach-Object {
|
|
[IO.Path]::GetRelativePath($OutputDirectory, $_.FullName).Replace('\', '/')
|
|
})
|
|
[Array]::Sort($artifactPaths, [StringComparer]::Ordinal)
|
|
$artifacts = @($artifactPaths | ForEach-Object {
|
|
$fullPath = Join-Path $OutputDirectory $_.Replace(
|
|
'/', [IO.Path]::DirectorySeparatorChar)
|
|
$item = Get-Item -LiteralPath $fullPath
|
|
[ordered]@{
|
|
path = $_
|
|
size = $item.Length
|
|
sha256 = (Get-FileHash -LiteralPath $fullPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
}
|
|
})
|
|
}
|
|
$failedCommands = @($commandResults | Where-Object { $_.status -eq 'failed' })
|
|
$report = [ordered]@{
|
|
schemaVersion = 1
|
|
kind = 'campaign-la-automated-preflight'
|
|
dryRun = [bool]$DryRun
|
|
success = ($failures.Count -eq 0 -and $failedCommands.Count -eq 0)
|
|
repository = $Repository
|
|
allowedOutputRoot = $AllowedOutputRoot
|
|
head = $head
|
|
dirty = ($dirtyLines.Count -gt 0)
|
|
dirtyPaths = @($dirtyLines | ForEach-Object { Protect-Text $_ })
|
|
platform = [ordered]@{
|
|
os = [Runtime.InteropServices.RuntimeInformation]::OSDescription
|
|
architecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
|
|
processArchitecture = [Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString()
|
|
rid = [Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier
|
|
framework = [Runtime.InteropServices.RuntimeInformation]::FrameworkDescription
|
|
powershell = $PSVersionTable.PSVersion.ToString()
|
|
}
|
|
startedUtc = $startedUtc.ToString('O')
|
|
finishedUtc = $finishedUtc.ToString('O')
|
|
durationSeconds = [Math]::Round(($finishedUtc - $startedUtc).TotalSeconds, 3)
|
|
installedDatIncluded = [bool]$IncludeInstalledDat
|
|
commands = @($commandResults)
|
|
failures = @($failures)
|
|
redaction = [ordered]@{
|
|
applied = $true
|
|
inheritedAcdreamEnvironmentCleared = $true
|
|
inheritedEnvironmentValuesRead = $false
|
|
credentialArgumentsAllowed = $false
|
|
}
|
|
artifacts = $artifacts
|
|
}
|
|
$reportPath = Join-Path $OutputDirectory 'report.json'
|
|
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $reportPath -Encoding utf8NoBOM
|
|
Write-Host "Campaign LA preflight report: $reportPath"
|
|
if (-not $report.success) { exit 1 }
|
|
}
|