Android identity is com.papyrus.reader; the first uploaded bundle establishes it
in Play Console. Keep this identity for all subsequent uploads.
The app name is Papyrus. The pinned Flutter 3.41.2 toolchain targets Android API 36.
Feature and fix PRs target the default development branch. Merge them without
bumping the version. CI checks PRs and integration pushes; development does not
build or publish releases.
When ready to release the accumulated changes, prepare the coordinated version
bump in a PR to development, then open development → master. Use Create a
merge commit for this promotion and bring master back into development
afterwards. The workspace release runbook describes the server/client merge
order. Keep both branches; do not squash release promotions.
app/pubspec.yaml owns version: MAJOR.MINOR.PATCH+BUILD. The semantic portion
matches the server release; BUILD is a positive, globally increasing Android
version code. Do not derive it from a workflow run number. Every new Play upload
must use a higher code, including rebuilds of the same semantic release.
The Release workflow considers changes to this file on master, then compares
its version value with the previous push commit. Dependency-only edits skip all
release builds. Normal PR CI continues independently. Manual dispatch on master
is available for the first build or a retry of an unreleased commit. Tags are
created after builds, not used as a second build trigger. A released tag cannot
be reused for a different commit. Client tags include the build number, e.g.
v0.0.1+1. Version changes build Android, web, Linux and Windows. Android
produces a signed AAB for Play and a signed universal APK for direct installation.
Both Android artifacts are available even if an unrelated desktop job fails.
Use the workspace tools/release.py bump command to coordinate manifest changes
with the server rather than editing each version separately. See the workspace
release runbook for merge order.
The client GitHub environment named release is restricted to master.
Its public endpoint variables use the registered papyrus-reader.com domain:
| Variable | Release value |
|---|---|
PAPYRUS_API_BASE_URL |
https://api.papyrus-reader.com |
POWERSYNC_SERVICE_URL |
https://sync.papyrus-reader.com |
Use origins without /v1: the client adds the API prefix. HTTP, private addresses,
localhost and placeholder endpoints are rejected. These addresses are public
configuration, not secrets. Existing development defaults remain available in
local debug builds. Changing endpoints requires a new Android build number.
Generate an upload key, not a debug key, on your own machine and store a backup securely. For example (keytool prompts for the passwords):
keytool -genkeypair -v -keystore upload.jks -storetype JKS \
-keyalg RSA -keysize 2048 -validity 10000 -alias uploadPut these secrets in the same GitHub environment:
| Secret | Value |
|---|---|
ANDROID_KEYSTORE_BASE64 |
Base64 encoding of the upload keystore file |
ANDROID_KEYSTORE_PASSWORD |
Keystore password |
ANDROID_KEY_ALIAS |
upload, or the alias you chose |
ANDROID_KEY_PASSWORD |
Key password |
The workflow restores the file in runner temporary storage, builds the AAB, checks 64-bit native ELF alignment for 16 KB page support, and uses the pinned bundletool to generate a universal APK from that same bundle. APK packaging requires the upload key explicitly, verifies its signature, package/version, 16 KB ZIP alignment and ELF alignment, then uploads both artifacts and removes temporary signing files. Passwords are passed through restricted temporary files. Direct APKs use the upload signing key, which can differ from Play's app signing key. See INSTALL.md before switching installation sources. Never commit a keystore or passwords.
Local release builds use the same four environment variables (replace
ANDROID_KEYSTORE_BASE64 with ANDROID_KEYSTORE_PATH, an absolute local path),
or an ignored app/android/key.properties:
storeFile=/absolute/path/upload.jks
storePassword=your-password
keyAlias=upload
keyPassword=your-passwordDebug builds keep debug signing. Release builds without an upload key fail with an actionable message; they cannot silently use the debug certificate.
- Create Papyrus in Play Console with the appropriate account/developer details.
The first uploaded bundle uses
com.papyrus.reader. Choose internal testing for the first device test. - Enable Play App Signing. Google holds the app signing key; GitHub uses your upload key. Keep the same upload key for later releases.
- Configure the public API, PowerSync, SMTP and Google OAuth settings using the server deployment runbook. Check the endpoints from outside your local network.
- Merge the initial
0.0.1+1version change (or dispatch Release onmaster), download theandroid-releaseartifact, and upload the*-android-universal.aabto internal testing. The local validation bundle uses a temporary certificate and placeholder URLs and must never be submitted to Play. - Add testers and share Play's opt-in link. Install through Play on real devices and test offline importing/reading, sign-in/callback, profile switching, media upload/download, sync/reconnect and reader resume.
- Check Play's native-library/page-size diagnostics. ELF alignment checks do not
replace APK packaging or actual Android 16 KB emulator/device tests. Use
bundletool dump config --bundle=app-play-release.aabto verifyPAGE_ALIGNMENT_16K, and test an APK generated from the bundle.
After GitHub publication, the delivery workflow automatically sends the signed bundle to Play internal testing and deploys the web app in independent jobs. The first app upload/setup remains manual. Configure the restricted Play service account described below before the next release. Delivery failures do not remove published downloads.
Complete Play's app content and store forms: privacy policy URL, Data safety, content rating, target audience, app access instructions/test login, screenshots, 512 px store icon and feature graphic. Declare the actual server/account/book file behavior rather than claiming that all data stays on-device.
Papyrus currently supports account creation but has no account-deletion flow. Google Play requires account deletion within the app and an external deletion request URL for applicable distribution tracks. Implement and validate deletion of the user's account and associated data before broader distribution. Publishing an invented privacy policy or marking this requirement complete is not part of build preparation. Check the Console requirements for your developer account; new personal accounts may require closed testing before production access.
Official references:
- Flutter Android release and signing
- Play target API requirements
- Android 16 KB page support and testing
- Account deletion requirements
Every download includes vMAJOR.MINOR.PATCH+BUILD in its filename. Desktop
artifacts also include x64. The Android AAB and APK are android-universal and web is
architecture independent. GitHub releases contain the AAB, APK, web ZIP, Linux tarball,
.deb, AppImage, Windows ZIP and installer, plus INSTALL.md,
release-manifest.json and SHA256SUMS. See installation instructions.
Linux uses Ubuntu 24.04 as its build baseline. The AppImage packages WebKit helper
processes and GTK resources; it requires host bubblewrap/xdg-dbus-proxy and user
namespaces to preserve WebKit's sandbox. Do not disable sandboxing to make a test
pass. Validate the installed .deb on Ubuntu 24.04 and AppImage on Ubuntu 24.04
and Debian 13. Run EPUB/PDF rendering, offline import, resume and profile switching
on real graphical sessions before advertising compatibility. Headless startup
checks alone do not establish reader functionality.
Windows uses an unsigned, per-user Inno Setup installer. Visual C++ runtimes are included, and Microsoft's Authenticode-verified offline WebView2 installer runs only when needed. Test a clean Windows x64 machine both with and without WebView2, then upgrade and uninstall while retaining a real library. No trusted signing service or Microsoft Store submission is configured. Installed Windows and AppImage builds include integrated update delivery. See update delivery for the plan, APT signing configuration and native acceptance checks.
The Windows packaging workflow also runs on app changes and supports manual
dispatch. It saves a windows-preview-RUN_ID artifact of the production entrypoint
with public production endpoints before compiling the instrumented test entrypoint.
Both installer and ZIP are checked against the build bundle and launched, including
the visible window title. The instrumented packages exercise reader and persistence
behavior separately. Follow Windows release acceptance for
real-machine checks, including upgrading from the currently published release.
Keep the executable's CompanyName=com.papyrus and ProductName=papyrus stable:
Windows path and credential plugins use them to locate existing user data.
The window title and FileDescription provide the capitalized display name.
Packaging PR checks need no production credentials. The release packaging jobs use the production endpoints, preserve the signed Android bundle alignment checks, and only publish after every required build/package job succeeds.
See delivery rollout evidence for completed setup and the remaining operational acceptance checks.
- In the existing Google Cloud project, enable the Google Play Android Developer API and create a service account dedicated to this client.
- Invite that service account in Play Console with access to only
com.papyrus.reader, app information visibility and testing-track release permissions. Do not grant production, financial or account-administration permissions. Initialize the app, upload the first AAB manually, and complete the internal-testing setup and tester list. - Store its JSON key as
PLAY_SERVICE_ACCOUNT_JSONin the client GitHubreleaseenvironment. Keep the key out of Git, logs and artifacts. The API preflight authenticates directly using Google's pinned Python auth library; it needs no Cloud project role or IAM impersonation permission. - Delivery compares Play's version code and bundle SHA-256 before upload. A
matching completed internal release is a no-op; a matching uploaded bundle
can be promoted without uploading again. Different content at the same code
or a newer existing code fails. The target is always
internal, statuscompleted. Draft-app restrictions fail visibly and must be resolved in Console rather than silently changing the requested delivery status.
The host serves /srv/apps/papyrus/deploy/web/current through its existing Caddy
container. Keep the parent web directory mounted; mounting the symlink target
would prevent activation of subsequent releases.
- Run the server's
deploy/migrate_web_layout.pyagainst the existing flat web directory. It copies the deployed files into an initial release, createscurrent, and preserves the original flat files. Install the updated Caddy configuration and reload only the web Caddy process. Check/and/login. - Install
tools/install_web.pyas root-owned/usr/local/lib/papyrus-web/install_web.pyon the host. Give a dedicatedpapyrus-web-deployuser write access only to the web artifact directory; do not add it to Docker or sudo groups. - Generate a dedicated SSH key. Its root-owned authorized-keys entry must use
restrict,command="/usr/bin/python3 /usr/local/lib/papyrus-web/install_web.py --root /srv/apps/papyrus/deploy/web". This command accepts onlydeploy REVISION SHA256 TAGand an archive on stdin. Do not permit arbitrary SSH commands or reuse the website/API deployment key. - Add
WEB_SSH_HOST,WEB_SSH_USERand verifiedWEB_SSH_KNOWN_HOSTSas variables in the GitHubreleaseenvironment; addWEB_SSH_PRIVATE_KEYas a secret.
The receiver validates the ZIP checksum, entry paths, required files and embedded
release identity, then switches current atomically under a file lock. Public
/release.json, /, /login and /flutter_bootstrap.js must match the artifact.
A failed verification restores the previous target. Old directories are retained;
there is no automatic data or artifact deletion. Caddy revalidates web content.
If Cloudflare fronts the app, configure an app-host-only cache rule for
http.host eq "app.papyrus-reader.com": bypass the edge cache and set Browser TTL
to Respect origin TTL. The default browser TTL can otherwise replace Caddy's
no-cache header with a four-hour lifetime for JavaScript. The public deployment
probe checks revalidation headers and exact bytes for metadata, both entrypoint
routes, the Flutter bootstrap and main.dart.js; failure rolls activation back.
The deployment user cannot restart or change the API, sync service or database.
Delivery rejects a lower build number than the active web release; an equal build number must have identical metadata and archive checksum. Retrying an old tag cannot silently downgrade production.
For an operator rollback, atomically replace current with the relative target
recorded by previous while holding .deployment.lock, then verify the app. The
previous link is retained across an identical successful delivery retry.
Use Deliver published release → Run workflow on master, enter the exact
published tag, and choose web, play or all. This downloads existing assets,
resolves the tag to its commit, and verifies the release manifest and checksums.
It never rebuilds the application. Historic releases without a manifest cannot
be delivered by this workflow; prepare a new versioned release instead.
Do not rerun the build workflow to retry delivery: rebuilt bytes may differ. Existing published assets are never overwritten. A draft interrupted during publication must be inspected and completed with its original artifacts; if those are unavailable, discard the draft and use a new build number. Tags cannot be reassigned. Release summaries distinguish builds, GitHub publication, web verification and Play delivery. Missing credentials are errors, not successful skips. Actual tester installation is a separate check from API acceptance.