diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml
deleted file mode 100644
index 7765576..0000000
--- a/.github/workflows/integration-test.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Update Integration Test
-
-on:
- workflow_call:
- workflow_dispatch:
-
-jobs:
- update-cycle:
- name: Update cycle (${{ matrix.scenario.name }})
- runs-on: windows-latest
- strategy:
- fail-fast: false
- matrix:
- 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
- with:
- fetch-depth: 0
- submodules: recursive
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v5
- with:
- dotnet-version: 10.0.x
-
- - name: Run ${{ matrix.scenario.name }}-channel update cycle
- shell: pwsh
- run: ${{ matrix.scenario.script }}
-
- - name: Upload diagnostics on failure
- if: failure()
- uses: actions/upload-artifact@v7
- with:
- name: update-cycle-${{ matrix.scenario.name }}-logs
- # Updater log: %TEMP%\extUpdateLog-*.txt; RawDevLauncher app log: %APPDATA%\RawDevLauncher\*.txt
- path: |
- ${{ runner.temp }}\extUpdateLog-*.txt
- ${{ env.APPDATA }}\RawDevLauncher\*.txt
- if-no-files-found: ignore
- retention-days: 7
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a55bdf0..ab19b84 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,50 +10,36 @@ on:
branch:
description: "The branch to a release to"
required: true
-
+
env:
- TOOL_PROJ_PATH: ./src/DevLauncher/DevLauncher.csproj
- PUBLISH_SCRIPT: ./ModdingToolBase/scripts/Publish-Release.ps1
- TOOL_EXE: RaW-DevLauncher.exe
- UPDATER_EXE: AnakinRaW.ExternalUpdater.exe
- EMBEDDED_TRUST_CERT: src/DevLauncher/Resources/Certs/anakinraw-trust.cer
- ORIGIN_BASE: https://republicatwar.com/downloads/RawDevLauncher/v2
- SFTP_BASE_PATH: downloads/RawDevLauncher/v2
+ # Per-run release channel. Every other per-app value lives in update-tooling.jsonc and is loaded
+ # into the environment by the "Load app config" step in each job below.
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.
- # NEXT_ORIGIN_BASE: https://republicatwar.com/downloads/RawDevLauncher/v2
-
- # SFTP path the next-generation channel uploads to. Set together with NEXT_ORIGIN_BASE.
- # NEXT_SFTP_BASE_PATH: downloads/RawDevLauncher/v2/
-
- # Previously-deployed updater used in place of the build-output one for the primary deploy.
- # Only the old-gen manifest lists this binary; the next-gen manifest still uses the build-output updater.
- # Requires NEXT_ORIGIN_BASE + NEXT_SFTP_BASE_PATH to be set.
- # COMPAT_UPDATER: tools/v1/AnakinRaW.ExternalUpdater.exe
-
jobs:
# Builds and tests the solution.
test:
uses: ./.github/workflows/test.yml
- # End-to-end self-update test
+ # End-to-end self-update test.
integration-test:
+ name: Integration test
needs: [test]
- uses: ./.github/workflows/integration-test.yml
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+ - name: Run self-update integration suite
+ shell: pwsh
+ run: |
+ $c = Get-Content -Raw ./update-tooling.jsonc | ConvertFrom-Json
+ & "$($c.scriptsDir)/Test-LocalUpdateSuite.ps1" -ConfigPath ./update-tooling.jsonc
pack:
name: Pack
@@ -65,6 +51,11 @@ jobs:
with:
fetch-depth: 0
submodules: recursive
+ - name: Load app config
+ shell: pwsh
+ run: |
+ $c = Get-Content -Raw ./update-tooling.jsonc | ConvertFrom-Json
+ "TOOL_PROJ_PATH=$($c.toolProject)" >> $env:GITHUB_ENV
- name: Setup .NET
uses: actions/setup-dotnet@v5
- name: Create NetFramework Release
@@ -103,26 +94,37 @@ jobs:
with:
dotnet-version: 10.0.x
+ - name: Load app config
+ shell: pwsh
+ # Per-app release values come from update-tooling.jsonc.
+ run: |
+ $c = Get-Content -Raw ./update-tooling.jsonc | ConvertFrom-Json
+ "PUBLISH_SCRIPT=$($c.scriptsDir)/Publish-Release.ps1" >> $env:GITHUB_ENV
+ "TOOL_EXE=$($c.appExe)" >> $env:GITHUB_ENV
+ "UPDATER_EXE=$($c.updaterExe)" >> $env:GITHUB_ENV
+ "EMBEDDED_TRUST_CERT=$($c.embeddedTrustCert)" >> $env:GITHUB_ENV
+ "ORIGIN_BASE=$($c.origin)" >> $env:GITHUB_ENV
+ "SFTP_BASE_PATH=$($c.sftpBasePath)" >> $env:GITHUB_ENV
+ "SFTP_HOST=$($c.sftpHost)" >> $env:GITHUB_ENV
+ "SFTP_PORT=$($c.sftpPort)" >> $env:GITHUB_ENV
+ "NEXT_ORIGIN_BASE=$($c.nextOrigin)" >> $env:GITHUB_ENV
+ "NEXT_SFTP_BASE_PATH=$($c.nextSftpBasePath)" >> $env:GITHUB_ENV
+ "COMPAT_UPDATER=$($c.compatUpdater)" >> $env:GITHUB_ENV
- 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 "$updaterExe" `
+ -UpdaterExePath "./releases/net481/$env:UPDATER_EXE" `
-EmbeddedTrustCertPath "$env:EMBEDDED_TRUST_CERT" `
-Origin "$env:ORIGIN_BASE" `
-SftpBasePath "$env:SFTP_BASE_PATH" `
-Branch "$env:BRANCH_NAME" `
-SigningPfxBase64 "${{ secrets.UPDATER_SIGNING_PFX_B64 }}" `
-SigningPfxPassword "${{ secrets.UPDATER_SIGNING_PFX_PASSWORD }}" `
- -SftpHost "republicatwar.com" `
- -SftpPort 1579 `
+ -SftpHost "$env:SFTP_HOST" `
+ -SftpPort $env:SFTP_PORT `
-SftpUser "${{ secrets.SFTP_USER }}" `
-SftpPassword "${{ secrets.SFTP_PASSWORD }}" `
-NextOrigin "$env:NEXT_ORIGIN_BASE" `
@@ -141,5 +143,5 @@ jobs:
tag_name: v${{ steps.nbgv.outputs.SemVer2 }}
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: false
- files: |
- ./releases/net481/$env:TOOL_EXE
\ No newline at end of file
+ files: |
+ ./releases/net481/${{ env.TOOL_EXE }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 05f2035..ff97bd9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -23,4 +23,21 @@ jobs:
dotnet-version: 10.0.x
- name: Build in Release Mode
- run: dotnet test --configuration Release --report-github
\ No newline at end of file
+ run: dotnet test --configuration Release --report-github
+
+ test-moddingtoolbase:
+ name: Test ModdingToolBase
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+ - name: Test ModdingToolBase (submodule)
+ shell: pwsh
+ run: |
+ $c = Get-Content -Raw ./update-tooling.jsonc | ConvertFrom-Json
+ & "$($c.scriptsDir)/Test-ModdingToolBase.ps1"
\ No newline at end of file
diff --git a/ModVerify b/ModVerify
index f7f3a24..0b66df6 160000
--- a/ModVerify
+++ b/ModVerify
@@ -1 +1 @@
-Subproject commit f7f3a248fb05dc579b425f42f2c659688568ba28
+Subproject commit 0b66df679003dc6b181d2a1433e68b44fb9d1e5a
diff --git a/ModdingToolBase b/ModdingToolBase
index d575a11..1fc635e 160000
--- a/ModdingToolBase
+++ b/ModdingToolBase
@@ -1 +1 @@
-Subproject commit d575a112196400d2710ed7d8b1d0759316c8b712
+Subproject commit 1fc635ed3050e77992fbc8fd3db1916669c00d33
diff --git a/deploy-local.ps1 b/deploy-local.ps1
deleted file mode 100644
index 7613e36..0000000
--- a/deploy-local.ps1
+++ /dev/null
@@ -1,67 +0,0 @@
-param(
- [string]$InstalledVersion = "0.0.1-local",
- [string]$ServerVersion = "99.99.99-local",
- [switch]$DualPublish,
- [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"
-
-$root = $PSScriptRoot
-if ([string]::IsNullOrEmpty($root)) { $root = Get-Location }
-
-. (Join-Path $root "ModdingToolBase\scripts\NbgvVersion.ps1")
-
-$deployRoot = Join-Path $root ".local_deploy"
-$installBuildDir = Join-Path $deployRoot "bin\install"
-$serverBuildDir = Join-Path $deployRoot "bin\tool"
-
-$toolProj = Join-Path $root "src\DevLauncher\DevLauncher.csproj"
-$baseScript = Join-Path $root "ModdingToolBase\scripts\Publish-LocalRelease.ps1"
-
-if (Test-Path $deployRoot) { Remove-Item -Recurse -Force $deployRoot }
-New-Item -ItemType Directory -Path $deployRoot | Out-Null
-
-$nbgv = Backup-NbgvVersion -RepoRoot $root
-try {
- Write-Host "--- Building DevLauncher (net481) @ installed v$InstalledVersion ---" -ForegroundColor Cyan
- Set-NbgvVersion -Snapshot $nbgv -Version $InstalledVersion
- dotnet build $toolProj --configuration Release -f net481 --output $installBuildDir /p:DebugType=None /p:DebugSymbols=false /p:LocalDeploy=true
-
- Write-Host "--- Building DevLauncher (net481) @ server v$ServerVersion ---" -ForegroundColor Cyan
- 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"
- DeployRoot = $deployRoot
- InstallBuildDir = $installBuildDir
- Branch = "beta"
- }
- if ($DualPublish) { $publishParams.DualPublish = $true }
- if ($CompatibilityUpdater) { $publishParams.CompatibilityUpdater = $CompatibilityUpdater }
-
- & $baseScript @publishParams
-}
-finally {
- Restore-NbgvVersion -Snapshot $nbgv
-}
\ No newline at end of file
diff --git a/src/DevLauncher/DevLauncher.csproj b/src/DevLauncher/DevLauncher.csproj
index e249ea2..4514c2e 100644
--- a/src/DevLauncher/DevLauncher.csproj
+++ b/src/DevLauncher/DevLauncher.csproj
@@ -18,8 +18,8 @@
true
-
+
$(DefineConstants);LOCAL_DEPLOY
diff --git a/test-local-update-dual.ps1 b/test-local-update-dual.ps1
deleted file mode 100644
index 57ae3a8..0000000
--- a/test-local-update-dual.ps1
+++ /dev/null
@@ -1,39 +0,0 @@
-#Requires -Version 7.0
-
-[CmdletBinding()]
-param(
- [string]$InstalledVersion = '0.0.1-local',
- [string]$ServerVersion = '99.99.99-local',
- [string]$Branch = 'beta',
- [string]$CompatibilityUpdater
-)
-
-$ErrorActionPreference = 'Stop'
-
-$root = $PSScriptRoot
-if ([string]::IsNullOrEmpty($root)) { $root = Get-Location }
-
-$deployArgs = @{
- InstalledVersion = $InstalledVersion
- ServerVersion = $ServerVersion
- DualPublish = $true
-}
-if ($CompatibilityUpdater) { $deployArgs.CompatibilityUpdater = $CompatibilityUpdater }
-
-& (Join-Path $root 'deploy-local.ps1') @deployArgs
-if ($LASTEXITCODE -ne 0) { throw "deploy-local.ps1 -DualPublish failed (exit $LASTEXITCODE)." }
-
-$nextServerDir = Join-Path $root '.local_deploy\server\v2'
-if (-not (Test-Path $nextServerDir)) {
- throw "Expected /v2/ server dir at '$nextServerDir' but it does not exist."
-}
-$nextServerUri = "file:///$(((Resolve-Path $nextServerDir).Path -replace '\\','/'))"
-
-& (Join-Path $root 'ModdingToolBase\scripts\Test-LocalUpdateCycle.ps1') `
- -AppExePath (Join-Path $root '.local_deploy\install\RaW-DevLauncher.exe') `
- -ServerUri $nextServerUri `
- -Branch $Branch `
- -NoUpdateMessage 'No update available.' `
- -ExpectedNewVersion $ServerVersion
-
-exit $LASTEXITCODE
\ No newline at end of file
diff --git a/test-local-update-updater-mismatch.ps1 b/test-local-update-updater-mismatch.ps1
deleted file mode 100644
index 3937fb9..0000000
--- a/test-local-update-updater-mismatch.ps1
+++ /dev/null
@@ -1,40 +0,0 @@
-# =========================================================================================
-# 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
diff --git a/test-local-update.ps1 b/test-local-update.ps1
deleted file mode 100644
index 5a5f69e..0000000
--- a/test-local-update.ps1
+++ /dev/null
@@ -1,37 +0,0 @@
-# =========================================================================================
-# Local bootstrap for the self-update integration test.
-#
-# Stages a local deploy via deploy-local.ps1, then runs the shared end-to-end test from
-# ModdingToolBase against the staged install dir + signed local server.
-#
-# 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
-if ($LASTEXITCODE -ne 0) { throw "deploy-local.ps1 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
\ No newline at end of file
diff --git a/tools/AnakinRaW.ExternalUpdater.exe b/tools/AnakinRaW.ExternalUpdater.exe
deleted file mode 100644
index 2bb788e..0000000
Binary files a/tools/AnakinRaW.ExternalUpdater.exe and /dev/null differ
diff --git a/update-tooling.jsonc b/update-tooling.jsonc
new file mode 100644
index 0000000..f498dd1
--- /dev/null
+++ b/update-tooling.jsonc
@@ -0,0 +1,40 @@
+// Per-app configuration for the shared ModdingToolBase release/deploy tooling.
+// Consumed by ModdingToolBase/scripts/*.ps1 (local dev/test) and by .github/workflows/release.yml (CI).
+//
+// Parsed only with PowerShell's ConvertFrom-Json, which tolerates // and /* */ comments and
+// trailing commas. Keep it that way: don't point a strict JSON validator at this file.
+{
+ // ===== SHARED -- consumed by BOTH dev/test (local scripts) and prod (release.yml CI) =======
+ "toolProject": "./src/DevLauncher/DevLauncher.csproj", // app csproj to build
+ "appExe": "RaW-DevLauncher.exe", // built app exe name
+ "updaterExe": "AnakinRaW.ExternalUpdater.exe", // external updater exe name
+ "scriptsDir": "./ModdingToolBase/scripts", // ModdingToolBase scripts dir (its submodule path)
+
+ // ===== DEV/TEST ONLY -- the local self-update test scripts; never used by a prod release ===
+ // testUpdateBranch: the update-server channel the local test publishes to and reads from
+ // (feeds --updateBranch). It's an update channel, NOT a git branch; prod uses "stable"
+ // (set in release.yml's BRANCH_NAME). Note what is NOT here:
+ // - targetFramework: fixed at net481 by the tooling (external updater is net481-only),
+ // so it's a Build-LocalRelease.ps1 default, not a per-app value.
+ // - noUpdateMessage: the test now asserts the re-check exits 0 and leaves the exe version
+ // unchanged, instead of scraping a host-specific console string.
+ "testUpdateBranch": "beta",
+ "appLogDir": "RawDevLauncher", // %APPDATA% subfolder with the app log (failure diagnostics)
+
+ // ===== PROD / RELEASE ONLY -- published by release.yml (CI); unused by the local test ======
+ "embeddedTrustCert": "src/DevLauncher/Resources/Certs/anakinraw-trust.cer", // public trust .cer shipped in the app
+ "origin": "https://republicatwar.com/downloads/RawDevLauncher/v2", // manifest origin URL
+ "sftpBasePath": "downloads/RawDevLauncher/v2", // SFTP upload base path
+ "sftpHost": "republicatwar.com",
+ "sftpPort": 1579,
+
+ // Migration release (cross-API). Leave all three empty for a normal release; populate them to
+ // publish a next-generation channel alongside the current one. See Publish-Release.ps1.
+ // nextOrigin - origin URL of the next-gen channel
+ // nextSftpBasePath - SFTP base path for the next-gen channel (set together with nextOrigin)
+ // compatUpdater - previous-gen updater used for the PRIMARY deploy during the cutover
+ // (only valid when nextOrigin + nextSftpBasePath are set)
+ "nextOrigin": "",
+ "nextSftpBasePath": "",
+ "compatUpdater": ""
+}