Skip to content

Commit 50a3b60

Browse files
committed
Refactor release pipeline, update local deploy & channels
Centralize release logic in Publish-Release.ps1, add dual-publish and compatibility-updater options, and update environment variables for flexibility. Rewrite deploy-local.ps1 to use a shared base script and Nbgv helpers. Update ModdingToolBase submodule. Switch .NET Framework update channel to /v2/
1 parent 0a581ed commit 50a3b60

4 files changed

Lines changed: 94 additions & 192 deletions

File tree

.github/workflows/release.yml

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,30 @@ on:
1313

1414
env:
1515
TOOL_PROJ_PATH: ./src/ModVerify.CliApp/ModVerify.CliApp.csproj
16-
CREATOR_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/ApplicationManifestCreator/ApplicationManifestCreator.csproj
17-
SIGNER_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/ApplicationManifestSigner/ApplicationManifestSigner.csproj
18-
UPLOADER_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/FtpUploader/FtpUploader.csproj
16+
PUBLISH_SCRIPT: ./modules/ModdingToolBase/scripts/Publish-Release.ps1
1917
TOOL_EXE: ModVerify.exe
2018
UPDATER_EXE: AnakinRaW.ExternalUpdater.exe
21-
MANIFEST_CREATOR: AnakinRaW.ApplicationManifestCreator.dll
22-
MANIFEST_SIGNER: AnakinRaW.ApplicationManifestSigner.dll
23-
SFTP_UPLOADER: AnakinRaW.FtpUploader.dll
19+
EMBEDDED_TRUST_CERT: src/ModVerify.CliApp/Resources/Certs/modverify-trust.cer
2420
ORIGIN_BASE: https://republicatwar.com/downloads/ModVerify
25-
ORIGIN_BASE_PART: downloads/ModVerify/
21+
SFTP_BASE_PATH: downloads/ModVerify/
2622
BRANCH_NAME: ${{ github.event.inputs.branch || 'stable' }}
2723

24+
# ---- Optional release knobs -------------------------------------------------------------
25+
# Valid combinations: none | (A) only | (A) + (B). (B) on its own is rejected.
26+
#
27+
# (A) Dual-publish to a next-generation channel. Use for URL migrations — the next-gen
28+
# channel gets seeded alongside the primary path. Pair the two lines below with the
29+
# matching -NextOrigin / -NextSftpBasePath args in the deploy step.
30+
# NEXT_ORIGIN_BASE: https://republicatwar.com/downloads/ModVerify/v2
31+
# NEXT_SFTP_BASE_PATH: downloads/ModVerify/v2/
32+
#
33+
# (B) Override the updater binary used for the PRIMARY deploy. Use for releases that ship
34+
# a new external-updater CLI: deployed clients still execute the previously-deployed
35+
# binary, so the manifest must keep listing that older binary. Pair with the matching
36+
# -CompatibilityUpdaterExePath arg in the deploy step. Requires (A) — the new updater
37+
# needs a next-gen channel to migrate clients to.
38+
# COMPAT_UPDATER: tools/legacy/AnakinRaW.ExternalUpdater.exe
39+
2840
jobs:
2941

3042
# Builds and tests the solution.
@@ -75,65 +87,37 @@ jobs:
7587
with:
7688
fetch-depth: 0
7789
submodules: recursive
78-
- name: Verify embedded trust cert
79-
shell: pwsh
80-
run: |
81-
$certPath = "src/ModVerify.CliApp/Resources/Certs/modverify-trust.cer"
82-
if (-not (Test-Path $certPath)) {
83-
Write-Error "$certPath is missing. Generate the production trust cert per docs/update-signing-setup.md and commit it before releasing."
84-
exit 1
85-
}
86-
try {
87-
$bytes = [IO.File]::ReadAllBytes($certPath)
88-
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)
89-
} catch {
90-
Write-Error "Failed to load $certPath as an X.509 certificate: $_"
91-
exit 1
92-
}
93-
if ($cert.HasPrivateKey) {
94-
Write-Error "$certPath contains a private key. Only the public DER (.cer) is allowed; embedding a PFX would ship the signing key to every consumer."
95-
exit 1
96-
}
97-
Write-Host "OK: $certPath is public-only ($($cert.Subject))."
9890
- uses: actions/download-artifact@v8
9991
with:
10092
name: Binary Releases
10193
path: ./releases
10294

