Files
qelectrotech-source-mirror/.github/workflows/windows-msi.yml
T
Laurent Trinques b98589d3c7 CI(windows): drop Qt5 build, sign Qt6 MSI, freeze legacy Qt5 nightly assets
windows-build.yml:
- Remove the Qt5 build job (build-windows). Qt6 is now the sole
  Windows track built in CI.
- publish-nightly-assets: only delete/replace .exe and .zip assets
  tagged "qt6" on the nightly release. Assets without "qt6" in the
  name (the last Qt5 build ever published) are left untouched and
  stay downloadable indefinitely as a frozen legacy build.
- Release notes: drop the "Try Qt6 — soon the only track" notice,
  add a line explaining the Qt5 (frozen) vs Qt6 (maintained) split.

windows-msi.yml:
- Matrix reduced to the single "qt6" entry (flavor string kept as
  "qt6", not renamed, since it seeds the MSI ProductCode and a
  rename would break upgrade detection for existing installs).
- Sign the Qt6 MSI via SignPath (previously Qt5-only). Guard is now
  just the upstream-repo fork check; no per-flavor exclusion left.
- deploy-pages: also detect legacy (non-"qt6") release assets and
  pass them to generate-page.py as LEGACY_INSTALLER_URL /
  LEGACY_PORTABLE_URL / LEGACY_MSI_URL.

generate-page.py:
- Drop the old Qt5/Qt6 dual-track rendering; INSTALLER_URL /
  PORTABLE_URL / MSI_URL now point at the Qt6 build directly.
- Add an optional "Windows — x86_64 — Qt5 (legacy, unmaintained)"
  card, rendered only when LEGACY_* URLs are set, with a frozen/
  no-longer-updated notice.

Before merging: manually trigger the current (pre-merge) "Windows
Build" + "Windows MSI" workflows once to publish an up-to-date,
signed Qt5 snapshot — that run becomes the frozen legacy reference,
since the Qt5 job won't exist to re-run afterwards.

No changes to QElectroTech.wxs (Qt5/Qt6-agnostic, only
QtPlatformArgs varies and is already handled at the CI level).
2026-09-04 10:57:10 +02:00

430 lines
18 KiB
YAML

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
# Single-entry matrix kept on purpose (rather than flattening the job):
# matrix.flavor is used as part of the MSI ProductCode seed, so changing
# it would generate a new ProductCode and break upgrade detection for
# existing installs. Keeping "qt6" here preserves continuity.
matrix:
include:
- flavor: qt6
portable_artifact: qelectrotech-windows-portable-qt6
version_source: hardcoded # see note in "Extract version" step
label_suffix: "-qt6"
experimental: false
permissions:
contents: write
pages: write
id-token: write # Required by SignPath
outputs:
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
# 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 version 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"
# Qt platform argument: kept from the Qt5 era (QTBUG-83161
# font-rendering workaround, GDI backend). Qt6 uses DirectWrite by
# default and does not need it, so this always resolves empty now.
if ("${{ matrix.flavor }}" -eq "qt5") {
$qtArgs = "-platform windows:fontengine=freetype"
} else {
$qtArgs = ""
}
echo "QT_ARGS=$qtArgs" >> $env:GITHUB_OUTPUT
Write-Host "Qt platform args: '$qtArgs'"
# ----------------------------------------------------------------
# 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: |
$qtArgs = "${{ steps.version.outputs.QT_ARGS }}"
$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/`" $qtArgs`r`n"
[System.IO.File]::WriteAllText($bat, $content, [System.Text.Encoding]::ASCII)
Write-Host "Lancer QET.bat replaced for MSI installation (qtArgs: '$qtArgs')."
# ----------------------------------------------------------------
# 9. Build the MSI (unsigned at this stage — signing happens below)
# Qt6 keeps its own ProductCode seed (distinct from the retired Qt5
# MSI), so a machine that still has the old Qt5 MSI installed gets a
# separate, coexisting install rather than an unexpected upgrade.
# ----------------------------------------------------------------
- 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"
$qtArgs = "${{ steps.version.outputs.QT_ARGS }}"
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
# Qt6 is now signed too (previously excluded while Qt5 was the stable
# track and Qt6 was experimental-only). The remaining guard is the fork
# check: forks never have the SignPath secrets, and a fork-originated
# PR/run must never attempt a signing request.
# (cf. DieterMayerOSS:fix/msi-signing-fork-guard, d3f60c88)
- name: Sign MSI via SignPath
if: github.repository == 'qelectrotech/qelectrotech-source-mirror'
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: 40
if-no-files-found: error
- name: Delete old nightly .msi asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
$pattern = "-qt6.*\\.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 { $_ })) {
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"
echo "msi_name_qt6=$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 "Signed : true"
# ---------------------------------------------------------------------------
# Job 2 : Génère et déploie la page GitHub Pages une fois le MSI publié,
# 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 'qt6' | grep '\.exe$' | head -1)
ZIP_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1)
MSI_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.msi$' | head -1 || echo "")
# Legacy Qt5 assets (no "qt6" in the name): last ever published,
# frozen — windows-build.yml/windows-msi.yml no longer delete or
# replace these, so they keep pointing at the same files release
# after release. Rendered as a separate "legacy" section if present.
LEGACY_EXE_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.exe$' | head -1 || echo "")
LEGACY_ZIP_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.zip$' | head -1 || echo "")
LEGACY_MSI_NAME=$(echo "$ASSETS" | grep -v '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"
LEGACY_INSTALLER_URL=""
LEGACY_PORTABLE_URL=""
LEGACY_MSI_URL=""
[ -n "$LEGACY_EXE_NAME" ] && LEGACY_INSTALLER_URL="$BASE/$LEGACY_EXE_NAME"
[ -n "$LEGACY_ZIP_NAME" ] && LEGACY_PORTABLE_URL="$BASE/$LEGACY_ZIP_NAME"
[ -n "$LEGACY_MSI_NAME" ] && LEGACY_MSI_URL="$BASE/$LEGACY_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
export LEGACY_INSTALLER_URL LEGACY_PORTABLE_URL LEGACY_MSI_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