docs: clarify production scanner signing on Gitea
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: Shared Android CI / unit-test (pull_request) Successful in 4m11s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Android CI / android-test (pull_request) Successful in 10m6s
QRun-IO/ci-policy: Shared Android CI / secrets (pull_request) Successful in 17s
QRun-IO/ci-policy: Shared Android CI / package (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared Android CI / gate (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / android-0 (pull_request) Successful in 14m59s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s

Replace the unselected GitHub release instructions with the actual Gradle
signing inputs, Gitea SDK publication scope, key custody and channel decisions.
Record operator-reported manual acceptance and preserve the development-to-
production signer migration distinction. No keys, workflows or runtime change.

Checked against the Gradle configuration, current Gitea caller and official
Android signing documentation; git diff --check passes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Sonnet 5.5 committed 2026-11-04 09:44:13 -06:00
1 parent 0ae2101f08
commit f58e28b0c2
3 files changed
+44 -24

No files matched your search

+34 -24
View File
@@ -1,43 +1,53 @@
# Scanner APK releases
# Scanner APK release readiness
Status: **no distribution channel is selected.** Everything below describes a candidate GitHub-based APK workflow that is prepared but not chosen; the primary forge is Gitea (`git.kof22.com/QRun-IO`) and GitHub is a shadow/archive, so treat the GitHub environment, tag and release steps as design notes to be re-decided, not instructions to follow now. The signing identity, complete native acceptance, final security review and first release are not yet qualified. Current installed demo APKs use a development certificate. Do not label them production releases.
Status (November 4, 2026): **no production signing identity or distribution channel has been selected.** Per the owner's October 31 decision, signing and distribution are the final step. Do not create signing keys, populate secrets, create release tags or publish releases until the decisions below are made. Current installed demo APKs use a development certificate and are not production releases. Manual TC22 acceptance was reported all-pass by the operator on November 4 (operator-reported, not independently verified); the last recorded installed APK was development-signed, and the tested build was not independently inspected. It does not qualify a production-signed APK.
The [November 1 independent automation review](RELEASE-AUTOMATION-REVIEW.md) found no actionable finding in the exact reviewed workflow/build files. External GitHub protections, the production identity and actual signed-device acceptance remain unverified.
## What exists today
Owner decision, October 31: leave signing and distribution until the final step because the distribution channel has not been chosen. Do not generate signing keys, populate production signing secrets, create release tags or treat the candidate GitHub APK workflow as a selected distribution channel. Complete implementation, audits, tests, documentation and ordinary CI first; then return to the owner for the distribution decision.
- **Gitea CI (actual).** `.gitea/workflows/qrun-android-release.yaml` runs on push to `main` (and manual dispatch) and calls the central `KofTwentyTwo/platform-ci` Android workflow at `qrun-release-v1`. Central main run 3634 passed and published the SDK AARs (the shared library). It is **not** a scanner-APK signing or release pipeline: it uses no scanner signing secrets and produces no production-signed scanner APK (any debug artifacts it builds can be development-signed).
- **Local Gradle signing (actual).** `scanner-host/build.gradle.kts` reads four environment variables: `SCANNER_KEYSTORE` (path to the keystore file), `SCANNER_STORE_PASSWORD`, `SCANNER_KEY_ALIAS`, `SCANNER_KEY_PASSWORD`.
- All four set: the `release` build type is signed with them.
- None set: `assembleRelease` produces an **unsigned** APK, for inspection only.
- Some but not all set: the build fails immediately.
- Debug, `e2e` and `qa` builds keep the development debug signing.
- Version comes from `-PscannerVersionName` (semantic version, default `0.1.0`) and `-PscannerVersionCode` (1 to 2100000000, default `1`).
- **Identity.** Application ID `io.qrun.mobile.scanner`. One APK serves all customers; branding is applied at runtime from the selected server's metadata, so no per-customer key or build is needed by default.
- **Legacy GitHub workflows (historical only).** `.github/workflows/release-scanner.yml` and `scanner.yml` were a candidate design from before Gitea became the primary forge (GitHub is a shadow/archive). They are not selected, not the release path, and nothing here claims a configured `scanner-release` environment, protected secrets, tag rules or `SCANNER_CERTIFICATE_SHA256` / `SCANNER_VERSION_CODE` variables exist on any forge. Treat them as design notes; do not run or follow them. The [November 1 review](RELEASE-AUTOMATION-REVIEW.md) covered those files only, not any live configuration.
The shared library keeps its existing main-only CircleCI publication. The candidate scanner workflow uses a separate `scanner-v<semantic-version>` tag and GitHub release (not selected). One APK serves customers through the selected server's runtime branding. The application ID and launcher identity remain `io.qrun.mobile.scanner` / QQQ Mobile.
## What is needed for production signing
## Release prerequisites
Android app signing uses a self-signed certificate that you own. It is not a Windows Authenticode or public-CA certificate, and nothing needs to be purchased. What matters is that the same signer is kept for every update. Reference: [Android app signing](https://developer.android.com/studio/publish/app-signing) and [Prepare for release](https://developer.android.com/studio/publish/preparing/).
- Merge only after the exact source passes the shared library, host, real-server native and companion browser tests, plus the independent security review. Update the immutable companion-server revision in `scanner.yml` to the qualified server commit first.
- Complete the navigation and physical TC22 acceptance matrix, camera/Record Files integration qualification, dependency attribution and production HTTPS/authentication/storage checks documented in the project TODO and companion readiness report.
- Establish an organization-owned release keystore and backed-up recovery process outside Git. Keep that signing identity for upgrades. Configure the `scanner-release` GitHub environment before adding secrets: restrict deployment tags to `scanner-v*`, protect release-tag creation/update/deletion with repository rules, and limit authorized release maintainers. Store `SCANNER_KEYSTORE_BASE64`, `SCANNER_STORE_PASSWORD`, `SCANNER_KEY_ALIAS`, and `SCANNER_KEY_PASSWORD` in that environment, not repository-wide secrets. A workflow environment declaration alone does not create these protection policies; actual GitHub configuration must be reviewed before loading production keys. Never place secrets in Gradle properties committed to source or pass passwords on the command line.
- Set repository variable `SCANNER_CERTIFICATE_SHA256` to the intended certificate's SHA-256 digest, 64 hex characters without separators. The build compares the resulting APK signer to this independently configured public fingerprint.
- Set repository variable `SCANNER_VERSION_CODE` to a positive integer greater than every previously distributed scanner version code. Android's maximum is 2100000000. Release workflows are serialized across tags and compare the selected code with every published `scanner-v*` release's `BUILD.txt`, including prereleases. Missing or malformed prior build evidence fails closed. Account for any APK distributed outside GitHub manually as well; changing the display version alone is insufficient.
Decisions the owner needs to make:
## Build and publication
1. **Distribution channel.** Direct APK / MDM (Zebra or other managed rollout), or Google Play. This drives the key setup.
2. **Key ownership.** Use an existing organization-owned Android key or create a new one, owned by the organization rather than an individual.
3. **Secret storage and backup.** A managed secret store for the keystore and its passwords, plus a named backup/recovery owner. Losing a self-managed app signing key without a recoverable backup means existing installs cannot be updated in place. (A Google Play upload key, by contrast, can be reset.)
4. **CI placement.** Whether signed releases are built locally by the key owner or by a Gitea release workflow. The latter does not exist yet; its secret scoping and protection settings would need to be designed and reviewed on Gitea before any key is loaded.
Create the immutable `scanner-v1.0.0` tag only after the prerequisites pass. Use a prerelease suffix for a qualified release candidate, for example `scanner-v1.0.0-rc.1`. Do not move a published tag or replace released APK bytes.
Channel notes:
Push one release tag at a time and wait for its workflow to finish. GitHub's default concurrency group serializes execution but retains only one pending run; it is not a durable multi-release queue. The workflow reuses the validated version code during signing and rechecks the remote tag's commit immediately before publication. Immutable tag protection remains necessary to close concurrent tag-change races.
- **Direct APK / MDM:** sign with the organization key via the four `SCANNER_*` variables. That key is the app's permanent identity.
- **Google Play:** use Play App Signing with a separate upload key. If the same signature is required across Play and direct/MDM installs, choose an organization-owned app signing key that is compatible with that Play setup **before the first release**, since it cannot be changed casually afterward.
The tag workflow first calls the existing scanner verification workflow, including native emulator and real-server tests. Before signing, it verifies the tag's commit is an ancestor of protected `main` and enforces the increasing version code. It then builds a non-debuggable release APK, runs release lint, verifies its signature against the expected certificate, verifies package/version metadata and writes checksums and the source commit. Only the final publication job receives repository write permission. A failed verification or absent signing configuration prevents publication. The temporary keystore is removed at the end of the signing step. Signing passwords stay in masked environment variables and never become workflow outputs or artifacts. Repository/environment protection is essential because a maintainer able to change trusted workflow code could otherwise remove its in-workflow checks.
## Release requirements
The candidate APK release contains `qqq-mobile-<version>.apk`, `SHA256SUMS`, `signature.txt`, `BUILD.txt`, `aboutlibraries.json`, `LICENSE` and `NOTICE`. The notices are generated from the release runtime and also packaged for offline access in Settings. Download and verify these assets, then qualify the actual signed bytes on a device before customer rollout. Checksums establish artifact integrity; they do not replace signer verification or functional acceptance. Signing/distribution remains deferred by the owner until the final step.
- Preserve a compatible signer and a strictly increasing `versionCode` for every distributed build, including any distributed outside CI.
- Build a non-debuggable release APK (`lintRelease` then `assembleRelease`) and verify the signer's certificate fingerprint against the intended key's published SHA-256 before distribution.
- Test the **actual signed APK** on a device (install, enrollment, scanner path in [Zebra scanning](ZEBRA-SCANNING.md), Back/Apps navigation, update over a prior build) before any customer rollout. Checksums show integrity, not signer correctness.
- Never put passwords or keystores in Git, command lines, logs or committed Gradle properties. Supply them as environment variables from the secret store, and remove temporary keystore files afterward.
- Production builds must exclude the synthetic scan receiver and compile-only SDK implementation, as described in [Zebra scanning](ZEBRA-SCANNING.md).
For local release builds, supply the four `SCANNER_*` signing environment variables (use `SCANNER_KEYSTORE` for the local keystore path, not the CI-only base64 variable), then run:
Local release build, once a key is approved (keystore path and passwords provided via environment, never inline):
```text
./gradlew :scanner-host:lintRelease :scanner-host:assembleRelease -PscannerVersionName=1.0.0-rc.1 -PscannerVersionCode=2
./gradlew :scanner-host:lintRelease :scanner-host:assembleRelease -PscannerVersionName=<semver> -PscannerVersionCode=<increasing integer>
```
Without signing variables, local release assembly produces an unsigned APK for inspection. Partial signing configuration fails immediately. Debug/E2E builds retain their existing development signing and default version unless explicitly overridden. Unsigned and debug APKs must not be distributed as production releases.
## Upgrading the existing TC22
## Upgrade and Zebra rollout
Android updates in place only when the signer is compatible and `versionCode` does not decrease. The existing development-signed TC22 installation cannot be upgraded by an unrelated production key. Reinstalling would remove enrollment and session state. Preserve its test evidence, plan the migration explicitly, and never uninstall or reset the operator's device or application implicitly.
Android accepts an in-place application update only with a compatible signing identity and version code. The existing development-signed TC22 installation cannot simply be replaced by an unrelated production certificate. Preserve its test evidence and explicitly plan that migration: reinstalling removes private enrollment/session state and requires enrollment/approval again. Do not silently uninstall the operator's application to make a release install work.
## Not yet established
For Zebra devices, qualify the device-provided EMDK scanner path described in [Zebra scanning](ZEBRA-SCANNING.md). Production must exclude the synthetic scan receiver and compile-only SDK implementation. Verify actual trigger capture, repeated and empty reads, rotation/reacquisition, background/leave/logout release, and restoration of scanner ownership to other applications. The production path requires no DataWedge profile changes; do not persistently disable DataWedge or treat its recipient signature check as sender authentication. Complete the separately signed ordinary-UID broadcast rejection test against the isolated production-path QA build, then qualify Back/Apps navigation, session termination, reconnect behavior and photo capture using the actual signed release APK. Subsequent customer updates should preserve server selection and device enrollment and keep the same signing identity. Customer branding comes from server metadata and does not require rebuilding the APK.
If a delivery path is later chosen, an APK from it could support manual or MDM distribution. Play distribution, managed enterprise rollout, signing-key rotation and a production backend deployment need their own qualified configuration; this workflow does not claim to establish them.
Production key and its backup, channel choice, Gitea release workflow and secrets, a signed-APK device acceptance, signing-key rotation and Play or managed-enterprise configuration. SDK development publication is done and the completed SDK publication and security audits are not reopened here. Still pending: the companion standalone owner dependencies, production signing and channel selection, qualification of an actual new production APK, and the first release (see [TODO](TODO.md)).
+4
View File
@@ -1,3 +1,7 @@
## November 4 — signing readiness documentation
[RELEASING.md](RELEASING.md) was rewritten as concise Gitea-first release readiness. This documentation update has been checked against the actual Gradle configuration and Gitea caller. No keys, secrets, tags, releases, CI, build or source were changed. Central main Android lifecycle 3634 passed and published SDK AARs but is not a scanner-APK signing pipeline. Local Gradle signing via `SCANNER_KEYSTORE`/`SCANNER_STORE_PASSWORD`/`SCANNER_KEY_ALIAS`/`SCANNER_KEY_PASSWORD` exists; release builds are unsigned without them. Production signing still needs the owner's channel decision (direct/MDM vs Play), an organization-owned key with secure storage and a backup owner, and signed-APK device acceptance. The operator reported manual acceptance all-pass on November 4; the last recorded installed build was development-signed. Signing/distribution remains last; older entries below are historical.
## November 3 — verified widget-state reset fix
`ProcessViewModel.reset()` now clears `widgetState` so last scanner feedback no longer carries into a new run. The regression proves ordinary step updates retain feedback and reset clears it. Root verified all 57 mobileapp JVM tests (zero failures/errors/skips), scanner lint (zero errors/warnings), debug APK assembly and the 255-file standards gate. Prior head `9cf789c` passed hosted CI3553 in 16m22s; this fix still needs its own hosted checks and normal PR10 → PR8 → main integration. The installed APK and manual acceptance are separate gates; signing/distribution remains deferred.
+6
View File
@@ -1,4 +1,10 @@
# TODO
Current status (November 4):
- [x] Rewrite [RELEASING.md](RELEASING.md) as concise Gitea-first signing/release readiness; legacy GitHub workflows marked historical. No keys, secrets, tags, releases, CI, build or source changed.
- [ ] Owner decisions for production signing: distribution channel (direct/MDM vs Play with Play App Signing and a separate upload key), organization-owned key (existing or new; chosen before first release if the same signature is needed across channels), managed secret storage and backup owner, and where signed builds run (no Gitea scanner-release workflow exists yet).
- [ ] Verify the actual signed APK (non-debuggable, fingerprint, increasing versionCode, update over prior build) on a device before rollout. The development-signed TC22 cannot upgrade in place to an unrelated key; never uninstall/reset it implicitly.
Latest November 3 checkpoint (current; supersedes the older entries below):
- [x] Android `f72b90d` the signed application-code commit published in PR 10 (SSH read-back verified); Gitea CI 3480 succeeded (unit-test, device-test, package, secrets, gate); clean merge-tree with `main` b75fdac, nothing merged. Pull requests #8 and #10 remain open; merge and release not done. This update changes documentation only.