diff --git a/.gitignore b/.gitignore index a1cf45f0..3b1d1d2b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,7 @@ local.properties # google-services.json is checked in — API keys are not secret (embedded in APK) relay/target/ .claude/worktrees/ -.signing/ -.signing +# Signing keystores and password files are private per clone. +# Only the *.example templates are tracked. +.signing/* +!.signing/release.properties.example diff --git a/.signing/release.properties.example b/.signing/release.properties.example new file mode 100644 index 00000000..0b2ac6b2 --- /dev/null +++ b/.signing/release.properties.example @@ -0,0 +1,10 @@ +# Template for .signing/release.properties — copy this file to +# .signing/release.properties and fill in the real passwords locally. +# +# The real file is gitignored (see .gitignore). This .example file is +# tracked so new clones know the expected format. +# +# Alternative: set ROUSE_RELEASE_STORE_PASSWORD and ROUSE_RELEASE_KEY_PASSWORD +# environment variables instead. Env vars take precedence over this file. +storePassword=your-release-store-password +keyPassword=your-release-key-password diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c607ad55..edf6c497 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -7,16 +9,36 @@ plugins { alias(libs.plugins.google.services) } +// Loads a value from `.signing/release.properties` if that file exists. +// The file is gitignored; see `.signing/release.properties.example` for format. +val releaseSigningProps: Properties? = run { + val f = rootProject.file(".signing/release.properties") + if (f.exists()) { + Properties().apply { f.inputStream().use { load(it) } } + } else { + null + } +} + +fun loadFromSigningProperties(key: String): String? = releaseSigningProps?.getProperty(key) + android { namespace = "com.rousecontext.app" compileSdk = 36 + // Release passwords are resolved lazily so non-release tasks (lint, tests, + // debug builds) don't require the env vars or .properties file. + val releaseStorePassword = System.getenv("ROUSE_RELEASE_STORE_PASSWORD") + ?: loadFromSigningProperties("storePassword") + val releaseKeyPassword = System.getenv("ROUSE_RELEASE_KEY_PASSWORD") + ?: loadFromSigningProperties("keyPassword") + signingConfigs { create("release") { storeFile = file("${rootProject.projectDir}/.signing/release.keystore") - storePassword = "rousecontext-release" + storePassword = releaseStorePassword keyAlias = "release" - keyPassword = "rousecontext-release" + keyPassword = releaseKeyPassword } getByName("debug") { storeFile = file("${rootProject.projectDir}/.signing/debug.keystore") @@ -26,6 +48,28 @@ android { } } + // Fail fast on release builds if passwords are missing, with a clear message. + gradle.taskGraph.whenReady { + val needsReleaseSigning = allTasks.any { task -> + if (task.project != project || !task.name.contains("Release")) return@any false + task.name.startsWith("package") || + task.name.startsWith("assemble") || + task.name.startsWith("bundle") + } + if (needsReleaseSigning) { + check(releaseStorePassword != null) { + "Release signing requires ROUSE_RELEASE_STORE_PASSWORD env var " + + "or storePassword in .signing/release.properties " + + "(see .signing/release.properties.example)." + } + check(releaseKeyPassword != null) { + "Release signing requires ROUSE_RELEASE_KEY_PASSWORD env var " + + "or keyPassword in .signing/release.properties " + + "(see .signing/release.properties.example)." + } + } + } + defaultConfig { applicationId = "com.rousecontext" minSdk = 24 diff --git a/docs/self-hosting.md b/docs/self-hosting.md index f0af9ef4..91c7d2c5 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -63,7 +63,26 @@ keytool -genkeypair -v \ -dname "CN=Release, O=YourOrg, C=US" ``` -Update `app/build.gradle.kts` signing config blocks to match your passwords, or (better) read them from environment variables. +Release passwords are read at build time from either environment variables or +a local properties file — they are **not** committed to the repo. Choose one: + +Environment variables (preferred for CI): + +```bash +export ROUSE_RELEASE_STORE_PASSWORD=YOUR_RELEASE_PASS +export ROUSE_RELEASE_KEY_PASSWORD=YOUR_RELEASE_PASS +``` + +Or a local properties file (preferred for interactive dev — survives shells): + +```bash +cp .signing/release.properties.example .signing/release.properties +# edit .signing/release.properties and set storePassword / keyPassword +``` + +`.signing/release.properties` is gitignored. Env vars take precedence over the +file. Debug signing still uses a fixed password in `app/build.gradle.kts` +because the debug keystore is not a secret. **Back these up off-machine.** Regenerating a keystore forces users to uninstall (app-data loss) and burns ACME cert quota on re-registration.