From 14d4aa772b59996facb4ac98992b7221a3cf2cbf Mon Sep 17 00:00:00 2001 From: Laurent Trinques Date: Sat, 13 Jun 2026 09:59:17 +0200 Subject: [PATCH] Update windows-msi.yml --- .github/workflows/windows-msi.yml | 178 ++++++++++++++++++------------ 1 file changed, 108 insertions(+), 70 deletions(-) diff --git a/.github/workflows/windows-msi.yml b/.github/workflows/windows-msi.yml index e82279a9c..735bc1e2b 100644 --- a/.github/workflows/windows-msi.yml +++ b/.github/workflows/windows-msi.yml @@ -18,7 +18,6 @@ jobs: build-msi: name: Build MSI with WiX v7 runs-on: windows-latest - # Only runs if Windows Build succeeded (or triggered manually) if: > github.event_name == 'workflow_dispatch' || @@ -27,10 +26,9 @@ jobs: permissions: contents: write pages: write - id-token: write + id-token: write # Required by SignPath steps: - # ---------------------------------------------------------------- # 1. Checkout (to retrieve QElectroTech.wxs and sources) # ---------------------------------------------------------------- @@ -41,12 +39,16 @@ jobs: # ---------------------------------------------------------------- # 2. Download the portable artifact from the main build + # Requires windows-build.yml to upload an artifact named + # "qelectrotech-windows-portable" (fixed name) # ---------------------------------------------------------------- - name: Download portable artifact uses: actions/download-artifact@v4 with: name: qelectrotech-windows-portable path: artifact\files + # workflow_run => use the triggering run's ID + # workflow_dispatch => use input run_id if provided, otherwise current run run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.repository }} @@ -58,10 +60,12 @@ jobs: id: version shell: pwsh run: | + # Version from qetversion.cpp (same logic as windows-build.yml) $src = Get-Content "sources\qetversion.cpp" -Raw -ErrorAction SilentlyContinue if ($src -match 'return QVersionNumber\{([^}]+)\}') { $ver = $Matches[1] -replace '\s','' -replace ',','.' } else { + # Fallback: CMakeLists.txt $cmake = Get-Content "CMakeLists.txt" -Raw if ($cmake -match 'project\s*\([^)]*VERSION\s+([\d]+\.[\d]+\.[\d]+)') { $ver = $Matches[1] @@ -69,41 +73,48 @@ jobs: $ver = "0.0.0" } } + + # Numeric MSI version: 4 digits required (e.g. 0.100.1.0) $verMsi = "$ver.0" + + # Short SHA for the display version $sha = git rev-parse --short HEAD 2>$null if (-not $sha) { $sha = "unknown" } + + # Cumulative revision number (same calculation as windows-build.yml) $count = git rev-list HEAD --count 2>$null $rev = [int]$count + 473 - $verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64" - # Generate a unique ProductCode GUID from the commit SHA - # This ensures MajorUpgrade always triggers, even for same-version builds - $fullSha = git rev-parse HEAD 2>$null - if (-not $fullSha) { $fullSha = [System.Guid]::NewGuid().ToString() } - $bytes = [System.Text.Encoding]::UTF8.GetBytes($fullSha) - $md5 = [System.Security.Cryptography.MD5]::Create().ComputeHash($bytes) - $guidBytes = [byte[]]$md5[0..15] - $productGuid = [System.Guid]::new($guidBytes).ToString().ToUpper() - echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT + $verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64" + + echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT - echo "PRODUCT_GUID=$productGuid" >> $env:GITHUB_OUTPUT Write-Host "Version MSI : $verMsi" Write-Host "Version display : $verDisplay" - Write-Host "Product GUID : $productGuid" # ---------------------------------------------------------------- # 4. Install WiX v7, accept EULA and install WixUI extension + # All done in one step: PATH is updated within the same step + # so wix is immediately available for eula and extension commands # ---------------------------------------------------------------- - name: Install WiX v7 shell: pwsh run: | dotnet tool install --global wix --version 7.0.0 + + # Update PATH immediately for the rest of this step $toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools') $env:PATH = "$toolsPath;$env:PATH" + + # Also export for subsequent steps echo $toolsPath >> $env:GITHUB_PATH + + # Accept OSMF EULA (official CI/CD method: writes a sentinel file) wix eula accept wix7 + + # Install WixUI extension wix extension add WixToolset.UI.wixext/7.0.0 - wix extension add WixToolset.Util.wixext/7.0.0 + Write-Host "WiX v7 installed, EULA accepted, UI extension added." # ---------------------------------------------------------------- @@ -115,6 +126,8 @@ jobs: $wxs = "build-aux\windows\QElectroTech.wxs" if (-not (Test-Path $wxs)) { Write-Error "WXS file not found: $wxs" + Write-Host "Contents of build-aux\windows\ :" + Get-ChildItem "build-aux\windows\" -ErrorAction SilentlyContinue exit 1 } Write-Host "WXS found: $wxs" @@ -129,8 +142,10 @@ jobs: Get-ChildItem -Path "artifact\files" -Depth 2 | Select-Object FullName | Format-Table -AutoSize + # Search for qelectrotech.exe in the artifact $exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1 if (-not $exe) { + # Also try QElectroTech.exe (capital Q) $exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1 } if (-not $exe) { @@ -138,30 +153,40 @@ jobs: exit 1 } Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)" - $binDir = $exe.Directory.FullName - $filesDir = Split-Path $binDir -Parent + + # FilesDir = folder containing bin\ + $binDir = $exe.Directory.FullName + $filesDir = Split-Path $binDir -Parent echo "FILES_DIR=$filesDir" >> $env:GITHUB_ENV Write-Host "FILES_DIR: $filesDir" # ---------------------------------------------------------------- # 7. Convert LICENSE (GPL-2) to RTF for the WixUI licence screen + # RTF is the only format accepted by Windows Installer. + # The conversion wraps plain text lines in basic RTF markup. # ---------------------------------------------------------------- - name: Convert LICENSE to RTF shell: pwsh run: | $licSrc = "LICENSE" $licRtf = "$env:TEMP\License.rtf" + if (-not (Test-Path $licSrc)) { Write-Error "LICENSE file not found in repository root" exit 1 } + $lines = Get-Content $licSrc -Encoding UTF8 + + # RTF header — Courier New, 9pt, black $rtf = New-Object System.Text.StringBuilder [void]$rtf.AppendLine('{\rtf1\ansi\ansicpg1252\deff0') [void]$rtf.AppendLine('{\fonttbl{\f0\fmodern\fprq1\fcharset0 Courier New;}}') [void]$rtf.AppendLine('{\colortbl;\red0\green0\blue0;}') [void]$rtf.AppendLine('\f0\fs18\cf1') + foreach ($line in $lines) { + # Escape RTF special characters $escaped = $line ` -replace '\\', '\\\\' ` -replace '\{', '\{' ` @@ -169,30 +194,29 @@ jobs: [void]$rtf.AppendLine("$escaped\par") } [void]$rtf.AppendLine('}') + [System.IO.File]::WriteAllText($licRtf, $rtf.ToString(), [System.Text.Encoding]::ASCII) echo "LICENSE_RTF=$licRtf" >> $env:GITHUB_ENV Write-Host "License.rtf generated: $licRtf ($([math]::Round((Get-Item $licRtf).Length/1KB,1)) KB)" # ---------------------------------------------------------------- - # 8. Remove Lancer QET.bat from the artifact - # The MSI does not use the .bat: shortcuts point directly to - # qelectrotech.exe, and elements\ is set read-only via a - # CustomAction in QElectroTech.wxs. - # The .bat is kept as-is in the ZIP portable build. + # 8. Replace Lancer QET.bat with the MSI-specific version + # The portable version uses relative paths suited for the zip. + # The MSI version uses %~dp0 to resolve paths relative to + # the installation directory in Program Files. # ---------------------------------------------------------------- - - name: Remove Lancer QET.bat from artifact + - name: Replace Lancer QET.bat for MSI shell: pwsh run: | - $bat = "$env:FILES_DIR\Lancer QET.bat" - if (Test-Path $bat) { - Remove-Item $bat -Force - Write-Host "Lancer QET.bat removed from artifact (MSI uses direct exe shortcut)." - } else { - Write-Host "Lancer QET.bat not found in artifact (already absent)." - } + $bat = "$env:FILES_DIR\Lancer QET.bat" + $content = "@echo off`r`nstart `"`" `"%~dp0bin\qelectrotech.exe`" --common-elements-dir=`"%~dp0elements/`" --common-tbt-dir=`"%~dp0titleblocks/`" --lang-dir=`"%~dp0lang/`" -style windowsvista`r`n" + [System.IO.File]::WriteAllText($bat, $content, [System.Text.Encoding]::ASCII) + Write-Host "Lancer QET.bat replaced for MSI installation." + Write-Host "=== Content of new Lancer QET.bat ===" + Get-Content $bat # ---------------------------------------------------------------- - # 9. Build the MSI + # 9. Build the MSI (unsigned) # ---------------------------------------------------------------- - name: Build MSI shell: pwsh @@ -213,57 +237,61 @@ jobs: Write-Host " LicenseRtf : $licRtf" Write-Host " Output : dist\$outputName" - $productGuid = "${{ steps.version.outputs.PRODUCT_GUID }}" - wix build $wxs ` -arch x64 ` -d "Version=$version" ` -d "ProductVersion=$verDisplay" ` - -d "ProductCode=$productGuid" ` -d "FilesDir=$filesDir" ` -d "LicenseRtf=$licRtf" ` -ext WixToolset.UI.wixext ` - -ext WixToolset.Util.wixext ` -o "dist\$outputName" if (-not (Test-Path "dist\$outputName")) { Write-Error "MSI not generated: dist\$outputName" exit 1 } + $size = [math]::Round((Get-Item "dist\$outputName").Length / 1MB, 1) Write-Host "MSI generated: dist\$outputName ($size MB) ✓" echo "MSI_NAME=$outputName" >> $env:GITHUB_ENV # ---------------------------------------------------------------- - # 10. Sign the MSI with SignPath + # 9b. Upload unsigned MSI as artifact (required by SignPath) + # SignPath fetches artifacts directly from GitHub Actions, + # so the MSI must be uploaded before the signing request. # ---------------------------------------------------------------- - - name: Install SignPath PowerShell module - shell: pwsh - run: | - Install-Module -Name SignPath -Force -Scope CurrentUser - - - name: Sign MSI with SignPath - shell: pwsh - run: | - $msi = Get-ChildItem "$env:GITHUB_WORKSPACE\dist\*.msi" | Select-Object -First 1 - if (-not $msi) { - Write-Error "No .msi found in dist/" - exit 1 - } - Write-Host "Signing: $($msi.FullName)" - Submit-SigningRequest ` - -InputArtifactPath $msi.FullName ` - -ApiToken "${{ secrets.SIGNPATH_API_TOKEN }}" ` - -OrganizationId "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" ` - -ProjectSlug "MSI" ` - -SigningPolicySlug "test-signing" ` - -OutputArtifactPath $msi.FullName ` - -Force ` - -WaitForCompletion - Write-Host "Signing complete: $($msi.Name)" + - name: Upload unsigned MSI artifact (pre-signing) + uses: actions/upload-artifact@v4 + with: + name: qelectrotech-windows-msi-unsigned + path: dist\*.msi + retention-days: 1 + if-no-files-found: error # ---------------------------------------------------------------- - # 11. Upload the MSI artifact + # 9c. Submit signing request to SignPath (OSS organization) + # Prerequisites: + # - SIGNPATH_API_TOKEN : CI user token from the OSS org + # - SIGNPATH_ORGANIZATION_ID : Organization ID of the OSS org + # (visible in app.signpath.io → Settings after accepting + # the OSS invitation) + # The action downloads the signed MSI and places it in + # dist/ (overwriting the unsigned one). + # ---------------------------------------------------------------- + - name: Sign MSI via SignPath + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} + project-slug: 'qelectrotech' + signing-policy-slug: 'release-signing' + artifact-configuration-slug: 'msi' + github-artifact-name: 'qelectrotech-windows-msi-unsigned' + wait-for-completion: true + output-artifact-directory: 'dist\' + + # ---------------------------------------------------------------- + # 10. Upload the signed MSI artifact # ---------------------------------------------------------------- - name: Upload MSI artifact uses: actions/upload-artifact@v4 @@ -284,9 +312,9 @@ jobs: gh release view nightly --repo "$REPO" --json assets \ --jq '.assets[] | select(.name | test("\\.msi$")) | .name' \ | while read -r name; do - echo "Deleting old asset: $name" - gh release delete-asset nightly "$name" --repo "$REPO" --yes - done + echo "Deleting old asset: $name" + gh release delete-asset nightly "$name" --repo "$REPO" --yes + done echo "Old .msi assets deleted." shell: bash @@ -307,18 +335,20 @@ jobs: shell: pwsh run: | Write-Host "=== MSI build summary ===" - Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}" - Write-Host "WiX : v7.0.0" - Write-Host "Runner image : ${{ runner.os }} / ${{ runner.arch }}" + Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}" + Write-Host "WiX : v7.0.0" + Write-Host "Signing : SignPath OSS" + Write-Host "Runner : ${{ runner.os }} / ${{ runner.arch }}" if (Test-Path "dist\$env:MSI_NAME") { $size = [math]::Round((Get-Item "dist\$env:MSI_NAME").Length / 1MB, 1) - Write-Host "MSI : $env:MSI_NAME ($size MB) ✓" + Write-Host "MSI : $env:MSI_NAME ($size MB) ✓" } else { - Write-Host "MSI : FAILED ✗" + Write-Host "MSI : FAILED ✗" } # ---------------------------------------------------------------- # 13. Generate and deploy the GitHub Pages download page + # Toutes les URLs sont connues ici (exe, zip, msi). # ---------------------------------------------------------------- - name: Checkout (for generate-page.py) uses: actions/checkout@v4 @@ -334,23 +364,31 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail + REPO="${{ github.repository }}" + + # Fetch asset names from the nightly release (source of truth) ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name') + EXE_NAME=$(echo "$ASSETS" | grep '\.exe$' | head -1) ZIP_NAME=$(echo "$ASSETS" | grep '\.zip$' | head -1) MSI_NAME=$(echo "$ASSETS" | grep '\.msi$' | head -1 || echo "") + BASE="https://github.com/$REPO/releases/download/nightly" INSTALLER_URL="$BASE/$EXE_NAME" PORTABLE_URL="$BASE/$ZIP_NAME" MSI_URL="" [ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME" + SHA="${{ github.event.workflow_run.head_sha || github.sha }}" SHORT="${SHA:0:7}" DATE=$(date -u '+%Y-%m-%d %H:%M UTC') RUN_URL="https://github.com/$REPO/actions/runs/${{ github.run_id }}" RUN_NUMBER="${{ github.run_number }}" + export DATE SHORT REPO SHA RUN_URL RUN_NUMBER export INSTALLER_URL PORTABLE_URL MSI_URL + python3 source/build-aux/generate-page.py - name: Add .nojekyll