Skip to content

CI

CI #21

Workflow file for this run

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