mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-21 12:01:36 +02:00
/changelog/ shows every release's What's new part (the same parts the app's changelog shows): the dev build as Coming next, the latest release large, then a timeline; the home page shows the latest changes. Release jobs restart the pages workflow so a new release appears on the site.
240 lines
11 KiB
YAML
240 lines
11 KiB
YAML
name: build
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
tags: ["v*"]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
inputs:
|
|
dev:
|
|
description: "Publish the bundles as the dev build (testers: Settings → Updates → Install the latest dev build)"
|
|
type: boolean
|
|
default: true
|
|
notes:
|
|
description: "What testers should try (shown before they install it)"
|
|
type: string
|
|
default: ""
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: astral-sh/setup-uv@v10.2.0
|
|
- run: uv sync --extra dev
|
|
- name: Lint
|
|
run: uv run ruff check .
|
|
- run: uv run pytest -m "not games and not device"
|
|
|
|
bundle:
|
|
needs: test
|
|
if: github.event_name != 'push' || startsWith(github.ref, 'refs/tags/v') # pushes to main: tests only
|
|
permissions:
|
|
contents: read
|
|
id-token: write # build provenance attestations (Sigstore)
|
|
attestations: write
|
|
env:
|
|
PYTHONIOENCODING: utf-8
|
|
PYTHONUTF8: "1"
|
|
FLET_CLI_NO_RICH_OUTPUT: "1"
|
|
HAS_CERT: ${{ secrets.WINDOWS_CODESIGN_PFX != '' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: windows-latest
|
|
archive: FramePort-windows-x64.zip
|
|
- os: macos-26 # Apple Silicon + Xcode 26 (macos-15's Xcode 16.4 fails a Flutter plugin; macos-latest
|
|
# went unassigned on 2026-10-02)
|
|
archive: FramePort-macos-arm64.zip
|
|
- os: ubuntu-22.04 # oldest supported: the bundle needs the build host's GLib/glibc or newer
|
|
archive: FramePort-linux-x64.tar.gz
|
|
- os: ubuntu-22.04-arm # Linux ARM64 (same base as x64)
|
|
archive: FramePort-linux-arm64.tar.gz
|
|
runs-on: ${{ matrix.os }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: astral-sh/setup-uv@v10.2.0
|
|
- name: Linux build deps (GTK for Flutter desktop)
|
|
if: runner.os == 'Linux'
|
|
run: sudo apt-get update && sudo apt-get install -y ninja-build libgtk-3-dev
|
|
- run: uv sync --extra dev
|
|
- name: Dev build version (next patch + .dev<run number>; not committed)
|
|
if: github.event_name == 'workflow_dispatch' && inputs.dev
|
|
run: uv run --no-project python scripts/dev_version.py ${{ github.run_number }}
|
|
- id: flet
|
|
run: uv run python scripts/package.py
|
|
continue-on-error: ${{ runner.os == 'macOS' || runner.arch == 'ARM64' }}
|
|
- name: macOS / Linux ARM64 fallback (PyInstaller bundle)
|
|
if: (runner.os == 'macOS' || runner.arch == 'ARM64') && steps.flet.outcome == 'failure'
|
|
run: uv run --with pyinstaller python scripts/package.py --pyinstaller
|
|
|
|
# ---- signing: self-signed certificate on Windows, ad-hoc on macOS ----
|
|
- name: Sign Windows binaries (self-signed certificate)
|
|
if: runner.os == 'Windows' && env.HAS_CERT == 'true'
|
|
shell: pwsh
|
|
env:
|
|
PFX: ${{ secrets.WINDOWS_CODESIGN_PFX }}
|
|
PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
|
|
run: |
|
|
$pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"
|
|
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:PFX))
|
|
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
|
|
Sort-Object FullName -Descending | Select-Object -First 1
|
|
# sign our executables/DLLs; leave files that already carry a signature (e.g. Microsoft runtime DLLs)
|
|
$files = @(Get-ChildItem dist\windows -Recurse -Include *.exe,*.dll |
|
|
Where-Object { (Get-AuthenticodeSignature $_.FullName).Status -eq 'NotSigned' })
|
|
Write-Host "signing $($files.Count) file(s)"
|
|
for ($i = 0; $i -lt $files.Count; $i += 40) {
|
|
$batch = $files[$i..([Math]::Min($i + 39, $files.Count - 1))].FullName
|
|
& $signtool.FullName sign /fd SHA256 /f $pfx /p $env:PFX_PASSWORD /tr http://timestamp.digicert.com /td SHA256 $batch
|
|
if ($LASTEXITCODE) { throw "signtool failed" }
|
|
}
|
|
Remove-Item $pfx
|
|
Get-AuthenticodeSignature dist\windows\FramePort.exe | Format-List Status, SignerCertificate
|
|
- name: Ad-hoc sign the macOS app
|
|
if: runner.os == 'macOS'
|
|
run: |
|
|
app=$(find dist -maxdepth 3 -name "*.app" -type d | head -1)
|
|
# Re-signing replaces the signature, entitlements included: pass them again, or the file picker refuses to
|
|
# open ("ENTITLEMENT_NOT_FOUND", GitHub #63). flet build writes them to Release.entitlements; the
|
|
# PyInstaller fallback has none, so it gets the same set.
|
|
ent=build/flutter/macos/Runner/Release.entitlements
|
|
if [ ! -f "$ent" ]; then
|
|
ent="$RUNNER_TEMP/FramePort.entitlements"
|
|
cat > "$ent" <<'EOF'
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0"><dict>
|
|
<key>com.apple.security.cs.allow-jit</key><true/>
|
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
|
|
<key>com.apple.security.network.client</key><true/>
|
|
<key>com.apple.security.network.server</key><true/>
|
|
<key>com.apple.security.files.user-selected.read-write</key><true/>
|
|
</dict></plist>
|
|
EOF
|
|
fi
|
|
codesign --force --deep --sign - --entitlements "$ent" "$app"
|
|
codesign --verify --deep --strict --verbose=2 "$app"
|
|
codesign -d --entitlements - "$app" 2>/dev/null | tee /dev/stderr | grep -q "files.user-selected.read-write"
|
|
|
|
# ---- package (archives keep permissions/symlinks) + provenance ----
|
|
- name: Package (Windows)
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: Compress-Archive -Path dist\windows\* -DestinationPath ${{ matrix.archive }}
|
|
- name: Package (macOS)
|
|
if: runner.os == 'macOS'
|
|
run: ditto -c -k --keepParent "$(find dist -maxdepth 3 -name '*.app' -type d | head -1)" ${{ matrix.archive }}
|
|
- name: Package (Linux)
|
|
if: runner.os == 'Linux'
|
|
run: | # flet build -> dist/linux; the PyInstaller fallback -> dist/FramePort (one folder)
|
|
if [ -d dist/linux ]; then tar czf ${{ matrix.archive }} -C dist --transform 's,^linux,FramePort,' linux
|
|
else tar czf ${{ matrix.archive }} -C dist FramePort; fi
|
|
# self-update with this archive: unpack, verify the layout (+ the signer on Windows), run the real swap script
|
|
- name: Update smoke test
|
|
run: uv run python scripts/update_smoke.py ${{ matrix.archive }}
|
|
- name: Attest build provenance
|
|
if: github.event_name != 'pull_request'
|
|
uses: actions/attest-build-provenance@v4
|
|
with:
|
|
subject-path: ${{ matrix.archive }}
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ${{ matrix.archive }}
|
|
path: ${{ matrix.archive }}
|
|
|
|
release:
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
needs: bundle
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
actions: write # starts the pages workflow
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: astral-sh/setup-uv@v10.2.0
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: out
|
|
merge-multiple: true
|
|
- name: Wheel (CLI install via uv tool / pipx / pip, and its self-update)
|
|
run: uv build --wheel -o out && ls out
|
|
- name: Videos (docs/media, rendered by the showcase workflow; frameport-<name>.mp4 → FramePort-<name>.mp4)
|
|
run: |
|
|
for f in docs/media/frameport-*.mp4; do
|
|
[ -f "$f" ] || continue
|
|
n=$(basename "$f" .mp4); cp "$f" "out/FramePort-${n#frameport-}.mp4"
|
|
done
|
|
- name: Checksums
|
|
run: cd out && sha256sum * > SHA256SUMS.txt && cat SHA256SUMS.txt
|
|
- name: Publish release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
tag="${GITHUB_REF_NAME}"
|
|
notes=$(mktemp)
|
|
# "What's new" = the annotated tag's message (FramePort's update dialog shows it); checkout may leave the
|
|
# tag lightweight, so fetch the tag object itself
|
|
git fetch --force origin "refs/tags/${tag}:refs/tags/${tag}"
|
|
whatsnew=$(git tag -l --format='%(contents:body)' "$tag")
|
|
[ -n "$whatsnew" ] || whatsnew=$(git tag -l --format='%(contents:subject)' "$tag")
|
|
{ echo "FramePort ${tag}: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64)."; echo;
|
|
if [ -n "$whatsnew" ] && [ "$(git cat-file -t "$tag")" = tag ]; then
|
|
echo "## What's new"; echo; echo "$whatsnew"; echo; fi
|
|
echo "Already have FramePort? It offers this update itself (Library → **Update now**)."
|
|
echo; cat packaging/release-footer.md; } > "$notes" # short: the update dialog shows these notes
|
|
gh release delete "$tag" --yes 2>/dev/null || true
|
|
gh release create "$tag" out/* packaging/FramePort-selfsigned.cer --title "FramePort ${tag}" --notes-file "$notes"
|
|
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: gh workflow run pages.yml --ref main
|
|
|
|
dev-release:
|
|
# "Run workflow" with dev ticked: replaces the rolling `dev` pre-release (never shown by the automatic update check;
|
|
# testers install it from Settings → Updates). Same assets + SHA256SUMS as a release.
|
|
if: github.event_name == 'workflow_dispatch' && inputs.dev
|
|
needs: bundle
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
actions: write # starts the pages workflow
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
- uses: astral-sh/setup-uv@v10.2.0
|
|
- id: version
|
|
run: echo "version=$(uv run --no-project python scripts/dev_version.py ${{ github.run_number }})" >> "$GITHUB_OUTPUT"
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: out
|
|
merge-multiple: true
|
|
- name: Wheel
|
|
run: uv build --wheel -o out && ls out
|
|
- name: Checksums
|
|
run: cd out && sha256sum * > SHA256SUMS.txt && cat SHA256SUMS.txt
|
|
- name: Publish the dev build
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
NOTES: ${{ inputs.notes }}
|
|
run: |
|
|
notes=$(mktemp)
|
|
last=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)
|
|
{ echo "Dev build **${VERSION}** from commit ${GITHUB_SHA::7} ($(date -u +%Y-%m-%d)). For testing fixes before"
|
|
echo "the next release; you get that release as a normal update."; echo
|
|
if [ -n "$NOTES" ]; then echo "## Please test"; echo; echo "$NOTES"; echo; fi
|
|
if [ -n "$last" ]; then echo "## Changes since ${last}"; echo
|
|
git log --no-merges --format='- %s' "${last}..HEAD" | cut -c1-160 | head -40; fi; } > "$notes"
|
|
gh release delete dev --yes --cleanup-tag 2>/dev/null || true
|
|
gh release create dev out/* packaging/FramePort-selfsigned.cer --prerelease --target "$GITHUB_SHA" \
|
|
--title "FramePort dev build ${VERSION}" --notes-file "$notes"
|
|
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: gh workflow run pages.yml --ref main
|