Close the gaps family idea #5103 found in how Inkwell distributes its app
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 5s
Android / Build, or is the channel already serving this? (push) Successful in 6s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 7s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 18s
CI & Build / integration (push) Successful in 1m13s
CI & Build / Build & push image (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 12s
Android / Core and FFI clippy and tests (push) Successful in 45s
Android / Kotlin + Rust (APK) (push) Successful in 9m50s
Android / Build the server image (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 5s
Android / Build, or is the channel already serving this? (push) Successful in 6s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 7s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 18s
CI & Build / integration (push) Successful in 1m13s
CI & Build / Build & push image (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 12s
Android / Core and FFI clippy and tests (push) Successful in 45s
Android / Kotlin + Rust (APK) (push) Successful in 9m50s
Android / Build the server image (push) Successful in 2s
Three of the idea's practices this project still owed (Scribe #5118): Practice 3, CI fails on the wrong signer. The signing step printed the certificate and went on. It now fails unless the APK has exactly one signer and that signer is the release certificate (SHA-256 408a5835…, pinned from run 8753). The steps that publish come after it in the same job, so a wrongly signed build is never staged or published. Practice 6, app downloads are throttled and carry a sha256 ETag. - The download route counts per account and answers 429 with Retry-After past the limit. The limit is a new Settings → Security value, "App downloads per account per hour" (default 30), live like the sign-in limits. - The ETag is the sidecar's sha256, not Quart's mtime-and-path, so a phone resuming a download across a redeploy is not told its partial copy is stale. Quart's own ETag and conditional handling are off, and the route runs the conditional pass after setting the ETag, so Range and If-Range are judged against the content. Practice 9, the update offer and debug builds. - The install-permission notice re-reads the grant each time the app comes back, as ReminderNotice does. Read once, it stayed up after someone granted the permission in Settings and came back. - A debuggable build says it can't update itself and checks for nothing. Android would refuse the release-signed APK over a debug signature anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,11 +232,31 @@ jobs:
|
||||
# generated. Signing with the WRONG key produces a perfectly valid APK that
|
||||
# simply refuses to install over the app already on the phone — a failure
|
||||
# that otherwise only shows up on the device, after the run is green.
|
||||
- name: Show the signing certificate
|
||||
#
|
||||
# And FAILS unless there is exactly one signer and it is the release key
|
||||
# (family idea #5103, practice 3). Printing alone was not a check: a build
|
||||
# signed with any other key would have gone on to be staged and published,
|
||||
# and every phone would have refused it. The steps that publish come after
|
||||
# this one in the same job, so a failure here stops them.
|
||||
- name: Check the signing certificate
|
||||
if: steps.build.outputs.keystore != ''
|
||||
env:
|
||||
# The release certificate's SHA-256, as apksigner printed it for the
|
||||
# signed APK in run 8753 (CN=Bryan Van Deusen, O=fabledsword). Every
|
||||
# installed copy carries this certificate. It only changes if the key
|
||||
# does, and then every install has to be replaced by hand anyway.
|
||||
RELEASE_CERT_SHA256: 408a5835ea1627f329d9fba4f00712b096faf2d30624ec6f3a8b2b5f8bb15caa
|
||||
run: |
|
||||
apksigner="$(ls /opt/android-sdk/build-tools/*/apksigner | head -1)"
|
||||
"$apksigner" verify --print-certs "app/build/outputs/apk/release/app-release.apk"
|
||||
certs="$("$apksigner" verify --print-certs "app/build/outputs/apk/release/app-release.apk")"
|
||||
printf '%s\n' "$certs"
|
||||
signers="$(printf '%s\n' "$certs" | grep -c '^Signer #[0-9]* certificate SHA-256 digest:' || true)"
|
||||
digest="$(printf '%s\n' "$certs" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')"
|
||||
if [ "$signers" != 1 ] || [ "$digest" != "$RELEASE_CERT_SHA256" ]; then
|
||||
echo "::error::The APK is not signed by the release key alone ($signers signer(s), first $digest; expected $RELEASE_CERT_SHA256)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Signed by the release key."
|
||||
|
||||
# Staged with a STABLE name plus the sidecar the server reads its version
|
||||
# out of — an APK keeps that in a binary manifest Python cannot parse, and
|
||||
|
||||
Reference in New Issue
Block a user