Skip to content

Ship the 1.2.8 store notes, and correct two claims that were wrong #221

Ship the 1.2.8 store notes, and correct two claims that were wrong

Ship the 1.2.8 store notes, and correct two claims that were wrong #221

Workflow file for this run

name: Build
on:
push:
branches: [main]
# The marketing site and prose changes cannot affect the binaries, and a
# macOS leg costs ~10 minutes plus a notarization wait. store_assets holds
# only Partner Center listing artifacts (screenshots, listing CSVs, the
# import builder) — the app bundles app/resources, never store_assets — so
# editing it must not fire the matrix or, worse, mint another App Store
# build off github.run_number. Skip them all.
paths-ignore:
- "site/**"
- "store_assets/**"
- "**/*.md"
- "LICENSE"
# No paths-ignore here, deliberately. `build (macos-latest)` and
# `build (windows-latest)` are required status checks on main, and a workflow
# that never triggers reports nothing at all — so a pull request touching only
# site/ or a .md file would sit BLOCKED for ever, waiting on checks that can
# never arrive. Filtering paths on push is fine; pushes are not gated.
#
# The cost is a build on documentation-only pull requests. That is acceptable
# now that the publishing steps below are guarded: a pull-request build
# compiles and tests, and signs, notarizes or uploads nothing.
pull_request:
workflow_dispatch:
inputs:
locale:
description: "UI languages for store screenshots, comma-separated (e.g. en,fr,de,ja)"
required: false
default: "en"
screenshots_only:
description: "Render screenshots without building or uploading anything"
type: boolean
required: false
default: false
jobs:
# A dispatch aimed at screenshots must not run the build matrix. That matrix
# signs, notarizes and uploads a Mac App Store package, so every dispatch mints
# a build off github.run_number — capturing seven locales would have burned
# seven build numbers and seven notarization waits to produce no binary anyone
# wanted. The screenshots job needs neither signing nor packaging.
build:
if: ${{ github.event.inputs.screenshots_only != 'true' }}
permissions:
id-token: write # OIDC federated-credential exchange for azure/login
contents: read
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
# Backstop for the whole job. GitHub's default is 6 hours, which is far
# longer than anything here should legitimately take.
timeout-minutes: 90
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
# The MCP bridge is direct-download only. The Windows leg builds the
# Store MSIX from the same tree, so the flag is written after the MSIX
# is packaged rather than here -- see "Enable MCP for the direct-download
# Windows build" further down.
- name: Enable MCP bridge for the macOS build
if: matrix.os == 'macos-latest'
run: touch app/resources/mcp_supported.flag
# The Windows leg's FIRST PyInstaller build feeds the Store MSIX, so the
# Store-variant flag must exist now, before that build. It is removed
# again before the direct-download rebuild further down. store_build.flag
# makes app/config.STORE_BUILD true, gating production behind the Store
# "Production unlock" add-on (see app/core/store_entitlement.py).
- name: Mark the Windows Store build
if: matrix.os == 'windows-latest'
shell: bash
run: touch app/resources/store_build.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
# Before notarization, not after: a bundle missing its flags must fail
# here rather than after half an hour in Apple's queue.
- name: Verify variant flags survived packaging (macOS)
if: matrix.os == 'macos-latest'
run: bash packaging/verify_variant_flags.sh dist/EasyPostDesktop.app
# Never on a pull request. Notarization is a paid round-trip to Apple that
# tells a reviewer nothing a plain build does not, and the Windows signing
# step below has excluded pull requests all along — this only makes the
# two consistent.
- name: Sign, notarize & package macOS (.dmg)
if: matrix.os == 'macos-latest' && env.HAS_MACOS_SIGNING == 'true' && github.event_name != 'pull_request'
# Hard ceiling on the whole step. Run 29849490422 sat here for 1h58m:
# the DMG uploaded fine, then `notarytool --wait` silently retried a
# dead runner network until it gave up. Without this the job would
# have run to GitHub's 6-hour default.
timeout-minutes: 45
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.
#
# Submit and wait are deliberately separate calls. `submit --wait`
# prints the submission id only to stdout as it goes, so when the
# wait died mid-poll the id was buried in a 1000-line log and the
# build looked like it had simply hung. Capturing the id first means
# it is always recorded and always queryable afterwards, whatever
# happens to the polling.
AUTH=(--apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID")
SUBMISSION_ID="$(
xcrun notarytool submit "$DMG" "${AUTH[@]}" --no-wait --output-format json \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'
)"
echo "Notarization submission id: $SUBMISSION_ID"
{
echo "### macOS notarization"
echo "- Submission id: \`$SUBMISSION_ID\`"
echo "- Query later: \`Notarization status\` workflow, or"
echo " \`xcrun notarytool info $SUBMISSION_ID --apple-id … --team-id …\`"
} >> "$GITHUB_STEP_SUMMARY"
# Bounded wait. Apple normally answers within minutes; 30m is
# generous without letting a stalled poll eat the whole job.
if ! xcrun notarytool wait "$SUBMISSION_ID" "${AUTH[@]}" --timeout 30m; then
echo "::error::Notarization did not reach Accepted within the timeout."
echo "Fetching whatever Apple has recorded for $SUBMISSION_ID:"
xcrun notarytool info "$SUBMISSION_ID" "${AUTH[@]}" || true
xcrun notarytool log "$SUBMISSION_ID" "${AUTH[@]}" || true
exit 1
fi
# A rejected submission still "completes", so check the verdict and
# print Apple's log, which names the exact offending binary.
STATUS="$(xcrun notarytool info "$SUBMISSION_ID" "${AUTH[@]}" --output-format json \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')"
echo "Notarization status: $STATUS"
if [ "$STATUS" != "Accepted" ]; then
echo "::error::Notarization returned $STATUS. Apple's log follows."
xcrun notarytool log "$SUBMISSION_ID" "${AUTH[@]}" || true
exit 1
fi
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
security delete-keychain "$KEYCHAIN"
# Mac App Store package: build the sandboxed .app with the MAS variant
# flag, sign it with the Apple Distribution + 3rd Party Mac Developer
# Installer certs, wrap it in a signed .pkg (productbuild), and upload to
# App Store Connect. Entirely gated on the MAS signing secrets: until they
# exist this is a no-op and the App Store build stays a manual Mac task.
# See CI-MAS-SETUP.md for how to add these repo secrets:
# MAS_CERTIFICATE_P12_BASE64 base64 of a .p12 holding BOTH the Apple
# Distribution and 3rd Party Mac Developer
# Installer certs (+ their private keys)
# MAS_CERTIFICATE_PASSWORD password for that .p12
# MAS_PROVISION_PROFILE_BASE64 base64 of the Mac App Store .provisionprofile
# MAS_SIGN_APP_IDENTITY "Apple Distribution: NAME (TEAMID)"
# MAS_SIGN_INSTALLER_IDENTITY "3rd Party Mac Developer Installer: NAME (TEAMID)"
# Upload authenticates with an App Store Connect API key (key
# 4CU796U485, App Manager). It replaced an Admin key that CI never used
# and that sat in cloud-synced storage; App Manager is the least privilege
# that can still manage versions, metadata and submissions.
#
# The Apple ID and app-specific password remain for *notarisation* of the
# direct-download .dmg, which is a separate flow and is left alone.
# Never on a pull request, and this one matters more than the notarization
# guard above: the step stamps CFBundleVersion from github.run_number and
# uploads to App Store Connect, so an unguarded pull-request build mints a
# real Mac App Store build off a branch nobody has merged.
- name: Build & upload Mac App Store package (.pkg)
if: matrix.os == 'macos-latest' && env.HAS_MAS_SIGNING == 'true' && github.event_name != 'pull_request'
timeout-minutes: 30
env:
HAS_MAS_SIGNING: ${{ secrets.MAS_CERTIFICATE_P12_BASE64 != '' }}
MAS_CERTIFICATE_P12_BASE64: ${{ secrets.MAS_CERTIFICATE_P12_BASE64 }}
MAS_CERTIFICATE_PASSWORD: ${{ secrets.MAS_CERTIFICATE_PASSWORD }}
MAS_PROVISION_PROFILE_BASE64: ${{ secrets.MAS_PROVISION_PROFILE_BASE64 }}
SIGN_APP_IDENTITY: ${{ secrets.MAS_SIGN_APP_IDENTITY }}
SIGN_INSTALLER_IDENTITY: ${{ secrets.MAS_SIGN_INSTALLER_IDENTITY }}
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
BUILD_NUMBER: ${{ github.run_number }}
run: |
set -euo pipefail
# 1) Import the signing certs into a throwaway keychain.
KEYCHAIN="$RUNNER_TEMP/mas.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 "$MAS_CERTIFICATE_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/mas.p12"
security import "$RUNNER_TEMP/mas.p12" -k "$KEYCHAIN" -P "$MAS_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/productbuild
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PW" "$KEYCHAIN" >/dev/null
security list-keychains -d user -s "$KEYCHAIN"
# 2) Materialise the provisioning profile for build_mas.sh.
echo "$MAS_PROVISION_PROFILE_BASE64" | base64 --decode > "$RUNNER_TEMP/app.provisionprofile"
export PROVISION_PROFILE="$RUNNER_TEMP/app.provisionprofile"
# 3) Stamp the version: marketing version from app.config.APP_VERSION,
# build number from the monotonic CI run number (App Store Connect
# rejects a re-used CFBundleVersion).
APP_VERSION="$(python3 -c 'import re,pathlib; print(re.search(r"APP_VERSION\s*=\s*\"([^\"]+)\"", pathlib.Path("app/config.py").read_text()).group(1))')"
PB=/usr/libexec/PlistBuddy
ADD=packaging/mas/Info.plist.additions
$PB -c "Set :CFBundleShortVersionString $APP_VERSION" "$ADD"
$PB -c "Set :CFBundleVersion $BUILD_NUMBER" "$ADD"
echo "MAS marketing version $APP_VERSION, build $BUILD_NUMBER"
# 4) Build + sign the .pkg (skip build_mas.sh's interactive validate
# hint; the upload below is the real thing).
export PYTHON=python3
SKIP_VALIDATE=1 bash packaging/mas/build_mas.sh
# 5) Upload to App Store Connect with the API key.
#
# altool resolves --apiKey by looking for AuthKey_<KeyID>.p8 in
# ~/.appstoreconnect/private_keys (among others), so the key is
# materialised there under exactly that name and removed afterwards.
# The runner is ephemeral, but a key left on disk between steps is
# readable by anything that runs after it, and this costs one line.
KEYDIR="$HOME/.appstoreconnect/private_keys"
mkdir -p "$KEYDIR"
echo "$ASC_API_KEY_P8_BASE64" | base64 --decode > "$KEYDIR/AuthKey_$ASC_KEY_ID.p8"
chmod 600 "$KEYDIR/AuthKey_$ASC_KEY_ID.p8"
trap 'rm -f "$KEYDIR/AuthKey_$ASC_KEY_ID.p8"' EXIT
xcrun altool --upload-app -f dist_mas/EasyPostDesktop.pkg -t macos \
--apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
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.
#
# Ordering matters: this runs while no variant flags exist, so the MSIX
# is packaged from a Store-shaped build (no licence gate, no MCP). The
# direct-download Windows build is produced afterwards, with the flags
# in place, overwriting dist/EasyPostDesktop/.
- name: Build MSIX package
if: matrix.os == 'windows-latest'
run: python packaging\build_msix.py
# Now rebuild the same tree as the direct-download variant. The MSIX is
# already written, so overwriting dist/EasyPostDesktop/ is safe, and it
# is this second build that ships from the website.
- name: Enable MCP + license gate for the direct-download Windows build
if: matrix.os == 'windows-latest'
shell: bash
run: |
# The MSIX is already packaged; this tree now becomes the direct
# download, which is the Paddle-gated variant — never the Store one.
rm -f app/resources/store_build.flag
touch app/resources/license_required.flag
touch app/resources/mcp_supported.flag
- name: Rebuild Windows direct-download variant
if: matrix.os == 'windows-latest'
run: pyinstaller packaging/build_exe.spec --noconfirm --distpath dist --workpath build
# A packaged build that quietly loses its variant flags would ship the
# paid app ungated, which is exactly what happened before the flags were
# added to the spec's datas. Fail the build rather than find out later.
- name: Verify variant flags survived packaging (Windows)
if: matrix.os == 'windows-latest'
shell: bash
run: bash packaging/verify_variant_flags.sh dist/EasyPostDesktop
# 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
# Sign the direct-download Windows .exe via Azure Artifact Signing
# (formerly Trusted Signing) — a Microsoft-managed, Public Trust
# certificate, which is what removes the SmartScreen "unknown publisher"
# warning. The Store MSIX is re-signed by Microsoft on publish, so only
# the direct-download build needs this.
#
# No-op until the repo variable AZURE_SIGNING_READY is 'true'. The
# AZURE_CLIENT_ID / TENANT_ID / SUBSCRIPTION_ID repo secrets authenticate
# through an OIDC federated credential (app registration
# EasyPostDesktop-GitHubActions, subject repo:sgf36/EasyPost:ref:refs/heads/main)
# — no client secret exists to leak. Because the credential is bound to
# that subject, signing can only ever happen on main.
#
# The signing action picks the Azure credential up from the CLI session
# azure/login leaves behind, so the login step is not optional.
# Account, endpoint and profile are read from the resource itself; see
# CI-AZURE-SIGNING-SETUP.md before changing any of the three.
- name: Azure login (OIDC) for signing
if: matrix.os == 'windows-latest' && vars.AZURE_SIGNING_READY == 'true' && github.event_name != 'pull_request'
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Sign Windows build (Azure Artifact Signing)
if: matrix.os == 'windows-latest' && vars.AZURE_SIGNING_READY == 'true' && github.event_name != 'pull_request'
uses: azure/artifact-signing-action@v2
with:
endpoint: https://weu.codesigning.azure.net/
signing-account-name: EasyPostDesktop
# The profile is named for the publisher, not the product — one
# profile signs everything the business ships. It is NOT named
# EasyPostDesktop, which is the signing *account*.
certificate-profile-name: SpencerFieldsSoftware
# Both executables sit at the top of the folder, so no recursion:
# EasyPostDesktop.exe is what people launch, easypost-mcp.exe ships
# beside it in the direct-download variant. Leaving either unsigned
# would leave a SmartScreen prompt on the build.
files-folder: ${{ github.workspace }}\dist\EasyPostDesktop
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
# A signing step that silently signs nothing looks exactly like one that
# worked, and the failure would only surface as a SmartScreen prompt on
# someone else's machine. Prove the chain and the timestamp validate
# before the build is allowed to publish anything.
- name: Verify Windows signature
if: matrix.os == 'windows-latest' && vars.AZURE_SIGNING_READY == 'true' && github.event_name != 'pull_request'
shell: pwsh
run: |
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object FullName -Descending | Select-Object -First 1
if (-not $signtool) { throw "signtool.exe not found on the runner." }
$exes = Get-ChildItem "dist\EasyPostDesktop" -Filter *.exe
if ($exes.Count -lt 2) { throw "Expected both EasyPostDesktop.exe and easypost-mcp.exe; found $($exes.Count)." }
foreach ($exe in $exes) {
& $signtool.FullName verify /pa /v $exe.FullName
if ($LASTEXITCODE -ne 0) { throw "Signature verification failed on $($exe.Name)." }
}
Write-Host "Both executables carry a valid, timestamped Authenticode signature."
# Runs AFTER the Azure signing steps above, deliberately. Signing
# rewrites EasyPostDesktop.exe and easypost-mcp.exe in place, so a
# checksum taken before it describes a file nobody will ever download —
# and SHA256SUMS.txt is published on the download page as the thing
# people verify against. Never move this above the signing steps.
#
# The macOS leg is still covered by Developer ID signing + notarization;
# this list is what lets someone confirm the download was not 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")
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: EasyPostDesktop-${{ matrix.os }}
path: dist/*
# Store screenshots, rendered by CI rather than captured by hand on a rented
# Mac. The macOS leg produces genuinely Mac-looking assets — real system
# fonts, real control metrics, real window chrome — because it runs on a
# macOS runner with the native cocoa platform plugin; the app is simply never
# shown, since QWidget.grab() paints an unmapped widget.
#
# Manual-only: screenshots are a release chore, not something every push needs.
# The locale input is comma-separated so one dispatch can capture the whole
# localised set. Actions expressions have no split() or replace() — fromJSON
# is the only way into a matrix dimension — so the list is turned into JSON
# here, in a shell, and consumed as a matrix below.
locales:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
list: ${{ steps.split.outputs.list }}
steps:
- id: split
env:
RAW: ${{ github.event.inputs.locale || 'en' }}
run: |
list=$(printf '%s' "$RAW" | tr -d '[:space:]' | tr ',' '\n' \
| grep -v '^$' | sort -u | jq -R . | jq -sc .)
echo "list=$list" >> "$GITHUB_OUTPUT"
echo "capturing: $list"
screenshots:
if: github.event_name == 'workflow_dispatch'
needs: locales
strategy:
fail-fast: false
matrix:
target:
- os: macos-latest
platform: mac
- os: windows-latest
platform: windows
locale: ${{ fromJSON(needs.locales.outputs.list) }}
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# No credentials are configured on the runner, and the script stubs the
# credential store regardless — a published screenshot must never contain
# a real API key or data from a real EasyPost account.
# Whole-window captures, not bare page widgets. Every image already on
# the Microsoft Store and App Store listings shows the full application
# — mode banner, navigation sidebar and all — so a bare view is
# recognisably the same product but visibly not the same screenshot, and
# looks wrong sitting beside the ones it did not replace.
# The window list matches the nine slots the Microsoft Store listing
# publishes, so the App Store and Partner Center sets tell the same story
# rather than each showing whichever views someone remembered that day.
- name: Render screenshots
run: >
python packaging/make_screenshots.py
--platform ${{ matrix.target.platform }}
--locale ${{ matrix.locale }}
--window CreateShipmentView
--window TrackingView
--window AddressBookView
--window BatchView
--window HistoryView
--window ReportsView
--window HtsLookupView
--window SettingsView
--window PickupsView
--out dist/screenshots
- name: Upload screenshots
uses: actions/upload-artifact@v7
with:
name: screenshots-${{ matrix.target.platform }}-${{ matrix.locale }}
path: dist/screenshots/**