Build Windows (Signed + Deploy) #22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |