name: 'Flutter Build Windows' description: 'Builds Windows Flutter desktop apps, compiles Inno Setup (.exe) and MSIX (.msix) installers, signs updates for WinSparkle (DSA), and generates release metadata.' inputs: working_directory: description: 'Path to the Flutter app package directory (e.g., packages/sfw_desktop)' required: false default: '.' arch: description: 'Target Windows architecture (x64 or arm64)' required: false default: 'x64' build_args: description: 'Extra arguments passed to flutter build windows (e.g., --dart-define=...)' required: false default: '' build_inno_setup: description: 'Whether to compile an Inno Setup installer (.exe)' required: false default: 'true' inno_setup_script: description: 'Path to Inno Setup .iss script relative to working_directory' required: false default: 'windows/installer/inno_setup.iss' output_dir: description: 'Output directory for compiled Inno Setup installer' required: false default: 'dist' exe_filename: description: 'Expected installer filename for metadata JSON (e.g., slides-for-worship-windows-x64-setup.exe)' required: false default: '' build_msix: description: 'Whether to build an MSIX package using msix:create' required: false default: 'true' msix_output_name: description: 'Output base name for the MSIX package (e.g., slides_for_worship_x64)' required: false default: '' pfx_base64: description: 'Base64-encoded Windows Authenticode .pfx certificate' required: false default: '' pfx_password: description: 'Password for the Windows .pfx certificate' required: false default: '' sparkle_dsa_private_key: description: 'WinSparkle DSA private key content (PEM) for signing installer' required: false default: '' sparkle_dsa_key_file: description: 'Fallback path to WinSparkle DSA private key file in repository' required: false default: 'signing/sparkle/dsa_priv.pem' generate_metadata_json: description: 'Whether to generate windows__metadata.json and windows_signature.txt' required: false default: 'false' outputs: version: description: 'Resolved package version from pubspec.yaml' value: ${{ steps.build.outputs.version }} exe_path: description: 'Path to compiled Inno Setup .exe installer' value: ${{ steps.package_meta.outputs.exe_path }} msix_path: description: 'Path to generated .msix installer' value: ${{ steps.package_meta.outputs.msix_path }} signature: description: 'WinSparkle DSA signature if generated' value: ${{ steps.package_meta.outputs.signature }} metadata_path: description: 'Path to generated windows__metadata.json' value: ${{ steps.package_meta.outputs.metadata_path }} runs: using: 'composite' steps: - name: Configure Windows Tooling PATH shell: powershell run: | $toolPaths = @( "C:\Program Files\Git\bin", "C:\Program Files\Git\usr\bin", "C:\Git\bin", "C:\Git\usr\bin", "C:\Users\rodyd\AppData\Local\Programs\Inno Setup 6", "C:\Program Files (x86)\Inno Setup 6", "C:\Program Files\Inno Setup 6", "C:\flutter\bin" ) foreach ($p in $toolPaths) { if (Test-Path $p) { $env:PATH = "$p;$env:PATH" if (Test-Path env:GITHUB_PATH) { [System.IO.File]::AppendAllText($env:GITHUB_PATH, "$p`r`n") } } } - name: Decode Windows PFX Certificate if: inputs.pfx_base64 != '' shell: powershell env: WINDOWS_PFX_BASE64: ${{ inputs.pfx_base64 }} run: | if ($env:WINDOWS_PFX_BASE64) { $pfx_cert_byte = [System.Convert]::FromBase64String($env:WINDOWS_PFX_BASE64) [IO.File]::WriteAllBytes("cert.pfx", $pfx_cert_byte) Write-Output "Decoded cert.pfx" } - name: Build Windows Release id: build shell: bash env: WORK_DIR: ${{ inputs.working_directory }} ARCH: ${{ inputs.arch }} BUILD_ARGS: ${{ inputs.build_args }} run: | ROOT_DIR="$(pwd)" VERSION=$(grep '^version: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r') echo "version=$VERSION" >> "$GITHUB_OUTPUT" cd "$WORK_DIR" if [ "$ARCH" = "arm64" ]; then # shellcheck disable=SC2086 flutter build windows --release --target-platform=windows-arm64 $BUILD_ARGS else # shellcheck disable=SC2086 flutter build windows --release $BUILD_ARGS fi cd "$ROOT_DIR" - name: Compile Inno Setup Installer if: inputs.build_inno_setup == 'true' shell: powershell env: WORK_DIR: ${{ inputs.working_directory }} ARCH: ${{ inputs.arch }} ISS_REL: ${{ inputs.inno_setup_script }} OUT_DIR: ${{ inputs.output_dir }} VERSION: ${{ steps.build.outputs.version }} run: | New-Item -ItemType Directory -Force -Path $env:OUT_DIR | Out-Null $sourceRel = Join-Path $env:WORK_DIR "build\windows\$($env:ARCH)\runner\Release" if (-not (Test-Path $sourceRel)) { $sourceRel = Join-Path $env:WORK_DIR "build\windows\x64\runner\Release" } $sourceDir = (Resolve-Path $sourceRel).Path $outputDir = (Resolve-Path $env:OUT_DIR).Path $issScript = Join-Path $env:WORK_DIR $env:ISS_REL if (-not (Test-Path $issScript)) { Write-Warning "Inno Setup script not found at $issScript; skipping Inno Setup compilation." exit 0 } $iscc = (Get-Command iscc.exe -ErrorAction SilentlyContinue).Source if (-not $iscc -and (Test-Path "C:\Users\rodyd\AppData\Local\Programs\Inno Setup 6\ISCC.exe")) { $iscc = "C:\Users\rodyd\AppData\Local\Programs\Inno Setup 6\ISCC.exe" } if ($iscc) { & $iscc /DMyAppVersion="$($env:VERSION)" /DAppArch="$($env:ARCH)" /DSourceDir="$sourceDir" /DOutputDir="$outputDir" "$issScript" } else { Write-Warning "ISCC.exe not found on runner; skipping Inno Setup installer." } - name: Create MSIX Package if: inputs.build_msix == 'true' shell: bash env: WORK_DIR: ${{ inputs.working_directory }} ARCH: ${{ inputs.arch }} MSIX_NAME: ${{ inputs.msix_output_name }} PFX_PASS: ${{ inputs.pfx_password }} run: | ROOT_DIR="$(pwd)" OUT_NAME="${MSIX_NAME:-slides_for_worship_${ARCH}}" cd "$WORK_DIR" if [ -f "$ROOT_DIR/cert.pfx" ]; then flutter pub run msix:create --certificate-path "$ROOT_DIR/cert.pfx" --certificate-password "$PFX_PASS" --install-certificate false --output-name "$OUT_NAME" else flutter pub run msix:create --install-certificate false --output-name "$OUT_NAME" fi cd "$ROOT_DIR" - name: Sign for WinSparkle & Generate Metadata id: package_meta shell: bash env: WORK_DIR: ${{ inputs.working_directory }} ARCH: ${{ inputs.arch }} OUT_DIR: ${{ inputs.output_dir }} VERSION: ${{ steps.build.outputs.version }} EXE_FILENAME_INPUT: ${{ inputs.exe_filename }} SPARKLE_DSA_KEY: ${{ inputs.sparkle_dsa_private_key }} SPARKLE_DSA_FILE: ${{ inputs.sparkle_dsa_key_file }} GEN_META: ${{ inputs.generate_metadata_json }} run: | EXE_FILE=$(find "$OUT_DIR" -name "*.exe" 2>/dev/null | head -n 1 || true) MSIX_FILE=$(find "$WORK_DIR/build/windows" -name "*.msix" 2>/dev/null | head -n 1 || true) echo "exe_path=$EXE_FILE" >> "$GITHUB_OUTPUT" echo "msix_path=$MSIX_FILE" >> "$GITHUB_OUTPUT" SETUP_FILE="${EXE_FILE:-$MSIX_FILE}" if [ -z "$SPARKLE_DSA_KEY" ] && [ -n "$SPARKLE_DSA_FILE" ] && [ -f "$SPARKLE_DSA_FILE" ]; then SPARKLE_DSA_KEY=$(cat "$SPARKLE_DSA_FILE") fi WIN_SIG="" if [ -n "$SPARKLE_DSA_KEY" ] && [ -n "$SETUP_FILE" ] && [ -f "$SETUP_FILE" ]; then echo "$SPARKLE_DSA_KEY" > dsa_priv.pem echo "Signing $SETUP_FILE with DSA key..." WIN_SIG=$(openssl dgst -sha1 -binary < "$SETUP_FILE" | openssl dgst -sha1 -sign dsa_priv.pem | openssl enc -base64 | tr -d '\n\r' || true) if [ -n "$WIN_SIG" ]; then echo "$WIN_SIG" > windows_signature.txt echo "signature=$WIN_SIG" >> "$GITHUB_OUTPUT" fi rm -f dsa_priv.pem fi if [ "$GEN_META" = "true" ]; then SIZE="0" if [ -n "$EXE_FILE" ] && [ -f "$EXE_FILE" ]; then SIZE=$(stat -c%s "$EXE_FILE" 2>/dev/null || stat -f%z "$EXE_FILE" 2>/dev/null || echo "0") fi if [ -n "$EXE_FILENAME_INPUT" ]; then FILENAME="$EXE_FILENAME_INPUT" elif [ -n "$EXE_FILE" ]; then FILENAME=$(basename "$EXE_FILE") else FILENAME="slides-for-worship-windows-${ARCH}-setup.exe" fi META_FILE="windows_${ARCH}_metadata.json" cat < "$META_FILE" { "version": "$VERSION", "platform": "windows", "arch": "$ARCH", "filename": "$FILENAME", "size": $SIZE, "signature": "$WIN_SIG", "signatureType": "dsaSignature" } EOF cat "$META_FILE" echo "metadata_path=$META_FILE" >> "$GITHUB_OUTPUT" fi - name: Clean up PFX Certificate if: always() shell: powershell run: | if (Test-Path "cert.pfx") { Remove-Item -Force "cert.pfx" -ErrorAction SilentlyContinue }