Skip to content

Build Windows (Signed + Deploy) #22

Build Windows (Signed + Deploy)

Build Windows (Signed + Deploy) #22

Workflow file for this run

name: Build Windows (Signed + Deploy)
on:
workflow_dispatch:
inputs:
configuration:
description: 'Build configuration'
required: true
default: 'Release'
type: choice
options:
- Release
- Debug
skip_deploy:
description: 'Skip deploying to registry (build + sign only)'
required: false
default: false
type: boolean
permissions:
id-token: write
contents: read
env:
DOTNET_VERSION: '9.0.x'
RUST_VERSION: '1.91'
jobs:
build-sign-deploy:
runs-on: windows-latest-large
steps:
# ── Checkout ──────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
# ── Setup toolchains ──────────────────────────────────────
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
targets: x86_64-pc-windows-msvc
- name: Restore Rust cache
id: rust-cache
uses: actions/cache/restore@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
core\target
key: rust-${{ runner.os }}-${{ hashFiles('core/Cargo.lock') }}
restore-keys: rust-${{ runner.os }}-
# ── Extract version from .csproj ──────────────────────────
- name: Extract version
id: version
shell: pwsh
run: |
[xml]$xml = Get-Content "desktop/PrivStack.Desktop/PrivStack.Desktop.csproj"
$version = $xml.Project.PropertyGroup.Version | Where-Object { $_ }
Write-Host "Version: $version"
"APP_VERSION=$version" >> $env:GITHUB_OUTPUT
# ── Build Rust core (MSVC) ────────────────────────────────
- name: Build Rust core
working-directory: core
run: cargo build --release --target x86_64-pc-windows-msvc -p privstack-ffi
- name: Save Rust cache
if: always()
uses: actions/cache/save@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
core\target
key: rust-${{ runner.os }}-${{ hashFiles('core/Cargo.lock') }}
- name: Copy native DLL to expected location
shell: pwsh
run: |
$src = "core/target/x86_64-pc-windows-msvc/release/privstack_ffi.dll"
$dest = "core/target/release"
New-Item -ItemType Directory -Path $dest -Force | Out-Null
Copy-Item $src "$dest/privstack_ffi.dll" -Force
Write-Host "Copied privstack_ffi.dll to $dest"
# ── Build .NET desktop ────────────────────────────────────
- name: Publish .NET desktop
shell: pwsh
run: |
$outputDir = "dist/windows/publish"
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
dotnet publish desktop/PrivStack.Desktop/PrivStack.Desktop.csproj `
-c ${{ inputs.configuration }} `
-r win-x64 `
--self-contained true `
-p:PublishSingleFile=false `
-p:PublishReadyToRun=true `
-o $outputDir
# ── Install Inno Setup ────────────────────────────────────
- name: Install Inno Setup
shell: cmd
run: choco install innosetup -y --no-progress
# ── Create EXE installer ──────────────────────────────────
- name: Create EXE installer
shell: pwsh
run: |
$version = "${{ steps.version.outputs.APP_VERSION }}"
$sourceDir = "${{ github.workspace }}/dist/windows/publish"
$outputDir = "${{ github.workspace }}/dist/windows"
# Write Inno Setup script inline
$issContent = @"
#define AppVersion "$version"
#define SourceDir "$sourceDir"
#define OutputDir "$outputDir"
[Setup]
AppId={{A8F4B2C1-5D3E-4F6A-9B8C-7D2E1F0A3B5C}
AppName=PrivStack
AppVersion={#AppVersion}
AppVerName=PrivStack {#AppVersion}
AppPublisher=PrivStack
AppPublisherURL=https://privstack.io
AppSupportURL=https://privstack.io/support
AppUpdatesURL=https://privstack.io/download
DefaultDirName={autopf}\PrivStack
DefaultGroupName=PrivStack
AllowNoIcons=yes
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputDir={#OutputDir}
OutputBaseFilename=PrivStack-{#AppVersion}-Setup
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
UninstallDisplayIcon={app}\PrivStack.Desktop.exe
DisableProgramGroupPage=yes
DisableWelcomePage=no
CloseApplications=yes
RestartApplications=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.pdb,AppxManifest.xml"
[Icons]
Name: "{group}\PrivStack"; Filename: "{app}\PrivStack.Desktop.exe"
Name: "{group}\{cm:UninstallProgram,PrivStack}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\PrivStack"; Filename: "{app}\PrivStack.Desktop.exe"; Tasks: desktopicon
[Run]
Filename: "{app}\PrivStack.Desktop.exe"; Description: "{cm:LaunchProgram,PrivStack}"; Flags: nowait postinstall skipifsilent
"@
$issPath = "${{ github.workspace }}/installer.iss"
$issContent | Out-File -FilePath $issPath -Encoding UTF8
# Find Inno Setup compiler
$iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
if (-not (Test-Path $iscc)) {
$iscc = "C:\Program Files\Inno Setup 6\ISCC.exe"
}
Write-Host "Running Inno Setup..."
& $iscc $issPath
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed" }
$exePath = "$outputDir/PrivStack-$version-Setup.exe"
if (Test-Path $exePath) {
$size = [math]::Round((Get-Item $exePath).Length / 1MB, 1)
Write-Host "EXE installer created: ${size}MB"
}
# ── Sign with Azure Artifact Signing ──────────────────────
- name: Azure login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Sign EXE installer
uses: azure/trusted-signing-action@v0.5.1
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: privstack
certificate-profile-name: privstack-signing
files-folder: dist/windows
files-folder-filter: exe
file-digest: SHA256
# ── Deploy to PrivStack registry ──────────────────────────
- name: Deploy EXE to registry
if: ${{ inputs.skip_deploy != true }}
shell: bash
env:
PRIVSTACK_ADMIN_TOKEN: ${{ secrets.PRIVSTACK_ADMIN_TOKEN }}
PRIVSTACK_API_URL: https://privstack.io
run: |
VERSION="${{ steps.version.outputs.APP_VERSION }}"
EXE_PATH="dist/windows/PrivStack-${VERSION}-Setup.exe"
if [ ! -f "$EXE_PATH" ]; then
echo "ERROR: EXE not found at $EXE_PATH"
exit 1
fi
SIZE=$(wc -c < "$EXE_PATH" | tr -d ' ')
SIZE_MB=$((SIZE / 1024 / 1024))
echo "Uploading PrivStack-${VERSION}-Setup.exe (${SIZE_MB}MB)..."
HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/upload_response.txt \
-X POST "${PRIVSTACK_API_URL}/api/admin/releases/publish" \
-H "Authorization: Bearer ${PRIVSTACK_ADMIN_TOKEN}" \
-F "package=@${EXE_PATH}" \
-F "version=${VERSION}" \
-F "platform=windows" \
-F "arch=x64" \
-F "format=exe" \
-F "filename=PrivStack-${VERSION}-Setup.exe" \
)
BODY=$(cat /tmp/upload_response.txt)
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
echo "Deployed OK ($HTTP_CODE)"
else
echo "FAILED ($HTTP_CODE): $BODY"
exit 1
fi
# ── Upload artifact to GitHub (backup) ────────────────────
- name: Upload EXE artifact
uses: actions/upload-artifact@v4
with:
name: PrivStack-Windows-x64-${{ steps.version.outputs.APP_VERSION }}
path: dist/windows/PrivStack-${{ steps.version.outputs.APP_VERSION }}-Setup.exe
retention-days: 30
if-no-files-found: warn