name: Build Windows # Windows-only build for the self-hosted org runner labelled "windows-latest". # Produces dist/macropad.exe as a build artifact, and on a version tag (v*) # creates/updates a Gitea release and attaches the exe. # # Notes for self-hosted Windows runners: # - Uses `shell: powershell` (Windows PowerShell 5.1); `pwsh`/PowerShell 7 is # not assumed to be installed. # - actions/setup-python is avoided (its Windows tool-cache install hangs on # self-hosted runners); Python 3.11 is used if present, else silently # installed per-user. on: workflow_dispatch: push: tags: - 'v*' jobs: # Fast smoke test so a broken runner fails in seconds, not minutes. preflight: runs-on: windows-latest timeout-minutes: 5 steps: - name: Runner check shell: powershell run: | Write-Host "Runner OK on $env:COMPUTERNAME" Write-Host "OS: $([System.Environment]::OSVersion.VersionString)" Write-Host "PowerShell: $($PSVersionTable.PSVersion)" $py = Get-Command py -ErrorAction SilentlyContinue if ($py) { & py -3.11 --version } else { Write-Host "No py launcher; build will self-install Python 3.11." } build-windows: needs: [preflight] runs-on: windows-latest timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 - name: Ensure Python 3.11 shell: powershell run: | $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Persistent, side-effect-free Python for this builder. We use a # standalone CPython from nuget (no registry/PATH changes, idempotent) # cached under LOCALAPPDATA so subsequent runs reuse it. A pre-existing # Python 3.11 on PATH is honoured if present. $pyVersion = "3.11.9" $root = Join-Path $env:LOCALAPPDATA "MacroPadBuild" $python = Join-Path $root "python.$pyVersion\tools\python.exe" if (-not (Test-Path $python)) { # Honour an existing 3.11 on PATH (e.g. if the VM is pre-provisioned). try { $v = & python --version 2>&1 if ($LASTEXITCODE -eq 0 -and "$v" -match '3\.11') { $python = (& python -c "import sys; print(sys.executable)").Trim() } } catch {} } if (-not (Test-Path $python)) { Write-Host "Provisioning standalone Python $pyVersion via nuget..." New-Item -ItemType Directory -Force -Path $root | Out-Null $nuget = Join-Path $root "nuget.exe" if (-not (Test-Path $nuget)) { Invoke-WebRequest -UseBasicParsing -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile $nuget } & $nuget install python -Version $pyVersion -OutputDirectory $root -Source "https://api.nuget.org/v3/index.json" -NonInteractive if ($LASTEXITCODE -ne 0) { throw "nuget install python exited with $LASTEXITCODE" } $python = Join-Path $root "python.$pyVersion\tools\python.exe" } if (-not (Test-Path $python)) { throw "Python not found at '$python'" } & $python -m ensurepip --upgrade 2>$null | Out-Null & $python --version # Write to GITHUB_ENV without a BOM (ascii) so the value parses cleanly. Add-Content -Path $env:GITHUB_ENV -Value "PYTHON=$python" -Encoding ascii - name: Install dependencies shell: powershell run: | $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Native command failures do NOT trip $ErrorActionPreference, so check # $LASTEXITCODE explicitly — otherwise a broken install ships silently. & $env:PYTHON -m pip install --upgrade pip if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed ($LASTEXITCODE)" } & $env:PYTHON -m pip install -e . if ($LASTEXITCODE -ne 0) { throw "pip install -e . failed ($LASTEXITCODE)" } & $env:PYTHON -m pip install pyinstaller if ($LASTEXITCODE -ne 0) { throw "pip install pyinstaller failed ($LASTEXITCODE)" } - name: Verify runtime imports shell: powershell run: | # Fail fast if any runtime dependency is missing before we bundle. & $env:PYTHON -c "import PySide6.QtWidgets, fastapi, uvicorn, aiohttp, PIL, pystray, qrcode, pyautogui, pyperclip, websockets, multipart; print('deps OK')" if ($LASTEXITCODE -ne 0) { throw "dependency import smoke test failed ($LASTEXITCODE)" } - name: Build Windows executable shell: powershell run: | $ErrorActionPreference = 'Stop' & $env:PYTHON -m PyInstaller --noconfirm macropad.spec - name: Verify build output shell: powershell run: | if (-not (Test-Path dist/macropad.exe)) { throw "Build failed: dist/macropad.exe not found" } Get-Item dist/macropad.exe | Format-List Name, Length, LastWriteTime - name: Upload build artifact # v3 uses the artifact protocol Gitea supports; v4+ requires a backend # Gitea (reported as GHES) does not provide. uses: actions/upload-artifact@v3 with: name: macropad-windows path: dist/macropad.exe if-no-files-found: error - name: Publish release asset (tags only) if: startsWith(github.ref, 'refs/tags/') shell: powershell env: GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $api = $env:GITHUB_API_URL $repo = $env:GITHUB_REPOSITORY $tag = $env:GITHUB_REF_NAME $headers = @{ Authorization = "token $env:GITEA_TOKEN" } # Create the release for this tag, or fetch it if it already exists. $body = @{ tag_name = $tag; name = $tag; draft = $false; prerelease = $false } | ConvertTo-Json try { $rel = Invoke-RestMethod -Method Post -Uri "$api/repos/$repo/releases" ` -Headers $headers -ContentType 'application/json' -Body $body } catch { $rel = Invoke-RestMethod -Method Get -Uri "$api/repos/$repo/releases/tags/$tag" -Headers $headers } # Attach the executable (replace a prior asset of the same name). $existing = $rel.assets | Where-Object { $_.name -eq 'macropad.exe' } if ($existing) { Invoke-RestMethod -Method Delete -Headers $headers ` -Uri "$api/repos/$repo/releases/$($rel.id)/assets/$($existing.id)" } Invoke-RestMethod -Method Post -Headers $headers ` -Uri "$api/repos/$repo/releases/$($rel.id)/assets?name=macropad.exe" ` -InFile dist/macropad.exe -ContentType 'application/octet-stream' Write-Host "Attached macropad.exe to release $tag"