Skip to content

License gate: enforce only in direct-download builds #34

License gate: enforce only in direct-download builds

License gate: enforce only in direct-download builds #34

Workflow file for this run

name: Build
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install dependencies
run: python -m pip install --upgrade pip && pip install -r requirements-dev.txt
- name: Run tests
run: python -m pytest tests/ -v
# Direct-download builds (macOS .dmg) enforce the Paddle license gate by
# bundling this flag file; the Windows leg omits it so the Microsoft Store
# MSIX is not double-gated. See app/config.py:LICENSE_REQUIRED.
- name: Enable license gate for direct-download build
if: matrix.os == 'macos-latest'
run: touch app/resources/license_required.flag
- name: Build with PyInstaller
run: pyinstaller packaging/build_exe.spec --noconfirm --distpath dist --workpath build
# On macOS, package the .app bundle with ditto. This preserves the
# executable bit, the symlinked Qt frameworks, and PyInstaller's ad-hoc
# code signature — all of which a plain cross-platform zip (or an
# artifact round-trip through Windows) destroys. arm64 macOS refuses to
# launch an app whose signature is invalid, so this native archive is
# the file to distribute to end users. macos-latest runners are Apple
# Silicon, so this is an arm64 build.
- name: Package macOS app (ditto)
if: matrix.os == 'macos-latest'
run: ditto -c -k --keepParent dist/EasyPostDesktop.app "dist/EasyPostDesktop-macOS-arm64.zip"
# Sign with a Developer ID cert + hardened runtime, notarize with Apple,
# staple, and produce a distributable .dmg. Entirely gated on the Apple
# signing secrets below: until they exist this is a no-op and the ad-hoc
# ditto zip above stays the macOS artifact. Add these repo secrets once
# enrolled in the Apple Developer Program:
# MACOS_CERTIFICATE_P12_BASE64 base64 of the Developer ID Application .p12
# MACOS_CERTIFICATE_PASSWORD password for that .p12
# MACOS_SIGN_IDENTITY e.g. "Developer ID Application: Name (TEAMID)"
# APPLE_ID Apple ID email used for notarization
# APPLE_APP_PASSWORD app-specific password for that Apple ID
# APPLE_TEAM_ID 10-character Apple Developer Team ID
- name: Sign, notarize & package macOS (.dmg)
if: matrix.os == 'macos-latest' && env.HAS_MACOS_SIGNING == 'true'
env:
HAS_MACOS_SIGNING: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 != '' }}
MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
APP="dist/EasyPostDesktop.app"
DMG="dist/EasyPostDesktop-macOS-arm64.dmg"
# 1) Import the Developer ID cert into a throwaway keychain.
KEYCHAIN="$RUNNER_TEMP/build.keychain"
KEYCHAIN_PW="$(uuidgen)"
security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
echo "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PW" "$KEYCHAIN" >/dev/null
security list-keychains -d user -s "$KEYCHAIN"
# 2) Codesign the app: hardened runtime, secure timestamp, entitlements.
codesign --force --deep --options runtime --timestamp \
--entitlements packaging/entitlements.mac.plist \
--sign "$MACOS_SIGN_IDENTITY" "$APP"
codesign --verify --strict --verbose=2 "$APP"
# 3) Build a compressed .dmg and sign it.
hdiutil create -volname "Easy-Post Desktop" -srcfolder "$APP" -ov -format UDZO "$DMG"
codesign --force --timestamp --sign "$MACOS_SIGN_IDENTITY" "$DMG"
# 4) Notarize and staple.
xcrun notarytool submit "$DMG" \
--apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
security delete-keychain "$KEYCHAIN"
# Packages dist/EasyPostDesktop/ (the PyInstaller output above) into
# dist/EasyPostDesktop.msix for Microsoft Store submission via Partner
# Center — see packaging/build_msix.py and README.
- name: Build MSIX package
if: matrix.os == 'windows-latest'
run: python packaging\build_msix.py
# MSIX/AppX packages must carry *some* signature to be structurally
# valid, but the Microsoft Store re-signs with its own certificate
# during publishing — a self-signed one is accepted for submission, so
# this generates and discards a throwaway cert per run rather than
# needing a purchased cert or any stored secret. Uses the same
# sign-only script (packaging/sign_msix.ps1) as local upload signing:
# it builds the throwaway cert in managed .NET (CertificateRequest),
# which avoids New-SelfSignedCertificate/certreq entirely. See:
# https://learn.microsoft.com/windows/apps/publish/publish-your-app/msix/app-package-requirements
- name: Sign MSIX package
if: matrix.os == 'windows-latest'
shell: pwsh
run: pwsh -NoProfile -File packaging\sign_msix.ps1
# Unsigned builds still trigger SmartScreen/Gatekeeper warnings on first
# run regardless of this step — only a paid code-signing certificate
# (see README) actually removes that. This just gives users a way to
# verify the download wasn't tampered with in transit.
- name: Generate SHA256 checksums
shell: python
run: |
import hashlib
from pathlib import Path
dist = Path("dist")
lines = []
for path in sorted(dist.rglob("*")):
if path.is_file():
digest = hashlib.sha256(path.read_bytes()).hexdigest()
lines.append(f"{digest} {path.relative_to(dist)}")
Path("dist/SHA256SUMS.txt").write_text("\n".join(lines) + "\n")
# No-op unless WINDOWS_CODE_SIGNING_CERT_BASE64/_PASSWORD secrets are
# set on the repo. Wire these up if a certificate is purchased later
# (see README's "Windows SmartScreen warning" section) — everything
# else in this workflow already works either way.
- name: Sign Windows build
if: matrix.os == 'windows-latest' && env.HAS_SIGNING_CERT == 'true'
env:
HAS_SIGNING_CERT: ${{ secrets.WINDOWS_CODE_SIGNING_CERT_BASE64 != '' }}
shell: pwsh
run: |
$bytes = [Convert]::FromBase64String("${{ secrets.WINDOWS_CODE_SIGNING_CERT_BASE64 }}")
[IO.File]::WriteAllBytes("cert.pfx", $bytes)
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object FullName -Descending | Select-Object -First 1
& $signtool.FullName sign /f cert.pfx /p "${{ secrets.WINDOWS_CODE_SIGNING_CERT_PASSWORD }}" `
/fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
dist\EasyPostDesktop\EasyPostDesktop.exe
Remove-Item cert.pfx
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: EasyPostDesktop-${{ matrix.os }}
path: dist/*