From 9fc1d0233ec47e846489b91196aab811635398af Mon Sep 17 00:00:00 2001 From: Dax Pryce Date: Sun, 6 Sep 2026 22:26:22 -0700 Subject: [PATCH 1/2] Support signed local release builds without passwords in argv Building a personal-profile release locally is the only way to obtain that artifact, and two things made it needlessly hazardous. Without the four signing variables no signingConfig is created and AGP emits `*-release-unsigned.apk`. scripts/build-apk copied that into dist/ under a name giving no hint it could not be installed, and exited 0; the failure surfaced much later as INSTALL_PARSE_FAILED_NO_CERTIFICATES. Verify release output with apksigner and fail with an actionable message. Getting the variables into the container also required --remote-env, placing passwords in the host process list, which docs/releases.md already warns against for keytool. Forward all four from localEnv as devcontainer.json already did for AUTOSPEED_PERSONAL_USE_ACK. Unset variables forward as empty, which is the existing no-signing path, so a clean clone is unaffected. Document the local recipe, including that AUTOSPEED_SIGNING_STORE_FILE is a path inside the container. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 379a93aa-5a91-48d4-af89-3c240ef6d47f --- .devcontainer/devcontainer.json | 6 ++++- docs/releases.md | 42 +++++++++++++++++++++++++++++++++ scripts/build-apk | 21 +++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d5adf9f..c776404 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,11 @@ "GRADLE_USER_HOME": "/home/vscode/.gradle" }, "remoteEnv": { - "AUTOSPEED_PERSONAL_USE_ACK": "${localEnv:AUTOSPEED_PERSONAL_USE_ACK}" + "AUTOSPEED_PERSONAL_USE_ACK": "${localEnv:AUTOSPEED_PERSONAL_USE_ACK}", + "AUTOSPEED_SIGNING_STORE_FILE": "${localEnv:AUTOSPEED_SIGNING_STORE_FILE}", + "AUTOSPEED_SIGNING_STORE_PASSWORD": "${localEnv:AUTOSPEED_SIGNING_STORE_PASSWORD}", + "AUTOSPEED_SIGNING_KEY_ALIAS": "${localEnv:AUTOSPEED_SIGNING_KEY_ALIAS}", + "AUTOSPEED_SIGNING_KEY_PASSWORD": "${localEnv:AUTOSPEED_SIGNING_KEY_PASSWORD}" }, "mounts": [ "source=autospeed-gradle-cache,target=/home/vscode/.gradle,type=volume", diff --git a/docs/releases.md b/docs/releases.md index 35282d9..348ac15 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -157,6 +157,48 @@ also permits key rotation later. Verify a build with: and confirm the reported certificate SHA-256 matches the keystore. +## Local release builds + +The workflow above is the only thing that publishes a release, but a signed, +release-quality APK can also be built locally. This is the only way to obtain a +personal-profile artifact, which the workflow never publishes. + +`devcontainer.json` forwards the signing variables from the host environment +into the container, so export them in the shell rather than passing them as +`--remote-env` arguments, where they would be visible in the host process list: + +```bash +export AUTOSPEED_SIGNING_STORE_FILE=/workspaces/autospeed/.release-secrets/autospeed-release.jks +export AUTOSPEED_SIGNING_KEY_ALIAS= +read -rs -p 'store password: ' AUTOSPEED_SIGNING_STORE_PASSWORD && export AUTOSPEED_SIGNING_STORE_PASSWORD +read -rs -p 'key password: ' AUTOSPEED_SIGNING_KEY_PASSWORD && export AUTOSPEED_SIGNING_KEY_PASSWORD +``` + +`AUTOSPEED_SIGNING_STORE_FILE` is a path *inside* the container, so the +keystore has to sit somewhere the workspace mount exposes. `.release-secrets/` +is git-ignored for exactly this, and a keystore kept outside the repository can +be copied there for the build. + +A personal-profile release additionally requires the design section 2.1 +acknowledgment, which is also forwarded from the host: + +```bash +export AUTOSPEED_PERSONAL_USE_ACK='Do not configure or interact with Autospeed while driving. Obey applicable laws and remain attentive.' +./scripts/container-run ./scripts/build-apk personalPlayRelease +``` + +Omitting the signing variables does not fail the build: the signing config is +simply never created and AGP emits `*-release-unsigned.apk`, which cannot be +installed (`INSTALL_PARSE_FAILED_NO_CERTIFICATES`). `scripts/build-apk` +therefore verifies release output with `apksigner` and fails instead of copying +an unusable APK into `dist/` under a name that looks finished. + +Unless `AUTOSPEED_VERSION_NAME` and `AUTOSPEED_VERSION_CODE` are set, a local +build carries the development version (`0.1.0` / `1`). Personal and public +profiles use different application IDs, so a locally built personal APK and a +published public release are separate installations and never update over one +another. + ## Publish After checks pass on `main`, create and push an annotated tag: diff --git a/scripts/build-apk b/scripts/build-apk index c1f5f98..ce196a7 100755 --- a/scripts/build-apk +++ b/scripts/build-apk @@ -37,6 +37,27 @@ if [[ -z "${apk}" ]]; then exit 1 fi +# A release build with no signing configuration still succeeds: AGP simply emits +# `*-release-unsigned.apk`, which this script would then copy to a destination name that gives no +# hint it cannot be installed. Attempting to install it fails much later with +# INSTALL_PARSE_FAILED_NO_CERTIFICATES, so confirm the signature here rather than trusting that a +# successful build implies a usable artifact. +if [[ "${BUILD_TYPE}" == "release" ]]; then + apksigner="$(find "${ANDROID_HOME:-/opt/android-sdk}/build-tools" -type f -name apksigner 2>/dev/null | sort | tail -1)" + if [[ -z "${apksigner}" ]]; then + echo "Cannot locate apksigner, so ${apk} cannot be confirmed as signed." >&2 + exit 1 + fi + if ! "${apksigner}" verify "${apk}" >/dev/null 2>&1; then + echo "${apk} is not signed and cannot be installed." >&2 + echo "Export AUTOSPEED_SIGNING_STORE_FILE, AUTOSPEED_SIGNING_STORE_PASSWORD," >&2 + echo "AUTOSPEED_SIGNING_KEY_ALIAS and AUTOSPEED_SIGNING_KEY_PASSWORD on the host" >&2 + echo "(devcontainer.json forwards them into the container) and build again." >&2 + echo "See docs/releases.md for the local release-build recipe." >&2 + exit 1 + fi +fi + mkdir -p "${DIST_DIR}" readonly DEST="${DIST_DIR}/autospeed-${FLAVOR}-${BUILD_TYPE}.apk" install -m 0644 "${apk}" "${DEST}" From 2ba876bd6c286ad3a6681f9ab1eead405c023173 Mon Sep 17 00:00:00 2001 From: Dax Pryce Date: Sun, 6 Sep 2026 22:42:17 -0700 Subject: [PATCH 2/2] Sort apksigner versions numerically Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/build-apk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-apk b/scripts/build-apk index ce196a7..d885762 100755 --- a/scripts/build-apk +++ b/scripts/build-apk @@ -43,7 +43,7 @@ fi # INSTALL_PARSE_FAILED_NO_CERTIFICATES, so confirm the signature here rather than trusting that a # successful build implies a usable artifact. if [[ "${BUILD_TYPE}" == "release" ]]; then - apksigner="$(find "${ANDROID_HOME:-/opt/android-sdk}/build-tools" -type f -name apksigner 2>/dev/null | sort | tail -1)" + apksigner="$(find "${ANDROID_HOME:-/opt/android-sdk}/build-tools" -type f -name apksigner 2>/dev/null | sort -V | tail -1)" if [[ -z "${apksigner}" ]]; then echo "Cannot locate apksigner, so ${apk} cannot be confirmed as signed." >&2 exit 1