CI #21
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| # Release tags in this repo have no "v" prefix, e.g. 1.16.0. The leading | |
| # digit keeps non-release tags like converter-bundle from triggering a build. | |
| tags: ['[0-9]*'] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| test: | |
| name: Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt -r requirements-dev.txt | |
| - name: Run unit tests | |
| run: python -m pytest -q | |
| - name: Smoke-test the GUI headlessly | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libegl1 libxkbcommon-x11-0 libxcb-cursor0 \ | |
| libxcb-icccm4 libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0 | |
| python - <<'PY' | |
| import sys | |
| sys.path.insert(0, "src") | |
| # Import every module so syntax/import errors fail the build, then | |
| # build the main window offscreen to catch Qt wiring regressions. | |
| from PyQt6.QtWidgets import QApplication | |
| import gui_main | |
| from gui_main import WhisperGUI | |
| for m in ("check_hardware_optimization", "check_yt_dlp_version", "check_app_update"): | |
| setattr(WhisperGUI, m, lambda self: None) | |
| WhisperGUI.check_and_setup_dependencies = lambda self: True | |
| app = QApplication([]) | |
| win = WhisperGUI() | |
| win.resize(1250, 820) | |
| win.show() | |
| app.processEvents() | |
| assert win.tabs.count() > 0, "no tabs were created" | |
| assert win._required_tab_bar_width(), "tab bar measurement failed" | |
| # These only run in frozen builds, so nothing else exercises them. | |
| sys.frozen = True | |
| win._apply_tab_minimums() | |
| win._enforce_splitter_sizes_for_frozen() | |
| win.resize(1300, 840) | |
| app.processEvents() | |
| print(f"OK: {win.tabs.count()} tabs, window {win.width()}x{win.height()}") | |
| PY | |
| build: | |
| name: Windows build | |
| runs-on: windows-latest | |
| needs: test | |
| outputs: | |
| unsigned-artifact-id: ${{ steps.unsigned.outputs.artifact-id }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| pip install pyinstaller | |
| - name: Build executable | |
| run: pyinstaller --noconfirm --clean faster-whisper-xxl-gui.spec | |
| - name: Verify the packaged app starts | |
| shell: pwsh | |
| timeout-minutes: 5 | |
| run: | | |
| # Builds the real main window inside the packaged exe. Catches an | |
| # `excludes` entry that removed something still needed, which would | |
| # otherwise only show up as a crash on a user's machine. | |
| # The selftest defaults to the offscreen plugin, so only the second | |
| # pass loads qwindows.dll and whatever it pulls in behind it. | |
| foreach ($platform in @("offscreen", "windows")) { | |
| $env:QT_QPA_PLATFORM = $platform | |
| Remove-Item "dist/selftest.log" -ErrorAction SilentlyContinue | |
| $p = Start-Process -FilePath "dist/faster-whisper-xxl-gui.exe" ` | |
| -ArgumentList "--selftest" -Wait -PassThru | |
| if (Test-Path "dist/selftest.log") { Get-Content "dist/selftest.log" } | |
| if ($p.ExitCode -ne 0) { | |
| throw "Packaged app failed to start under QT_QPA_PLATFORM=$platform (exit $($p.ExitCode)). A PyInstaller exclude probably removed something needed." | |
| } | |
| Write-Host "Packaged app starts cleanly under $platform." | |
| } | |
| # SignPath pulls the artifact back out of GitHub, so the unsigned exe has | |
| # to be uploaded on its own first. Nothing else may ride along: the | |
| # artifact configuration describes one executable inside the zip that | |
| # upload-artifact produces, and a stray file will not match it. | |
| - name: Upload the unsigned executable | |
| id: unsigned | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: faster-whisper-xxl-gui-unsigned | |
| path: dist/faster-whisper-xxl-gui.exe | |
| # Long enough to re-run signing on a later day without a rebuild. | |
| retention-days: 7 | |
| # Kept out of the build job: release signing waits on a human in the SignPath | |
| # UI, and a missed approval should not cost another PyInstaller run. | |
| sign: | |
| name: Sign and package | |
| runs-on: windows-latest | |
| needs: build | |
| permissions: | |
| contents: read | |
| # The signing action reads the job details and downloads the uploaded | |
| # artifact back through the GitHub API. | |
| actions: read | |
| env: | |
| # Signed on tags, and on a manual run so the pipeline can be rehearsed | |
| # without cutting a release. Ordinary pushes are left alone: a signing | |
| # request per commit would spend the Foundation's certificate for nothing. | |
| SIGNING: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} | |
| steps: | |
| - name: Sign the executable | |
| if: env.SIGNING == 'true' | |
| uses: signpath/github-action-submit-signing-request@v2 | |
| with: | |
| api-token: ${{ secrets.SIGNPATH_API_TOKEN }} | |
| organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} | |
| project-slug: Faster-Whisper-XXL-GUI | |
| # Tags reach users and must carry the Foundation's release | |
| # certificate. Manual runs use the test certificate, which chains to | |
| # an untrusted root, so choosing the policy from the trigger rather | |
| # than by hand keeps a test signature out of a published release. | |
| signing-policy-slug: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || 'test-signing' }} | |
| github-artifact-id: ${{ needs.build.outputs.unsigned-artifact-id }} | |
| wait-for-completion: true | |
| # Release signing waits on manual approval, and the request only | |
| # appears once the build is done, so the default 600 is easy to miss. | |
| wait-for-completion-timeout-in-seconds: ${{ startsWith(github.ref, 'refs/tags/') && '3600' || '600' }} | |
| output-artifact-directory: signed | |
| - name: Fetch the unsigned build | |
| if: env.SIGNING != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: faster-whisper-xxl-gui-unsigned | |
| path: signed | |
| # After signing, never before. The signature changes the file, so a hash | |
| # taken earlier would not describe what people actually download. | |
| - name: Report size and checksum | |
| shell: pwsh | |
| run: | | |
| $exe = "signed/faster-whisper-xxl-gui.exe" | |
| $bytes = (Get-Item $exe).Length | |
| $mb = [math]::Round($bytes / 1MB, 1) | |
| Write-Host "Size: $bytes bytes ($mb MiB)" | |
| # Microsoft's false-positive submission portal caps uploads at 50 MB. | |
| if ($bytes -gt 50000000) { | |
| Write-Warning "Over the 50 MB limit of Microsoft's submission portal." | |
| } else { | |
| Write-Host "Under the 50 MB submission limit." | |
| } | |
| $hash = (Get-FileHash $exe -Algorithm SHA256).Hash.ToLower() | |
| "$hash faster-whisper-xxl-gui.exe" | Out-File -Encoding ascii signed/faster-whisper-xxl-gui.exe.sha256 | |
| Write-Host "SHA256: $hash" | |
| - name: Verify the signature | |
| if: env.SIGNING == 'true' | |
| shell: pwsh | |
| run: | | |
| $sig = Get-AuthenticodeSignature "signed/faster-whisper-xxl-gui.exe" | |
| Write-Host "Status: $($sig.Status)" | |
| Write-Host "Signer: $($sig.SignerCertificate.Subject)" | |
| if ($sig.Status -eq 'NotSigned') { | |
| throw "SignPath returned a file with no signature on it." | |
| } | |
| # The test certificate chains to a root Windows does not trust, so on | |
| # a manual run anything other than NotSigned means the pipeline works. | |
| # A tag ships to users and has to chain properly. | |
| if ($sig.Status -ne 'Valid' -and $env:GITHUB_REF -like 'refs/tags/*') { | |
| throw "Release signature is not trusted: $($sig.Status)" | |
| } | |
| # Manual runs sign with the test certificate. Publishing that as an | |
| # artifact would leave a binary nobody should run sitting in the Actions | |
| # tab of a public repository, so it stays on the runner and dies with it. | |
| - uses: actions/upload-artifact@v4 | |
| if: github.event_name != 'workflow_dispatch' | |
| with: | |
| name: faster-whisper-xxl-gui-windows | |
| path: | | |
| signed/faster-whisper-xxl-gui.exe | |
| signed/faster-whisper-xxl-gui.exe.sha256 | |
| retention-days: 30 | |
| release: | |
| name: Publish release | |
| runs-on: ubuntu-latest | |
| needs: sign | |
| if: startsWith(github.ref, 'refs/tags/') | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: faster-whisper-xxl-gui-windows | |
| path: dist | |
| - uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| dist/faster-whisper-xxl-gui.exe | |
| dist/faster-whisper-xxl-gui.exe.sha256 | |
| draft: true | |
| generate_release_notes: true |