Files
spoopyghosty0--frameport/.github/workflows/build.yml
T
spoopyghosty0 6bfaaf788f Site: What's new page and section from GitHub releases
/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.
2026-10-10 16:41:42 -04:00

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