name: Windows MSI (WiX v7) on: # Triggered automatically after a successful Windows build on master workflow_run: workflows: ["Windows Build"] types: [completed] branches: [master] # Manual trigger still available (e.g. for a specific run_id) workflow_dispatch: inputs: run_id: description: "Run ID of 'Windows Build' (leave empty for automatic)" required: false default: "" jobs: build-msi: name: Build MSI with WiX v7 (${{ matrix.flavor }}) runs-on: windows-latest # Only runs if Windows Build succeeded (or triggered manually) if: > github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' strategy: fail-fast: false matrix: include: - flavor: qt5 portable_artifact: qelectrotech-windows-portable version_source: qetversion # parsed from sources/qetversion.cpp label_suffix: "" experimental: false - flavor: qt6 portable_artifact: qelectrotech-windows-portable-qt6 version_source: hardcoded # see note in "Extract version" step label_suffix: "-qt6-EXPERIMENTAL" experimental: true permissions: contents: write pages: write id-token: write # Required by SignPath outputs: qt5_msi: ${{ steps.export.outputs.msi_name_qt5 }} qt6_msi: ${{ steps.export.outputs.msi_name_qt6 }} steps: # ---------------------------------------------------------------- # 1. Checkout (to retrieve QElectroTech.wxs and sources) # ---------------------------------------------------------------- - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 # ---------------------------------------------------------------- # 2. Download the portable artifact for this flavor # ---------------------------------------------------------------- - name: Download portable artifact uses: actions/download-artifact@v8 with: name: ${{ matrix.portable_artifact }} 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 }} # ---------------------------------------------------------------- # 3. Extract version # Qt5: parsed from sources/qetversion.cpp (single source of truth # for the software's own reported version). # Qt6: hardcoded "0.200.1" here — deliberately NOT parsed from the # binary/source, since Qt-version self-detection inside the # compiled code proved unreliable (see commit e1aa65f). The CI # matrix entry already knows for certain which flavor it is # packaging, so the experimental-track label is set explicitly # at the packaging level instead. # ---------------------------------------------------------------- - name: Extract version id: version shell: pwsh run: | if ("${{ matrix.version_source }}" -eq "hardcoded") { $ver = "0.200.1" } else { $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] } else { $ver = "0.0.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 # MSI Version only compares Major.Minor.Build (the 4th field is # ignored by Windows Installer for upgrade detection), so we # inject rev into Build to force a real increase between # nightlies and trigger MajorUpgrade's automatic uninstall. $verParts = $ver -split '\.' $verMsi = "$($verParts[0]).$($verParts[1]).$($rev % 65535)" $flavorTag = "${{ matrix.label_suffix }}" $verDisplay = "${ver}${flavorTag}-r${rev}-${sha}_x86_64-win64" echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT Write-Host "Version MSI : $verMsi" Write-Host "Version display : $verDisplay" # ---------------------------------------------------------------- # 4. Install WiX v7, accept EULA and install WixUI extension # ---------------------------------------------------------------- - name: Install WiX v7 shell: pwsh run: | dotnet tool install --global wix --version 7.0.0 $toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools') $env:PATH = "$toolsPath;$env:PATH" echo $toolsPath >> $env:GITHUB_PATH wix eula accept wix7 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 + Util extensions added." # ---------------------------------------------------------------- # 5. Check that the WXS file exists in the repository # ---------------------------------------------------------------- - name: Check WXS file shell: pwsh run: | $wxs = "build-aux\windows\QElectroTech.wxs" if (-not (Test-Path $wxs)) { Write-Error "WXS file not found: $wxs" exit 1 } Write-Host "WXS found: $wxs" # ---------------------------------------------------------------- # 6. Check the artifact structure and locate files/ # ---------------------------------------------------------------- - name: Check artifact structure shell: pwsh run: | $exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1 if (-not $exe) { $exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1 } if (-not $exe) { Write-Error "qelectrotech.exe not found in artifact" exit 1 } Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)" $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 # ---------------------------------------------------------------- - 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 = 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) { $escaped = $line ` -replace '\\', '\\\\' ` -replace '\{', '\{' ` -replace '\}', '\}' [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" # ---------------------------------------------------------------- # 8. Replace Lancer QET.bat with the MSI-specific version # ---------------------------------------------------------------- - name: Replace Lancer QET.bat for MSI shell: pwsh run: | $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." # ---------------------------------------------------------------- # 9. Build the MSI (unsigned) # Qt6 (experimental) uses a distinct ProductCode seed so it never # collides with / upgrades over the Qt5 MSI — they must be able to # coexist as clearly separate installs. # ---------------------------------------------------------------- - name: Build MSI shell: pwsh run: | $version = "${{ steps.version.outputs.VERSION_MSI }}" $verDisplay = "${{ steps.version.outputs.VERSION_DISPLAY }}" $filesDir = $env:FILES_DIR $licRtf = $env:LICENSE_RTF $wxs = "build-aux\windows\QElectroTech.wxs" $outputName = "QElectroTech-${verDisplay}.msi" $seed = "qelectrotech-msi-${{ matrix.flavor }}-$version" $sha1 = [System.Security.Cryptography.SHA1]::Create() $hash = $sha1.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($seed)) $hash[6] = ($hash[6] -band 0x0F) -bor 0x50 $hash[8] = ($hash[8] -band 0x3F) -bor 0x80 $productCode = "{$([System.Guid]::new([byte[]]$hash[0..15]).ToString().ToUpper())}" New-Item -ItemType Directory -Force -Path "dist" | Out-Null Write-Host "=== wix build (${{ matrix.flavor }}) ===" Write-Host " Version : $version" Write-Host " ProductCode : $productCode" Write-Host " Output : dist\$outputName" if ("${{ matrix.flavor }}" -eq "qt5") { $qtArgs = "-platform windows:fontengine=freetype" } else { $qtArgs = "" } wix build $wxs ` -arch x64 ` -d "Version=$version" ` -d "ProductVersion=$verDisplay" ` -d "ProductCode=$productCode" ` -d "FilesDir=$filesDir" ` -d "LicenseRtf=$licRtf" ` -d QtPlatformArgs="$qtArgs" ` -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) OK" echo "MSI_NAME=$outputName" >> $env:GITHUB_ENV - name: Upload unsigned MSI artifact (pre-signing) id: upload_unsigned uses: actions/upload-artifact@v7 with: name: qelectrotech-windows-msi-unsigned-${{ matrix.flavor }} path: dist\*.msi retention-days: 1 if-no-files-found: error - name: Sign MSI via SignPath uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: 'qelectrotech-source-mirror' signing-policy-slug: 'test-signing' artifact-configuration-slug: 'MSI' github-artifact-id: ${{ steps.upload_unsigned.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: 'dist\' - name: Upload signed MSI artifact uses: actions/upload-artifact@v7 with: name: qelectrotech-windows-msi-${{ matrix.flavor }} path: dist\*.msi retention-days: 14 if-no-files-found: error - name: Delete old nightly .msi asset for this flavor env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} run: | $pattern = if ("${{ matrix.flavor }}" -eq "qt6") { "-qt6-EXPERIMENTAL.*\.msi$" } else { "\.msi$" } $names = gh release view nightly --repo $env:REPO --json assets --jq ".assets[] | select(.name | test(`"$pattern`")) | .name" foreach ($name in ($names -split "`n" | Where-Object { $_ })) { if ("${{ matrix.flavor }}" -eq "qt5" -and $name -match "-qt6-EXPERIMENTAL") { continue } Write-Host "Deleting old asset: $name" gh release delete-asset nightly $name --repo $env:REPO --yes } shell: pwsh - name: Upload MSI to nightly release uses: softprops/action-gh-release@v3 with: tag_name: nightly files: dist/*.msi fail_on_unmatched_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Export MSI name for page generation id: export shell: pwsh run: | $name = "$env:MSI_NAME" if ("${{ matrix.flavor }}" -eq "qt6") { echo "msi_name_qt6=$name" >> $env:GITHUB_OUTPUT } else { echo "msi_name_qt5=$name" >> $env:GITHUB_OUTPUT } - name: Summary if: always() shell: pwsh run: | Write-Host "=== MSI build summary (${{ matrix.flavor }}) ===" Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}" Write-Host "Experimental : ${{ matrix.experimental }}" # --------------------------------------------------------------------------- # Job 2 : Génère et déploie la page GitHub Pages une fois les DEUX MSI # (Qt5 + Qt6) publiés, pour que toutes les URLs soient connues. # --------------------------------------------------------------------------- deploy-pages: needs: build-msi runs-on: ubuntu-latest if: always() && needs.build-msi.result == 'success' permissions: contents: write pages: write id-token: write steps: - name: Checkout master (for generate-page.py) uses: actions/checkout@v7 with: ref: master path: source sparse-checkout: build-aux/generate-page.py sparse-checkout-cone-mode: false - name: Generate download page (index.html) shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail REPO="${{ github.repository }}" ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name') EXE_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.exe$' | head -1) ZIP_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.zip$' | head -1) MSI_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.msi$' | head -1 || echo "") EXE_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.exe$' | head -1 || echo "") ZIP_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1 || echo "") MSI_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | 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" INSTALLER_QT6_URL="" PORTABLE_QT6_URL="" MSI_QT6_URL="" [ -n "$EXE_QT6_NAME" ] && INSTALLER_QT6_URL="$BASE/$EXE_QT6_NAME" [ -n "$ZIP_QT6_NAME" ] && PORTABLE_QT6_URL="$BASE/$ZIP_QT6_NAME" [ -n "$MSI_QT6_NAME" ] && MSI_QT6_URL="$BASE/$MSI_QT6_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 export INSTALLER_QT6_URL PORTABLE_QT6_URL MSI_QT6_URL python3 source/build-aux/generate-page.py - name: Add .nojekyll shell: bash run: touch gh-pages/.nojekyll - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@v5 with: path: gh-pages/ - name: Deploy to GitHub Pages uses: actions/deploy-pages@v4