103-
# Deploy .NET Framework self-update release
10495
- name: Setup .NET
10596
uses: actions/setup-dotnet@v5
10697
with:
10798
dotnet-version: 10.0.x
108-
- name: Build Creator
109-
run: dotnet build ${{env.CREATOR_PROJ_PATH}} --configuration Release --output ./dev
110-
- name: Build Signer
111-
run: dotnet build ${{env.SIGNER_PROJ_PATH}} --configuration Release --output ./dev
112-
- name: Build Uploader
113-
run: dotnet build ${{env.UPLOADER_PROJ_PATH}} --configuration Release --output ./dev
114-
- name: Create binaries directory
115-
run: mkdir -p ./deploy
116-
- name: Copy self-update files
99+
100+
- name: Publish self-update release
101+
shell: pwsh
117102
run: |
118-
cp ./releases/net481/${{env.TOOL_EXE}} ./deploy/
119-
cp ./releases/net481/${{env.UPDATER_EXE}} ./deploy/
120-
- name: Create Manifest
121-
run: dotnet ./dev/${{env.MANIFEST_CREATOR}} -a deploy/${{env.TOOL_EXE}} --appDataFiles deploy/${{env.UPDATER_EXE}} --origin ${{env.ORIGIN_BASE}} -o ./deploy -b ${{env.BRANCH_NAME}}
122-
- name: Decode signing pfx
123-
shell: bash
124-
run: echo "${{ secrets.UPDATER_SIGNING_PFX_B64 }}" | base64 -d > $RUNNER_TEMP/signing.pfx
125-
- name: Sign Manifest
126-
run: dotnet ./dev/${{env.MANIFEST_SIGNER}} --manifest ./deploy/manifest.json --pfx $RUNNER_TEMP/signing.pfx --password "${{ secrets.UPDATER_SIGNING_PFX_PASSWORD }}"
127-
- name: Wipe pfx
128-
if: always()
129-
run: rm -f $RUNNER_TEMP/signing.pfx
130-
- name: Upload Build
131-
run: dotnet ./dev/${{env.SFTP_UPLOADER}} ftp --host $host --port $port -u ${{secrets.SFTP_USER}} -p ${{secrets.SFTP_PASSWORD}} --base $base_path -s $source
132-
env:
133-
host: republicatwar.com
134-
port: 1579
135-
base_path: ${{env.ORIGIN_BASE_PART}}
136-
source: ./deploy
103+
& ${{ env.PUBLISH_SCRIPT }} `
104+
-AppExePath "./releases/net481/${{ env.TOOL_EXE }}" `
105+
-UpdaterExePath "./releases/net481/${{ env.UPDATER_EXE }}" `
106+
-EmbeddedTrustCertPath "${{ env.EMBEDDED_TRUST_CERT }}" `
107+
-Origin "${{ env.ORIGIN_BASE }}" `
108+
-SftpBasePath "${{ env.SFTP_BASE_PATH }}" `
109+
-Branch "${{ env.BRANCH_NAME }}" `
110+
-SigningPfxBase64 "${{ secrets.UPDATER_SIGNING_PFX_B64 }}" `
111+
-SigningPfxPassword "${{ secrets.UPDATER_SIGNING_PFX_PASSWORD }}" `
112+
-SftpHost "republicatwar.com" `
113+
-SftpPort 1579 `
114+
-SftpUser "${{ secrets.SFTP_USER }}" `
115+
-SftpPassword "${{ secrets.SFTP_PASSWORD }}"
116+
# (A) Dual-publish — uncomment with NEXT_ORIGIN_BASE / NEXT_SFTP_BASE_PATH above.
117+
# -NextOrigin "${{ env.NEXT_ORIGIN_BASE }}" `
118+
# -NextSftpBasePath "${{ env.NEXT_SFTP_BASE_PATH }}" `
119+
# (B) Compatibility updater — uncomment with COMPAT_UPDATER above.
120+
# -CompatibilityUpdaterExePath "${{ env.COMPAT_UPDATER }}"
137121
138122
# Deploy .NET Core and .NET Framework apps to Github
139123
- name: Create NET Core .zip

deploy-local.ps1

