Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ jobs:
scenario:
- { name: single, script: ./test-local-update.ps1 }
- { name: dual, script: ./test-local-update-dual.ps1 }
# Regression guard: server ships a byte-different external updater (same version), which
# is installed eagerly and so must be trusted from the signed manifest at launch time.
- { name: updater-mismatch, script: ./test-local-update-updater-mismatch.ps1 }
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down
21 changes: 19 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ env:
SFTP_BASE_PATH: downloads/RawDevLauncher/v2
BRANCH_NAME: ${{ github.event.inputs.branch || 'stable' }}

# RESCUE (transitional). Publish THIS external-updater binary instead of the freshly built one.
# A client on an old version rejects any updater whose bytes differ from the copy it embeds, so it
# cannot self-update once the updater is rebuilt. Serving the exact binary it embeds lets its old
# integrity check pass, so it can pull the fixed app exe. The binary MUST be byte-identical to that
# client's embedded updater (verified for 3.0.2). This does not change the app, only which updater
# is published. CLEAR IT the release after the fleet has moved off the broken version (otherwise a
# patched client re-downloads it every launch). Unlike COMPAT_UPDATER below, this needs no next-gen
# channel — it simply substitutes the primary updater, so no NEXT_ORIGIN guard applies.
RESCUE_UPDATER: tools/AnakinRaW.ExternalUpdater.exe

# Migration-release values. Leave empty for a normal release; populate to enable.
#
# Origin URL of the next-generation channel, written into the manifest's componentOriginInfo.
Expand Down Expand Up @@ -97,9 +107,14 @@ jobs:
- name: Publish self-update release
shell: pwsh
run: |
# RESCUE_UPDATER, when set, replaces the freshly built updater with a pinned binary so old
# clients can still self-update (see env comment). Falls back to the build output.
$updaterExe = if ($env:RESCUE_UPDATER) { $env:RESCUE_UPDATER } else { "./releases/net481/$env:UPDATER_EXE" }
if (-not (Test-Path $updaterExe)) { throw "Updater exe not found at '$updaterExe'." }
Write-Host "Publishing updater: $updaterExe"
& $env:PUBLISH_SCRIPT `
-AppExePath "./releases/net481/$env:TOOL_EXE" `
-UpdaterExePath "./releases/net481/$env:UPDATER_EXE" `
-UpdaterExePath "$updaterExe" `
-EmbeddedTrustCertPath "$env:EMBEDDED_TRUST_CERT" `
-Origin "$env:ORIGIN_BASE" `
-SftpBasePath "$env:SFTP_BASE_PATH" `
Expand All @@ -125,4 +140,6 @@ jobs:
name: v${{ steps.nbgv.outputs.SemVer2 }}
tag_name: v${{ steps.nbgv.outputs.SemVer2 }}
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: false
generate_release_notes: false
files: |
./releases/net481/$env:TOOL_EXE
20 changes: 19 additions & 1 deletion deploy-local.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ param(
[string]$InstalledVersion = "0.0.1-local",
[string]$ServerVersion = "99.99.99-local",
[switch]$DualPublish,
[string]$CompatibilityUpdater
[string]$CompatibilityUpdater,

# Make the server's updater byte-different from the installed app's embedded copy (same version,
# still runnable) so the cycle exercises the launch-time integrity check. See block below.
[switch]$PerturbServerUpdater
)

$ErrorActionPreference = "Stop"
Expand Down Expand Up @@ -32,6 +36,20 @@ try {
Set-NbgvVersion -Snapshot $nbgv -Version $ServerVersion
dotnet build $toolProj --configuration Release -f net481 --output $serverBuildDir /p:DebugType=None /p:DebugSymbols=false /p:LocalDeploy=true

if ($PerturbServerUpdater) {
# Make the server's updater differ byte-for-byte from the installed app's embedded copy.
# Deterministic builds otherwise produce identical updaters, so the cycle never exercises an
# updater whose bytes changed. Appended trailing bytes change the SHA-256 but are ignored by
# the PE loader (the exe still runs) and leave the version untouched — i.e. a same-version
# rebuild, which must be trusted from the signed manifest at launch.
$serverUpdater = Join-Path $serverBuildDir "AnakinRaW.ExternalUpdater.exe"
if (-not (Test-Path $serverUpdater)) { throw "Server external updater not found at '$serverUpdater'." }
Write-Host "--- Perturbing server external updater (trailing bytes) to force a hash mismatch ---" -ForegroundColor Cyan
$marker = [Text.Encoding]::ASCII.GetBytes("/*INTEGRITY-REGRESSION*/")
$fs = [IO.File]::Open($serverUpdater, [IO.FileMode]::Append, [IO.FileAccess]::Write)
try { $fs.Write($marker, 0, $marker.Length) } finally { $fs.Dispose() }
}

$publishParams = @{
AppExePath = Join-Path $serverBuildDir "RaW-DevLauncher.exe"
UpdaterExePath = Join-Path $serverBuildDir "AnakinRaW.ExternalUpdater.exe"
Expand Down
40 changes: 40 additions & 0 deletions test-local-update-updater-mismatch.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# =========================================================================================
# Regression scenario for the external-updater launch-time integrity check.
#
# Deploys via deploy-local.ps1 -PerturbServerUpdater (server ships an updater byte-different
# from the installed app's embedded copy, same version) and runs the shared end-to-end cycle.
# The updater installs eagerly, so it must be trusted from the signed manifest at launch.
# Pre-fix this threw "match none of the trusted hashes" at step [1/5]. Windows-only.
# =========================================================================================

#Requires -Version 7.0

[CmdletBinding()]
param(
[string]$InstalledVersion = '0.0.1-local',
[string]$ServerVersion = '99.99.99-local',
[string]$Branch = 'beta'
)

$ErrorActionPreference = 'Stop'

$root = $PSScriptRoot
if ([string]::IsNullOrEmpty($root)) { $root = Get-Location }

& (Join-Path $root 'deploy-local.ps1') `
-InstalledVersion $InstalledVersion `
-ServerVersion $ServerVersion `
-PerturbServerUpdater
if ($LASTEXITCODE -ne 0) { throw "deploy-local.ps1 -PerturbServerUpdater failed (exit $LASTEXITCODE)." }

$serverDir = Join-Path $root '.local_deploy\server'
$serverUri = "file:///$(((Resolve-Path $serverDir).Path -replace '\\','/'))"

& (Join-Path $root 'ModdingToolBase\scripts\Test-LocalUpdateCycle.ps1') `
-AppExePath (Join-Path $root '.local_deploy\install\RaW-DevLauncher.exe') `
-ServerUri $serverUri `
-Branch $Branch `
-NoUpdateMessage 'No update available.' `
-ExpectedNewVersion $ServerVersion

exit $LASTEXITCODE
Binary file added tools/AnakinRaW.ExternalUpdater.exe
Binary file not shown.
Loading