Maintenance
Maintenance
Automation lives in the scripts/ package (swpngx). Run swpngx
via uv run --project scripts swpngx … from the repo root (or install the package
so swpngx is on your PATH).
Device ids, simulator names, bezel PNGs, and framing geometry are defined once in
screenshot_devices.toml. screenshots.toml
references those ids for capture; frames.toml references the same file
for framing. iPhone screenshots use Pro Max only (App Store Connect scales for smaller
phones). Check alignment with:
TestFlight betas
A beta is cut entirely on CI — no commit, no tag, no local build:
just beta # from origin/main
just beta --ref release/v1.12 # from a release branch
just beta --dry-run # CI builds and exports, uploads nothing
This previews the notes the build will publish, then dispatches
.github/workflows/beta.yml (which you can also
start straight from the Actions tab). CI asks App Store Connect for the build
number, writes it into Config/Shared/Version.xcconfig for that build only,
archives, signs, uploads, and — after the upload succeeds — creates the
builds/<version>/<build> tag and its GitHub prerelease at the built commit. The
tag is the record of what shipped, not the trigger, so it can never name a build
that failed to upload.
Notes come from current_changelog.txt, which
accumulates the user-facing bullets for the current marketing version. Nothing
clears it at build time:
- a build's notes are the bullets added since the previous build tag; they
become the prerelease body, which is what the in-app What's New screen shows
(
just beta-notespreviews them) - TestFlight "What to Test" is one
1.11.0 (209)section per build of the current version, newest first — the same shape aschangelog.txt, scoped to the version in flight. App Store Connect caps that field at 4000 characters and a version's notes routinely run past it, so whole builds are dropped off the oldest end until it fits and a link to the releases takes their place. A build is either there in full or not at all; only a single build whose own notes exceed the budget falls back to trimming by line. No more clearing the changelog by hand to fit - when the version ships, empty the file in the same commit that bumps
MARKETING_VERSIONwithjust set-version
To fix notes after the fact, edit the release body on GitHub (the copy users
read) and, for TestFlight, run just set-test-notes <build>.
changelog.txt is an offline copy of the per-build notes,
regenerated from the releases with just changelog-archive.
What's New decks
The paged What's New screen for a version is written as YAML front matter at the
top of its release note, docs/release_notes/v<version>.md. The docs build
turns each one into whats_new/<version>.json on the site, which the app
fetches; a note without a whats_new key publishes nothing.
---
whats_new:
pages:
- title: What's new in 1.11
subtitle: Tags # optional
blocks:
- type: card # card, tip (adds tip buttons) or telemetry
icon: tag # any SF Symbol name, e.g. ladybug for a fixes card
tint: teal # optional: blue, indigo, orange, pink, purple, red, teal
title: Nested tags
text: Support for hierarchical tags.
image: ../assets/tags.png # optional picture above the card
image_shadow: true # optional; a glow in dark mode. Needs transparent corners
detail: | # optional Markdown for "Learn more"
Tags can now have parents. 
---
Relative links and images in detail, and a card's image, resolve against
https://swift-paperless.app/release_notes/, so an image in docs/assets/ is
../assets/<name>. An image has to exist in the docs; the check below
fails if it doesn't. Check a deck before pushing with:
If a deck has a mistake, this command and the docs build both stop and name the
file and field. While editing, run just whats-new-serve and open the
"Generated" preview in WhatsNewDeckView.swift: it rebuilds the deck into
docs/whats_new/ on every save, the preview reloads within a second, and
images load from the local docs server on port 5006, as they would from the
site. When copying a release note into its GitHub release, leave out
the front matter block: it is only for the app.
App Store screenshots
The whole pipeline runs locally with just; nothing needs fastlane.
just demo-reset # Paperless-ngx in Docker, seeded with the fixture PDFs
just screenshots # build, capture every locale, frame
just previews # render the app preview video per locale
just screenshots-preview # review in the browser
just deliver-preview # dry run against App Store Connect
just deliver # upload metadata, screenshots and previews
1. Demo backend. just demo-up starts Paperless-ngx on port 9988 and
uploads the PDFs in Preview PDFs/; just demo-reset wipes it first, so it
holds only those documents (setup is not idempotent). Pin the image with
uv run --project scripts swpngx capture setup --pngx-tag 2.19.4, or add
--random-tags 1000 (and --random-correspondents, --random-document-types)
for UI stress testing. just demo-down removes it.
2. Capture. swpngx capture capture (config:
screenshots.toml) builds the app, reinstalls it on each
simulator, and takes one screenshot per step and locale. Every step is a fresh
launch in preview mode that routes through its urls (x-paperless deep links,
see DataModel/Sources/DataModel/Route.swift; device_urls overrides them per
device), so no taps or link prompts are involved. It writes files like
store/screenshots/en-US/iPhone_18_Pro_Max-01_documents.png; the prefix is
the device id from screenshot_devices.toml.
Simulator UDIDs in screenshots.toml are per machine.
iPhone Duo is one simulator with two displays: iPhone_Duo_Inner and
iPhone_Duo_Outer share its UDID and pick a display with display. simctl
cannot fold the device, so capture stops before each and asks you to set the
pose (Open or Closed) in Device Hub. Open is captured in its landscape
orientation, with the side status bar. --device iPhone_Duo_Outer
captures one device only. Both land in App Store Connect's IPHONE_DUO set,
which needs asc 6.4 or later.
3. Frame. swpngx promo screenshots draws each capture in a device frame
on the promo background with a headline from
store/promo/headlines.toml, and writes
store/screenshots/framed/<App Store locale>/*-framed.png at 1320 × 2868
(iPhone), 2064 × 2752 (iPad), and 2853 × 2007 and 1398 × 2034 (iPhone Duo's
inner and outer displays). The scene is
store/promo/scenes/screenshot.html; long headlines shrink to fit. It renders
with Playwright's Chromium (uv run --project scripts playwright install
chromium once).
swpngx frame is the older framer: Apple device bezels and titles from
store/screenshots/Screenshots.xcstrings
(config: frames.toml; bezels via swpngx frames download,
font via swpngx fonts download). It writes the same file names, so either can
feed just deliver.
4. App preview. swpngx promo previews renders the "Paper to pixels"
video (store/promo/scenes/paper_to_pixels.html, 886 × 1920, 17 s) for each
locale, from that locale's first three iPhone captures, the first three
headlines and video_end in headlines.toml. It writes
store/previews/<App Store locale>/paper_to_pixels.mp4; a locale takes two to
three minutes.
Review with just screenshots-preview, which serves the app previews, framed screenshots
and the metadata for every locale.
Upload metadata and framed screenshots with asc
(brew install asc):
just deliver-preview # dry run
just deliver # metadata + screenshots
just deliver-metadata # metadata only (What's New, description, …)
just deliver runs swpngx deliver for MARKETING_VERSION from
Config/Shared/Version.xcconfig, creating that App Store version if it does not
exist yet. It reads metadata from store/metadata/, sets the copyright and
categories, and replaces all screenshots of each device in
screenshot_devices.toml that has framed PNGs. No IPA
is uploaded; TestFlight builds go through just beta separately. just
deliver-preview also validates locally, then shows the changes against App Store
Connect once the version exists.
store/metadata/default/ applies to every locale, including en-US, unless
the locale directory overrides a file.
What's New is edited only in store/metadata/default/release_notes.txt
and applies to every locale.
Authentication uses the asc auth login credentials, or the same App Store
Connect API key as TestFlight (APP_STORE_CONNECT_API_KEY_ID,
APP_STORE_CONNECT_ISSUER_ID, and APP_STORE_CONNECT_KEY_FILEPATH or
APP_STORE_CONNECT_KEY_CONTENT).
PNG screenshots under store/screenshots/ are gitignored; generate them locally
before upload.
Panorama montage
Concatenate framed screenshots (adjust the glob for your device name):
montage store/screenshots/framed/en-US/iPhone_18_Pro_Max-0*-framed.png \
-tile 4x1 -geometry +20+0 panorama.png
If montage fails with unable to read font `', the ImageMagick install has no
configured fonts; pass one explicitly, e.g.
-font /System/Library/Fonts/Supplemental/Arial.ttf.
docs/panorama.png is a symlink to the top-level panorama.png, so regenerating
it updates both the README and the docs site.