Lines changed: 48 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,67 @@
1-
# Local deployment script for ModVerify to test the update feature.
2-
# This script builds the application twice at different versions, creates an update manifest
3-
# for the newer one, and stages an "installed" copy of the older one — so triggering the
4-
# update flow against the local server actually finds an update.
1+
# =========================================================================================
2+
# Local deployment to test the ModVerify update flow end-to-end.
3+
#
4+
# USAGE
5+
# .\deploy-local.ps1 # single-channel test
6+
# .\deploy-local.ps1 -DualPublish # also publish to a next-gen channel
7+
# .\deploy-local.ps1 -DualPublish -CompatibilityUpdater <path>
8+
# # primary uses compat updater;
9+
# # next-gen uses build-output updater
10+
#
11+
# -CompatibilityUpdater requires -DualPublish.
12+
# -InstalledVersion / -ServerVersion override the version pair used to set up the test.
13+
#
14+
# Builds ModVerify twice (older "installed" + newer "server"), then hands off to the shared
15+
# Publish-LocalRelease.ps1 in ModdingToolBase for cert generation, manifest signing, and
16+
# install-dir staging.
17+
# =========================================================================================
518

619
param(
7-
# Version baked into the "already installed" copy. Must be lower than $ServerVersion
8-
# so the updater treats the server build as newer.
920
[string]$InstalledVersion = "0.0.1-local",
10-
11-
# Version baked into the build that ends up on the local "server" / in the manifest.
12-
[string]$ServerVersion = "0.0.2-local"
21+
[string]$ServerVersion = "99.99.99-local",
22+
[switch]$DualPublish,
23+
[string]$CompatibilityUpdater
1324
)
1425

1526
$ErrorActionPreference = "Stop"
1627

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

20-
$deployRoot = Join-Path $root ".local_deploy"
21-
$stagingDir = Join-Path $deployRoot "staging"
22-
$serverDir = Join-Path $deployRoot "server"
23-
$installDir = Join-Path $deployRoot "install"
24-
25-
$toolProj = Join-Path $root "src\ModVerify.CliApp\ModVerify.CliApp.csproj"
26-
$creatorProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\ApplicationManifestCreator\ApplicationManifestCreator.csproj"
27-
$signerProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\ApplicationManifestSigner\ApplicationManifestSigner.csproj"
28-
$uploaderProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\FtpUploader\FtpUploader.csproj"
29-
30-
$toolExe = "ModVerify.exe"
31-
$updaterExe = "AnakinRaW.ExternalUpdater.exe"
32-
$manifestCreatorDll = "AnakinRaW.ApplicationManifestCreator.dll"
33-
$manifestSignerDll = "AnakinRaW.ApplicationManifestSigner.dll"
34-
$uploaderDll = "AnakinRaW.FtpUploader.dll"
31+
. (Join-Path $root "modules\ModdingToolBase\scripts\NbgvVersion.ps1")
3532

36-
$devPfx = Join-Path $deployRoot "dev-signing.pfx"
37-
$devCer = Join-Path $deployRoot "dev-trust.cer"
38-
$devPwd = "devpass"
33+
$deployRoot = Join-Path $root ".local_deploy"
34+
$installBuildDir = Join-Path $deployRoot "bin\install"
35+
$serverBuildDir = Join-Path $deployRoot "bin\tool"
3936

40-
$versionJsonPath = Join-Path $root "version.json"
41-
$versionJsonBackup = [IO.File]::ReadAllText($versionJsonPath)
42-
43-
function Set-NbgvVersion {
44-
param([string]$Version)
45-
$json = $versionJsonBackup | ConvertFrom-Json
46-
$json.version = $Version
47-
# publicReleaseRefSpec defaults the build to non-public; clearing it gives us a clean
48-
# "X.Y.Z" InformationalVersion locally without the +gitHash height suffix making
49-
# comparisons noisier than they need to be.
50-
if ($json.PSObject.Properties.Name -contains 'publicReleaseRefSpec') {
51-
$json.publicReleaseRefSpec = @()
52-
}
53-
($json | ConvertTo-Json -Depth 32) | Set-Content -Path $versionJsonPath -Encoding UTF8
54-
}
37+
$toolProj = Join-Path $root "src\ModVerify.CliApp\ModVerify.CliApp.csproj"
38+
$baseScript = Join-Path $root "modules\ModdingToolBase\scripts\Publish-LocalRelease.ps1"
5539

56-
try {
57-
58-
# 1. Clean and Create directories
5940
if (Test-Path $deployRoot) { Remove-Item -Recurse -Force $deployRoot }
60-
New-Item -ItemType Directory -Path $stagingDir | Out-Null
61-
New-Item -ItemType Directory -Path $serverDir | Out-Null
62-
New-Item -ItemType Directory -Path $installDir | Out-Null
63-
64-
Write-Host "--- Building ModVerify (net481) @ installed v$InstalledVersion ---" -ForegroundColor Cyan
65-
Set-NbgvVersion -Version $InstalledVersion
66-
dotnet build $toolProj --configuration Release -f net481 --output "$deployRoot\bin\install" /p:DebugType=None /p:DebugSymbols=false /p:LocalDeploy=true
67-
68-
Write-Host "--- Building ModVerify (net481) @ server v$ServerVersion ---" -ForegroundColor Cyan
69-
Set-NbgvVersion -Version $ServerVersion
70-
dotnet build $toolProj --configuration Release -f net481 --output "$deployRoot\bin\tool" /p:DebugType=None /p:DebugSymbols=false /p:LocalDeploy=true
71-
72-
Write-Host "--- Building Manifest Creator ---" -ForegroundColor Cyan
73-
dotnet build $creatorProj --configuration Release --output "$deployRoot\bin\creator"
41+
New-Item -ItemType Directory -Path $deployRoot | Out-Null
7442

75-
Write-Host "--- Building Manifest Signer ---" -ForegroundColor Cyan
76-
dotnet build $signerProj --configuration Release --output "$deployRoot\bin\signer"
77-
78-
Write-Host "--- Building Local Uploader ---" -ForegroundColor Cyan
79-
dotnet build $uploaderProj --configuration Release --output "$deployRoot\bin\uploader"
80-
81-
Write-Host "--- Generating dev signing cert ---" -ForegroundColor Cyan
82-
$curve = [System.Security.Cryptography.ECCurve]::CreateFromFriendlyName("nistP256")
83-
$ecdsa = [System.Security.Cryptography.ECDsa]::Create($curve)
84-
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
85-
"CN=ModVerify Dev Signing",
86-
$ecdsa,
87-
[System.Security.Cryptography.HashAlgorithmName]::SHA256)
88-
$cert = $req.CreateSelfSigned(
89-
[DateTimeOffset]::UtcNow.AddDays(-1),
90-
[DateTimeOffset]::UtcNow.AddYears(10))
91-
[IO.File]::WriteAllBytes($devPfx, $cert.Export(
92-
[System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $devPwd))
93-
[IO.File]::WriteAllBytes($devCer, $cert.Export(
94-
[System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
95-
$cert.Dispose()
96-
$ecdsa.Dispose()
97-
98-
# 2. Prepare staging
99-
Write-Host "--- Preparing Staging ---" -ForegroundColor Cyan
100-
Copy-Item "$deployRoot\bin\tool\$toolExe" $stagingDir
101-
Copy-Item "$deployRoot\bin\tool\$updaterExe" $stagingDir
102-
103-
# 3. Create Manifest
104-
# Origin must be an absolute URI for the manifest creator.
105-
# Using 127.0.0.1 and file:// is tricky with Flurl/DownloadManager sometimes.
106-
# We'll use the local path and ensure it's formatted correctly.
107-
$serverPath = (Resolve-Path $serverDir).Path
108-
$serverUri = "file:///$($serverPath.Replace('\', '/'))"
109-
# If we have 3 slashes, Flurl/DownloadManager might still fail on Windows if it expects a certain format.
110-
# However, the ManifestCreator just needs a valid URI for the 'Origin' field in the manifest.
111-
Write-Host "--- Creating Manifest (Origin: $serverUri) ---" -ForegroundColor Cyan
112-
dotnet "$deployRoot\bin\creator\$manifestCreatorDll" `
113-
-a "$stagingDir\$toolExe" `
114-
--appDataFiles "$stagingDir\$updaterExe" `
115-
--origin "$serverUri" `
116-
-o "$stagingDir" `
117-
-b "beta"
118-
119-
Write-Host "--- Signing Manifest ---" -ForegroundColor Cyan
120-
dotnet "$deployRoot\bin\signer\$manifestSignerDll" `
121-
--manifest "$stagingDir\manifest.json" `
122-
--pfx $devPfx `
123-
--password $devPwd
124-
125-
# 4. "Deploy" to server using the local uploader
126-
Write-Host "--- Deploying to Local Server ---" -ForegroundColor Cyan
127-
dotnet "$deployRoot\bin\uploader\$uploaderDll" local --base "$serverDir" --source "$stagingDir"
128-
129-
# 5. Setup a "test" installation — uses the older-version build so the updater sees the
130-
# staged server build as an upgrade.
131-
Write-Host "--- Setting up Test Installation (v$InstalledVersion) ---" -ForegroundColor Cyan
132-
Copy-Item "$deployRoot\bin\install\*" $installDir -Recurse
133-
134-
Write-Host "`nLocal deployment complete!" -ForegroundColor Green
135-
Write-Host "Installed version: $InstalledVersion"
136-
Write-Host "Server version: $ServerVersion"
137-
Write-Host "Server directory: $serverDir"
138-
Write-Host "Install directory: $installDir"
139-
Write-Host "`nTo test the update:"
140-
Write-Host "1. Run ModVerify from the install directory with the following command:"
141-
Write-Host " cd '$installDir'"
142-
Write-Host " .\ModVerify.exe updateApplication --updateBranch beta --updateServerUrl '$serverUri'"
143-
Write-Host "`n Note: --updateServerUrl takes a server base URL and resolves to <server>/<branch>/manifest.json."
144-
Write-Host " Use --updateManifestUrl instead if you want to point directly at a full manifest URL."
145-
Write-Host "`n2. To re-test, just rerun this script — every run produces v$InstalledVersion installed against v$ServerVersion on the server."
146-
Write-Host " Override with -InstalledVersion / -ServerVersion to exercise other version transitions."
43+
$nbgv = Backup-NbgvVersion -RepoRoot $root
44+
try {
45+
Write-Host "--- Building ModVerify (net481) @ installed v$InstalledVersion ---" -ForegroundColor Cyan
46+
Set-NbgvVersion -Snapshot $nbgv -Version $InstalledVersion
47+
dotnet build $toolProj --configuration Release -f net481 --output $installBuildDir /p:DebugType=None /p:DebugSymbols=false /p:LocalDeploy=true
48+
49+
Write-Host "--- Building ModVerify (net481) @ server v$ServerVersion ---" -ForegroundColor Cyan
50+
Set-NbgvVersion -Snapshot $nbgv -Version $ServerVersion
51+
dotnet build $toolProj --configuration Release -f net481 --output $serverBuildDir /p:DebugType=None /p:DebugSymbols=false /p:LocalDeploy=true
52+
53+
$publishParams = @{
54+
AppExePath = Join-Path $serverBuildDir "ModVerify.exe"
55+
UpdaterExePath = Join-Path $serverBuildDir "AnakinRaW.ExternalUpdater.exe"
56+
DeployRoot = $deployRoot
57+
InstallBuildDir = $installBuildDir
58+
Branch = "beta"
59+
}
60+
if ($DualPublish) { $publishParams.DualPublish = $true }
61+
if ($CompatibilityUpdater) { $publishParams.CompatibilityUpdater = $CompatibilityUpdater }
14762

63+
& $baseScript @publishParams
14864
}
14965
finally {
150-
# Always restore version.json verbatim (bytes-in == bytes-out), even if a build step above failed.
151-
[IO.File]::WriteAllText($versionJsonPath, $versionJsonBackup)
66+
Restore-NbgvVersion -Snapshot $nbgv
15267
}

modules/ModdingToolBase

src/ModVerify.CliApp/ModVerifyAppEnvironment.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ internal sealed class ModVerifyAppEnvironment(Assembly assembly, IFileSystem fil
2626

2727
#if NETFRAMEWORK
2828

29+
// The /v2/ path segment is the post-migration update channel: deployed legacy clients
30+
// still fetch from the historical /downloads/ModVerify path (frozen with the migration
31+
// release), while this and every subsequent build pulls from /v2/.
2932
public override ICollection<Uri> UpdateMirrors { get; } = new List<Uri>
3033
{
3134
#if DEBUG
32-
new(CreateDebugPath()),
35+
new(CreateDebugPath()),
3336
#endif
34-
new($"https://republicatwar.com/downloads/{ModVerifyConstants.ModVerifyToolPath}")
37+
new($"https://republicatwar.com/downloads/{ModVerifyConstants.ModVerifyToolPath}/v2")
3538
};
3639

3740
private static string CreateDebugPath()

0 commit comments

Comments
 (0)