diff --git a/.claude/skills/android-expert/SKILL.md b/.claude/skills/android-expert/SKILL.md index bf4f49affe..b5c63a5324 100644 --- a/.claude/skills/android-expert/SKILL.md +++ b/.claude/skills/android-expert/SKILL.md @@ -738,125 +738,29 @@ fun SignerIntegration(accountViewModel: AccountViewModel) { ## 6. Build Configuration -### Android Block +Build files — the `android {}` block, the version catalog, dependencies, +Proguard/R8, and Desktop packaging — are **gradle-expert's** domain. Use +`/gradle-expert` instead of duplicating that guidance here. In particular, the +app version and the Android `versionCode` both live in +`gradle/libs.versions.toml` (`app` / `appCode`); `amethyst/build.gradle.kts` +reads both from the catalog, so a release bump is a single-file edit. + +The one build detail that is genuinely Android-specific — not generic Gradle — +is the **product-flavor split** that ships two channels from one codebase: -**build.gradle (Amethyst pattern):** ```gradle -android { - namespace = 'com.vitorpamplona.amethyst' - compileSdk = 37 // from libs.versions.toml android-compileSdk — check there, it drifts - - defaultConfig { - applicationId = "com.vitorpamplona.amethyst" - minSdk = 26 // Android 8.0 (Oreo) - targetSdk = 37 // android-targetSdk in libs.versions.toml - versionCode = 448 - versionName = generateVersionName(libs.versions.app.get(), rootDir) - - vectorDrawables { - useSupportLibrary = true - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - - buildFeatures { - compose = true - buildConfig = true // Enable BuildConfig access - } - - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } - - packaging { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } - - // Product flavors for Play Store vs F-Droid - flavorDimensions = ["channel"] - productFlavors { - create("play") { - dimension = "channel" - // Firebase, Google services - } - create("fdroid") { - dimension = "channel" - // UnifiedPush, open-source alternatives - } - } -} - -kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_21 - } +flavorDimensions = ["channel"] +productFlavors { + create("play") { dimension = "channel" } // Firebase, Google services + create("fdroid") { dimension = "channel" } // UnifiedPush, open-source only } ``` -### Dependencies +`play` carries Firebase/Google services; `fdroid` swaps them for UnifiedPush and +open-source alternatives so the F-Droid build stays proprietary-free. -**Key Android Dependencies:** -```gradle -dependencies { - // Compose BOM - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.tooling.preview) - - // Navigation - implementation(libs.androidx.navigation.compose) - - // Lifecycle - implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.androidx.lifecycle.viewmodel.compose) - - // Activity - implementation(libs.androidx.activity.compose) - - // Accompanist - implementation(libs.accompanist.permissions) - - // Shared module - implementation(project(":commons")) - implementation(project(":quartz")) -} -``` - -### Proguard Rules - -**Common Rules for Amethyst:** -```proguard -# Keep Kotlin metadata --keep class kotlin.Metadata { *; } - -# Keep Nostr event classes --keep class com.vitorpamplona.quartz.events.** { *; } - -# Keep serialization --keepattributes *Annotation*, InnerClasses --dontnote kotlinx.serialization.AnnotationsKt - -# OkHttp --dontwarn okhttp3.** --keep class okhttp3.** { *; } - -# Compose --keep class androidx.compose.** { *; } --dontwarn androidx.compose.** -``` - -**Reference:** See `references/proguard-rules.md` for complete Proguard configuration. - -### APK Optimization - -**Reference:** See `scripts/analyze-apk-size.sh` for APK size analysis. +Proguard/R8 rules: see `references/proguard-rules.md`. APK size analysis: +`scripts/analyze-apk-size.sh`. ## 7. KMP Android Source Sets diff --git a/.claude/skills/gradle-expert/SKILL.md b/.claude/skills/gradle-expert/SKILL.md index cd806eeff6..853a27620e 100644 --- a/.claude/skills/gradle-expert/SKILL.md +++ b/.claude/skills/gradle-expert/SKILL.md @@ -167,7 +167,7 @@ implementation(libs.jna) **Current project config** (always re-check `gradle/libs.versions.toml` — these drift): ```toml -composeMultiplatform = "1.12.0" # Plugin + runtime +composeMultiplatform = "1.11.1" # Plugin + runtime composeBom = "2026.05.01" # AndroidX Compose BOM kotlin = "2.3.21" ``` diff --git a/.claude/skills/gradle-expert/references/dependency-graph.md b/.claude/skills/gradle-expert/references/dependency-graph.md index 95d5c8817b..a1985b564e 100644 --- a/.claude/skills/gradle-expert/references/dependency-graph.md +++ b/.claude/skills/gradle-expert/references/dependency-graph.md @@ -212,7 +212,7 @@ implementation(compose.ui) // Compose Multiplatform BOM implementation(compose.material3) // Version catalog alignment (re-check libs.versions.toml — these drift) -composeMultiplatform = "1.12.0" +composeMultiplatform = "1.11.1" composeBom = "2026.05.01" // AndroidX Compose ``` **Why:** Two Compose ecosystems (Multiplatform + AndroidX) must align diff --git a/.claude/skills/quartz-integration/SKILL.md b/.claude/skills/quartz-integration/SKILL.md index dd50e7fc98..3747860330 100644 --- a/.claude/skills/quartz-integration/SKILL.md +++ b/.claude/skills/quartz-integration/SKILL.md @@ -7,7 +7,7 @@ description: Integration guide for using the Quartz Nostr KMP library in externa Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects. -**Published artifact**: `com.vitorpamplona.quartz:quartz:1.12.0` (Maven Central) +**Published artifact**: `com.vitorpamplona.quartz:quartz:1.12.1` (Maven Central) **Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`) **License**: MIT @@ -19,7 +19,7 @@ Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr ```toml [versions] -quartz = "1.12.0" +quartz = "1.12.1" [libraries] quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" } @@ -41,7 +41,7 @@ kotlin { ```kotlin dependencies { - implementation("com.vitorpamplona.quartz:quartz:1.12.0") + implementation("com.vitorpamplona.quartz:quartz:1.12.1") } ``` diff --git a/.claude/skills/quartz-integration/references/gradle-setup.md b/.claude/skills/quartz-integration/references/gradle-setup.md index 428096a69f..5283a730f3 100644 --- a/.claude/skills/quartz-integration/references/gradle-setup.md +++ b/.claude/skills/quartz-integration/references/gradle-setup.md @@ -3,7 +3,7 @@ ## Current version ``` -com.vitorpamplona.quartz:quartz:1.12.0 +com.vitorpamplona.quartz:quartz:1.12.1 ``` Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz @@ -16,7 +16,7 @@ Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/qua ```toml [versions] -quartz = "1.12.0" +quartz = "1.12.1" [libraries] quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" } @@ -55,7 +55,7 @@ kotlin { ```kotlin // build.gradle.kts (app module) dependencies { - implementation("com.vitorpamplona.quartz:quartz:1.12.0") + implementation("com.vitorpamplona.quartz:quartz:1.12.1") } ``` @@ -70,7 +70,7 @@ plugins { } dependencies { - implementation("com.vitorpamplona.quartz:quartz:1.12.0") + implementation("com.vitorpamplona.quartz:quartz:1.12.1") // JNA needed for libsodium (NIP-44) on JVM implementation("net.java.dev.jna:jna:5.18.1") } diff --git a/BUILDING.md b/BUILDING.md index 3b60ed105a..941bf4b3f1 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,13 +1,22 @@ # Building Amethyst Desktop -This guide covers building Amethyst Desktop from source, the release pipeline, -and one-time bootstrap steps for distribution channels. +This guide has everything **any fork** needs to build Amethyst from source and +cut its own release: prerequisites, build commands, the CI release pipeline, the +secrets it needs, the distribution channels, and one-time bootstrap steps. + +> **Amethyst maintainers:** the account-specific checklist for shipping the +> official build (Play Console upload, Zapstore `zsp publish` with our nsec, +> secret ownership) lives in [`RELEASE_OPS.md`](RELEASE_OPS.md). This +> file stays fork-generic. - [Prerequisites](#prerequisites) - [Clone + first build](#clone--first-build) +- [Generated & vendored artifacts](#generated--vendored-artifacts) - [Per-format build commands](#per-format-build-commands) - [Asset naming contract](#asset-naming-contract) - [Release runbook](#release-runbook) +- [Secrets the CI needs](#secrets-the-ci-needs) +- [Distribution channels](#distribution-channels) - [Bootstrap runbook (one-time)](#bootstrap-runbook-one-time) - [Troubleshooting installs](#troubleshooting-installs) - [Uninstall + state paths](#uninstall--state-paths) @@ -68,6 +77,29 @@ cd amethyst --- +## Generated & vendored artifacts + +Two build inputs are **generated by tools but committed to the repo**, so a +normal build or release does **not** run either — Gradle just consumes the +checked-in output. You only regenerate them under the specific conditions below, +and each has its own guide: + +| Artifact | Committed at | Regenerate when | Guide | +|---|---|---|---| +| **Material Symbols subset font** | `commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf` | You add/remove a `MaterialSymbol("\uXXXX")` codepoint in `MaterialSymbols.kt`, or bump the upstream font | [`tools/material-symbols-subset/README.md`](tools/material-symbols-subset/README.md) — run `./tools/material-symbols-subset/subset.sh` | +| **Arti (Tor) native libs** | `amethyst/src/main/jniLibs/*.so` | You update the pinned Arti version, change the JNI wrapper, or want to reproduce the binaries | [`tools/arti-build/README.md`](tools/arti-build/README.md) | + +> **Material Symbols is mandatory after icon changes.** The bundled font is a +> ~210-glyph subset; a new codepoint that isn't in it renders as tofu (□) at +> runtime. Regenerate and commit the `.ttf` alongside the `MaterialSymbols.kt` +> change. Reusing an existing codepoint needs no regeneration. + +Both tools have their own prerequisites (`fonttools`/`brotli` for the font; a +Rust toolchain + Android NDK 25+ for Arti) documented in their READMEs — they +are **not** required to build Amethyst from the committed sources. + +--- + ## Per-format build commands | Artifact | Command | Output | @@ -111,11 +143,11 @@ amethyst-desktop---. Where: -| Field | Values | -|---|---------------------------------------------------------| -| `` | Tag stripped of leading `v` (e.g. `1.12.0`) | -| `` | `macos`, `windows`, `linux` | -| `` | `x64`, `arm64` | +| Field | Values | +|---|-------------------------------------------------------| +| `` | Tag stripped of leading `vX.YY.ZZ` | +| `` | `macos`, `windows`, `linux` | +| `` | `x64`, `arm64` | | `` | `dmg`, `msi`, `zip`, `deb`, `rpm`, `AppImage`, `tar.gz` | Single source of truth: [`scripts/asset-name.sh`](scripts/asset-name.sh). @@ -124,10 +156,10 @@ any change is a breaking contract. Examples: -- `amethyst-desktop-1.12.0-macos-x64.dmg` -- `amethyst-desktop-1.12.0-macos-arm64.dmg` -- `amethyst-desktop-1.12.0-windows-x64.msi` -- `amethyst-desktop-1.12.0-linux-x64.AppImage` +- `amethyst-desktop-1.12.1-macos-x64.dmg` +- `amethyst-desktop-1.12.1-macos-arm64.dmg` +- `amethyst-desktop-1.12.1-windows-x64.msi` +- `amethyst-desktop-1.12.1-linux-x64.AppImage` --- @@ -136,38 +168,37 @@ Examples: The release flow is driven by a tag push. Every cut ships Android + Desktop + Quartz library in one pipeline. -1. **Bump the app version** in `gradle/libs.versions.toml`: +1. **Bump the app version and Android `versionCode`** in + `gradle/libs.versions.toml` (`appCode` is a monotonic integer — it must + increment even when `app` is unchanged): ```toml [versions] app = "1.08.1" # new semver + appCode = "449" # Android versionCode ``` -2. **Bump Android `versionCode`** in `amethyst/build.gradle` (monotonic integer, - must increment even for same `versionName`): + `amethyst/build.gradle.kts` reads both from the catalog + (`versionCode = libs.versions.appCode.get().toInt()`), so there is nothing + else to edit. - ```groovy - versionCode = 448 - versionName = generateVersionName(libs.versions.app.get()) - ``` - -3. **Commit + tag + push**: +2. **Commit + tag + push**: ```bash - git commit -am "chore(release): 1.12.0" - git tag -s v1.12.0 -m "Release 1.12.0" + git commit -am "chore(release): 1.12.1" + git tag -s v1.12.1 -m "Release 1.12.1" git push && git push --tags ``` -4. **Wait** for the `Create Release Assets` workflow to finish (~25–30 min). +3. **Wait** for the `Create Release Assets` workflow to finish (~25–30 min). -5. **Verify**: +4. **Verify**: - GH Release contains 8 desktop assets + 12 Android assets - Asset sizes look sane (see §Enforce asset size budget — CI auto-fails at 1 GB/asset) - Intel + ARM DMGs both present - Android flow unchanged -6. **Stable vs prerelease** — a tag containing `-rc`, `-beta`, `-alpha`, `-dev`, +5. **Stable vs prerelease** — a tag containing `-rc`, `-beta`, `-alpha`, `-dev`, or `-snapshot` is auto-classified as prerelease. Stable tags trigger the Homebrew + Winget bump workflows. @@ -203,19 +234,96 @@ uninstall before a new release. Leave it alone forever. --- +## Secrets the CI needs + +The `Create Release Assets` workflow reads these from GitHub repo secrets. A +fork must provide its **own** values — none are inherited. (`GITHUB_TOKEN` is +provided automatically; everything else you set yourself.) + +| Secret | What it is | Used for | +|---|---|---| +| `SIGNING_KEY` | Base64 of your **Android keystore** (`.jks`/`.keystore`) | Signs the Play + F-Droid **AAB and APK** | +| `KEY_ALIAS` | Keystore key alias | Same Android signing step | +| `KEY_STORE_PASSWORD` | Keystore password | Same | +| `KEY_PASSWORD` | Key password | Same | +| `SONATYPE_USERNAME` | Maven Central (Sonatype) user token name | Publishing the `quartz` library | +| `SONATYPE_PASSWORD` | Maven Central user token password | Same | +| `SIGNING_PRIVATE_KEY` | **GPG/PGP** private key, ASCII-armored | Signs the Maven artifacts (Central requires it) | +| `SIGNING_PASSWORD` | Passphrase for that GPG key | Same | +| `HOMEBREW_TOKEN` | PAT for `Homebrew/homebrew-cask` | Desktop cask bump (stable tags) | +| `WINGET_TOKEN` | PAT for `microsoft/winget-pkgs` | Desktop winget bump (stable tags) | +| `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Crowdin API creds | Translation sync (separate workflow, not the release) | + +Note the **two distinct signing identities** people often conflate: +`SIGNING_KEY` + `KEY_*` is the **Android keystore**; `SIGNING_PRIVATE_KEY` + +`SIGNING_PASSWORD` is the **GPG key** for Maven Central. They are unrelated. + +Generating the values: + +```bash +# Android keystore → base64 for SIGNING_KEY (one line, no wrapping) +keytool -genkey -v -keystore upload.jks -keyalg RSA -keysize 2048 \ + -validity 10000 -alias upload # creates the keystore (once) +base64 -i upload.jks | tr -d '\n' # paste output into SIGNING_KEY + +# GPG key → armored private key for SIGNING_PRIVATE_KEY +gpg --full-generate-key # create the key (once) +gpg --armor --export-secret-keys # paste output into SIGNING_PRIVATE_KEY +``` + +`SONATYPE_USERNAME`/`SONATYPE_PASSWORD` are a **user token** from + (Account → Generate User Token), not your login. +A fork that doesn't publish a library can drop the `Publish Quartz Lib` step and +the four Sonatype/GPG secrets. + +--- + +## Distribution channels + +One `v*` tag fans out to several channels. Which apply depends on where a fork +distributes; the official Amethyst rollout for each is in +[`RELEASE_OPS.md`](RELEASE_OPS.md). + +| Channel | How it ships | Push or pull | +|---|---|---| +| **GitHub Releases** | The release workflow builds + signs all assets and attaches them to the tag's Release | Automatic (CI) | +| **Maven Central** | Same workflow runs `publishAllPublicationsToMavenCentral` for `quartz` | Automatic (CI) | +| **Google Play** | Download the signed `amethyst-googleplay-.aab` from the GH Release and upload it in Play Console | **Manual push** | +| **F-Droid** | F-Droid's build server detects the new tag and **builds the `fdroid` flavor from source** per its recipe in the external [`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo, then signs + publishes itself | **Pull (build-from-source)** | +| **Zapstore** | The [`zsp`](https://zapstore.dev/) CLI reads [`zapstore.yaml`](zapstore.yaml) and publishes a Nostr software-release event signed with the app's nsec | **Manual push (Nostr)** | +| **Homebrew + Winget** | `bump-homebrew.yml` / `bump-winget.yml` open version-bump PRs on stable tags | Automatic (CI) | + +Two channels need the build to stay split into product flavors (see +`amethyst/build.gradle.kts` → `productFlavors`): + +- **`play`** carries Firebase / Google Play Services (push notifications, ML + Kit, etc.) → the Google Play AAB. +- **`fdroid`** swaps those for UnifiedPush and no-op/open-source + implementations (`amethyst/src/fdroid/…`) so the build is free of proprietary + dependencies → what F-Droid builds and what Zapstore distributes. + +**F-Droid is pull, not push.** We never upload to F-Droid; its server builds our +tagged source. Keeping the `fdroid` flavor proprietary-free and the +`fastlane/metadata/android/` descriptions current is all that's required. F-Droid +reads an optional per-release changelog from +`fastlane/metadata/android/en-US/changelogs/.txt`. + +--- + ## Bootstrap runbook (one-time) ### Secrets to provision in GitHub repo settings +The full secret inventory is in [§ Secrets the CI needs](#secrets-the-ci-needs). +The two that need the most setup care are the package-manager PATs, because of +their token type and scope: + | Secret | Purpose | Scope | |---|---|---| | `HOMEBREW_TOKEN` | Bump Homebrew cask | Fine-grained PAT — `Homebrew/homebrew-cask` only — `Contents: write` + `Pull requests: write` — 90d expiry | | `WINGET_TOKEN` | Submit Winget manifests | Classic PAT — `public_repo` — 90d expiry (dedicated bot account preferred; `vedantmgoyal9/winget-releaser` does not support fine-grained) | -All existing secrets (`SIGNING_KEY`, `SONATYPE_USERNAME`, etc.) remain -unchanged. - -Rotate both on a 90-day cadence. Owner: assigned via `docs/RELEASE_OPS.md` +Rotate both on a 90-day cadence. Owner: assigned via `RELEASE_OPS.md` or equivalent issue tracker. On rotation, paste new token and run `gh workflow run bump-homebrew.yml` on the most recent stable tag to verify. @@ -223,8 +331,8 @@ or equivalent issue tracker. On rotation, paste new token and run ```bash brew bump-cask-pr amethyst-nostr \ - --version 1.12.0 \ - --url "https://github.com/vitorpamplona/amethyst/releases/download/v1.12.0/amethyst-desktop-1.12.0-macos-arm64.dmg" + --version 1.12.1 \ + --url "https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amethyst-desktop-1.12.1-macos-arm64.dmg" ``` The cask filename is `amethyst-nostr` (not `amethyst` — that's taken by a @@ -235,7 +343,7 @@ auto-submits new version bumps on each stable release. ```bash wingetcreate new \ - https://github.com/vitorpamplona/amethyst/releases/download/v1.12.0/amethyst-desktop-1.12.0-windows-x64.msi + https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amethyst-desktop-1.12.1-windows-x64.msi ``` Set `PackageIdentifier = VitorPamplona.Amethyst`. After the first manifest is diff --git a/README.md b/README.md index 5f72d7de3e..3407892f4b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ Join the social network you control. [![PlayStore downloads](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.vitorpamplona.amethyst%26gl%3DUS%26hl%3Den%26l%3DPlayStore%26m%3D%24shortinstalls)](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst) [![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/vitorpamplona/amethyst) -[![JitPack version](https://jitpack.io/v/vitorpamplona/amethyst.svg)](https://jitpack.io/#vitorpamplona/amethyst) +[![Maven Central](https://img.shields.io/maven-central/v/com.vitorpamplona.quartz/quartz?label=Quartz%20%28Maven%20Central%29&labelColor=27303D&color=0877d2)](https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz) +[![JitPack snapshots](https://img.shields.io/badge/Quartz%20snapshots-JitPack-27303D?labelColor=27303D&color=0877d2)](https://jitpack.io/#vitorpamplona/amethyst) [![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml) [![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/vitorpamplona/amethyst) @@ -269,18 +270,16 @@ For the Play build: ## Deploying -Full release + bootstrap runbooks (Android AAB upload, desktop packaging, -Homebrew cask, Winget manifest, Apple Developer signing budget time-box) live -in [BUILDING.md § Release runbook](BUILDING.md#release-runbook) and -[BUILDING.md § Bootstrap runbook (one-time)](BUILDING.md#bootstrap-runbook-one-time). +A release is one tag push. Bump `app` and `appCode` in +`gradle/libs.versions.toml`, then `git tag -s vX.Y.Z && git push --tags` — the +`Create Release Assets` workflow builds and signs every Android, desktop, CLI, +and Maven artifact, and Homebrew + Winget auto-bump on stable tags. -TL;DR for cutting a release: - -1. Bump `app` in `gradle/libs.versions.toml` (e.g. `"1.08.1"`) -2. Bump `versionCode` in `amethyst/build.gradle` -3. `git commit -am "chore(release): 1.08.1" && git tag -s v1.08.1 && git push --tags` -4. Wait for `Create Release Assets` workflow — 20 Android assets + 8 desktop assets go live on GH Release; Homebrew + Winget auto-bump on stable tags -5. Upload AAB to Play Store manually (existing step) +- **[BUILDING.md](BUILDING.md)** — everything any fork needs: build commands, + the CI pipeline, the secrets it requires, and the distribution channels. +- **[RELEASE_OPS.md](RELEASE_OPS.md)** — the Amethyst maintainers' + ship checklist: the manual Play Store upload, Zapstore `zsp publish`, F-Droid + pull, and release-notes publishing. ## Using the Quartz library @@ -298,20 +297,43 @@ repositories { Add the following line to your `commonMain` dependencies: ```gradle -implementation('com.vitorpamplona.quartz:quartz:1:05.0') +implementation('com.vitorpamplona.quartz:quartz:1.12.1') ``` Variations to each platform are also available: ```gradle -implementation('com.vitorpamplona.quartz:quartz-android:1:05.0') -implementation('com.vitorpamplona.quartz:quartz-jvm:1:05.0') -implementation('com.vitorpamplona.quartz:quartz-iosarm64:1:05.0') -implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:1:05.0') +implementation('com.vitorpamplona.quartz:quartz-android:1.12.1') +implementation('com.vitorpamplona.quartz:quartz-jvm:1.12.1') +implementation('com.vitorpamplona.quartz:quartz-iosarm64:1.12.1') +implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:1.12.1') ``` Check versions on [MavenCentral](https://central.sonatype.com/search?q=com.vitorpamplona.quartz) +#### Snapshots (JitPack) + +Tagged releases go to Maven Central. For **pre-release / snapshot** builds — +e.g. to test an unreleased fix straight from `main` or a feature branch — use +[JitPack](https://jitpack.io/#vitorpamplona/amethyst), which builds the module +on demand from any git ref: + +```gradle +repositories { + maven { url = uri("https://jitpack.io") } +} + +dependencies { + // version can be a tag, a commit hash, or "-SNAPSHOT" + implementation("com.github.vitorpamplona.amethyst:quartz:main-SNAPSHOT") +} +``` + +The resolvable refs and the exact module coordinates are listed on the +[JitPack page](https://jitpack.io/#vitorpamplona/amethyst). Prefer a Maven +Central release for anything shipping to production — JitPack snapshots are not +guaranteed stable. + ### How to use Manage logged in users with the `KeyPair` class diff --git a/RELEASE_OPS.md b/RELEASE_OPS.md new file mode 100644 index 0000000000..920eaf7d16 --- /dev/null +++ b/RELEASE_OPS.md @@ -0,0 +1,204 @@ +# Release Ops (Amethyst maintainers) + +This is the **operational checklist the Amethyst team follows to ship a +release** — the account-specific, push-the-buttons side of cutting a version. +The *generic* build/release mechanics (how the CI pipeline works, the asset +naming contract, the secret names a fork must set, desktop packaging) live in +[`BUILDING.md`](BUILDING.md). Read that first; this doc only covers what is +specific to shipping the official Amethyst artifacts. + +> Forks: you do **not** need this file. `BUILDING.md` has everything you need to +> build and release your own fork. This describes our accounts and channels. + +--- + +## At a glance + +A release is one tag push that fans out to five distribution channels: + +| Channel | Mechanism | Who pushes | +|---|---|---| +| **GitHub Releases** | Automatic — the `Create Release Assets` workflow builds + signs everything on the `v*` tag | CI | +| **Google Play** | **Manual** — download the signed AAB from the GH Release, upload in Play Console | Maintainer | +| **F-Droid** | **Pull** — F-Droid's build server builds the `fdroid` flavor from source when it sees the new tag | F-Droid (we just maintain the recipe + metadata) | +| **Zapstore** | `zsp publish` reads `zapstore.yaml`, signs a Nostr release event with Amethyst's nsec | Maintainer | +| **Homebrew + Winget** | Automatic — `bump-homebrew.yml` / `bump-winget.yml` fire on stable tags | CI | + +Maven Central (the `quartz` library) also publishes automatically from the same +workflow. + +--- + +## 1. Pre-tag checklist + +1. **Bump the version** in `gradle/libs.versions.toml` — both keys: + ```toml + app = "1.12.1" # semver, drives every module + the tag + appCode = "449" # Android versionCode, monotonic — must increment + ``` + That single edit propagates to Android (`versionName`/`versionCode`), + Desktop & CLI (`packageVersion`), `quartz` (Maven version) and `geode` + (`RelayInfo.VERSION`). Nothing else hardcodes the version. + +2. **Write the changelog** as `docs/changelog/vMAJOR.MINOR.PP.md` (zero-padded, + e.g. `v1.12.01.md`) and add it to `docs/changelog/README.md`. Follow the + house style: plain text, short verb-first sentences. + +3. **Publish the release-notes note on Nostr** with Amethyst's account and paste + its event id into `amethyst/build.gradle.kts`: + ```kotlin + buildConfigField("String", "RELEASE_NOTES_ID", "\"\"") + ``` + This id is what the in-app drawer's "Release Notes" link and the donation + card open (`DrawerContent.kt`, `ShowDonationCard.kt`). It must point at the + note for *this* version, so publish the note **before** tagging and commit + the new id together with the version bump. + + + +4. **Sanity-build locally** (optional but cheap): `./gradlew assembleRelease` + and a desktop `packageDistributionForCurrentOS`, or run the workflow's + dry-run (see BUILDING.md § Dry-run). + +--- + +## 2. Cut the release + +Commit, tag, push — see [`BUILDING.md` § Release runbook](BUILDING.md#release-runbook) +for the exact commands. The tag must equal `app` from the catalog (the workflow +asserts this and fails fast otherwise). A clean `vMAJOR.MINOR.PATCH` tag is +classified **stable** and triggers the Homebrew/Winget bumps; anything with a +`-rc`/`-beta`/`-alpha`/`-dev` suffix is a prerelease and skips them. + +When the `Create Release Assets` workflow finishes (~25–30 min) the GH Release +holds, per the asset-name contract: + +- **Android:** 5 Google Play APKs + 5 F-Droid APKs + 2 AABs + (`amethyst-googleplay-*-v…apk` / `.aab`, `amethyst-fdroid-*-v…apk` / `.aab`) +- **Desktop:** 8 assets (DMG/MSI/DEB/RPM/AppImage/zip/tar.gz) +- **CLI:** the `amy` artifacts +- **Maven Central:** `com.vitorpamplona.quartz:quartz:` published + +--- + +## 3. Per-channel shipping + +### GitHub Releases — automatic +Nothing to do beyond pushing the tag. Verify the asset count and that Intel + +ARM DMGs are both present (BUILDING.md § Verify). + +### Google Play — manual upload +1. Download `amethyst-googleplay-.aab` from the GH Release. +2. Play Console → app `com.vitorpamplona.amethyst` → **Production** (or the + staged-rollout track we're using) → create release → upload the AAB. +3. The release notes field can reuse the `docs/changelog` text. +4. Roll out. + +### F-Droid — pull / build-from-source +F-Droid does **not** accept an upload from us. Its build server polls the repo, +and when it sees the new `v*` tag it builds the **`fdroid` product flavor** from +source (reproducibly) per the recipe in the separate +[`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo +(`metadata/com.vitorpamplona.amethyst.yml`), then signs and publishes to the +F-Droid repo on its own cadence. + +What we own to keep that working: +- The **`fdroid` flavor** (`amethyst/src/fdroid/…`) must stay free of + proprietary deps — it swaps Firebase/Google services for UnifiedPush and + no-op/open implementations (ML Kit, writing assistant, push). Google-only + libraries live behind the `play` flavor. +- The fastlane metadata under `fastlane/metadata/android/` (descriptions, + images). F-Droid reads per-version changelogs from + `fastlane/metadata/android/en-US/changelogs/.txt` if present — + add one (e.g. `449.txt`) when we want a changelog shown on F-Droid; otherwise + none is displayed. +- The `AutoUpdateMode`/`UpdateCheckMode` in the fdroiddata recipe tracks tags, + so a correct `vX.Y.Z` tag + bumped `versionCode` is usually all F-Droid needs. + +After a release, just confirm F-Droid picked up the new version (it can lag a +few days): . + +### Zapstore — `zsp publish` with Amethyst's nsec +[Zapstore](https://zapstore.dev/) is a Nostr-native app store. The `zsp` CLI +reads [`zapstore.yaml`](zapstore.yaml) at the repo root (name, summary, +description, tags, license, screenshots, and the `variants` regexes that match +our `*-fdroid-*.apk` / `*-googleplay-*.apk` GH-release assets), then publishes a +signed software-release event to Nostr relays. + +```bash +# from the repo root, after the GH Release assets exist +zsp publish +``` + +It signs with **Amethyst's nsec** — provide the key the way `zsp` expects +(env var / prompt / its own config), never commit it. + +### Homebrew + Winget — automatic +`bump-homebrew.yml` and `bump-winget.yml` fire on stable tags and open PRs +against `Homebrew/homebrew-cask` (cask `amethyst-nostr`) and +`microsoft/winget-pkgs` (`VitorPamplona.Amethyst`). No action unless one fails — +then see BUILDING.md § Bootstrap and § Incident response. + +--- + +## 4. Operated infrastructure + +### Push notification server + +The Google Play (FCM) flavor delivers push through a server we operate at +`push.amethyst.social`, built from +[`vitorpamplona/amethyst-push-notif-server`](https://github.com/vitorpamplona/amethyst-push-notif-server). +It registers devices, watches their NIP-65 inbox / NIP-17 DM relays, and sends +wake-up pushes. + +- **`play` flavor** → push via this server (Firebase/FCM). +- **`fdroid` flavor** → UnifiedPush through a distributor app the user installs + (e.g. ntfy); it does **not** use our server. +- Both are complemented by the on-device always-on `NotificationRelayService` + (see [`PULL_NOTIFICATION.md`](PULL_NOTIFICATION.md)), which keeps the user's + relay connections alive without any push server at all. + +The push server has its **own repo, deploy, and release cadence** — a normal app +release does **not** redeploy it. Coordinate a server deploy only when the app +changes the registration/push contract (token format, payload, or endpoint), so +the running server stays compatible with the shipped app. + + + +--- + +## 5. Secrets ownership & rotation + +The workflow's required secrets and what they sign are inventoried generically +in [`BUILDING.md` § Secrets](BUILDING.md#secrets-the-ci-needs). Amethyst-specific +ownership: + +| Secret(s) | Protects | Rotation | +|---|---|---| +| `SIGNING_KEY`, `KEY_ALIAS`, `KEY_STORE_PASSWORD`, `KEY_PASSWORD` | The **Android upload keystore** — losing/leaking it is the worst case; Play app signing identity | Keep the keystore backed up offline; never rotate casually (Play upload key reset is a support process) | +| `SONATYPE_USERNAME`, `SONATYPE_PASSWORD` | Maven Central namespace `com.vitorpamplona` | On compromise | +| `SIGNING_PRIVATE_KEY`, `SIGNING_PASSWORD` | The **GPG key** signing Maven artifacts | Per GPG key expiry | +| `HOMEBREW_TOKEN`, `WINGET_TOKEN` | Cask + winget bump PRs | **90-day cadence** (see BUILDING.md § Bootstrap) | +| `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Translation sync | On compromise | + +Owner assignments and rotation reminders live with the team (issue tracker). + + + +--- + +## 6. Post-release verification + +- [ ] GH Release: expected asset count, Intel + ARM DMGs, sizes sane. +- [ ] Maven Central: `quartz:` resolves (allow propagation time). +- [ ] Play Console: rollout started, no policy rejection. +- [ ] Zapstore: release event visible. +- [ ] F-Droid: new version detected (may lag days). +- [ ] Homebrew + Winget bump PRs opened (stable only). +- [ ] In-app "Release Notes" link opens the note matching `RELEASE_NOTES_ID`. +- [ ] Push still works on a `play` build (only if the push contract changed — + see § 4); UnifiedPush still works on an `fdroid` build. + +If anything ships broken, see [`BUILDING.md` § Incident response](BUILDING.md#incident-response). diff --git a/amethyst/build.gradle.kts b/amethyst/build.gradle.kts index a55d77093a..c44598b810 100644 --- a/amethyst/build.gradle.kts +++ b/amethyst/build.gradle.kts @@ -76,7 +76,10 @@ android { libs.versions.android.targetSdk .get() .toInt() - versionCode = 448 + versionCode = + libs.versions.appCode + .get() + .toInt() versionName = generateVersionName(libs.versions.app.get(), rootDir) buildConfigField("String", "RELEASE_NOTES_ID", "\"40e817712e397c07ba31784a92fa474aa095896a828c0e2dea0d09c60d49ee1e\"") diff --git a/build.gradle.kts b/build.gradle.kts index 4ffcd6c263..3a9b1df6dd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ plugins { } // Shared app version for all subprojects — read from gradle/libs.versions.toml. -// Android versionCode stays local in amethyst/build.gradle.kts (must be monotonic int). +// Android versionCode is the `appCode` entry in the same catalog (must be monotonic int). // Desktop packageVersion inherits via project.version in desktopApp/build.gradle.kts. val appVersion = libs.versions.app.get() diff --git a/compound-engineering.local.md b/compound-engineering.local.md deleted file mode 100644 index db48fb9f9d..0000000000 --- a/compound-engineering.local.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -review_agents: - - kieran-typescript-reviewer - - performance-oracle - - architecture-strategist - - code-simplicity-reviewer - - security-sentinel ---- - -## Review Context - -Kotlin Multiplatform desktop app (Compose Desktop). Nostr client with WebSocket relay connections. -Key patterns: FeedViewModel + FeedFilter + FeedContentState (cache-centric), Coordinator for relay subscription management. diff --git a/docs/changelog/README.md b/docs/changelog/README.md index 370aa06488..9a03e5721d 100644 --- a/docs/changelog/README.md +++ b/docs/changelog/README.md @@ -2,6 +2,7 @@ Release notes for Amethyst, one file per version. Files are named with zero-padded version numbers so they sort correctly in any file browser. Use [`TEMPLATE.md`](TEMPLATE.md) as the starting point for the next release. +- [v1.12.1 — Health Connect Workouts, Share as Image](v1.12.01.md) - [v1.12.0 — Payments, Private Posts & Workouts](v1.12.00.md) - [v1.11.0 — NIP-52 Calendars, On-Chain Zap Polish](v1.11.00.md) - [v1.10.0 — On-Chain zaps](v1.10.00.md) diff --git a/docs/changelog/v1.12.01.md b/docs/changelog/v1.12.01.md new file mode 100644 index 0000000000..7ea1721545 --- /dev/null +++ b/docs/changelog/v1.12.01.md @@ -0,0 +1,55 @@ +# v1.12.1: Health Connect Workouts, Share as Image + +Highlights: + +- Adds Health Connect: detect recorded workouts and suggest a NIP-101e post. +- Adds Share as Image: turn any note into an uploaded image and share it. +- Adds Immersive scrolling: hide the OS system bars while reading and in full-screen media. + +## New Features + +### Workouts + +- Detects workouts from Health Connect and suggests a kind 1301 post. +- Re-scans Health Connect when the app resumes. +- Adds a recent-workouts carousel to the New Workout composer. +- Moves Health Connect entirely into the New Workout composer. +- Defaults the workout distance unit to the phone's measurement system. +- Renders workout notes through the kind-1 text pipeline. +- Shows workout metrics in the viewer's own units. +- Records the source app or device name from Health Connect. +- Aligns Health Connect import with RUNSTR (active calories and title). +- Adds a Compose setting to disable workout suggestions. +- Limits the feed workout banner to today's workouts. + +### Sharing + +- Shares any note as an uploaded image. +- Adds a share-as-image screen with an image preview. + +### Reading + +- Hides the OS status bar during scroll immersive mode. +- Hides the OS system bars during full-screen media. + +## Improvements and Bug fixes + +- Modernizes the New Workout composer. +- Tightens the New Workout composer vertical spacing. +- Centers the activity type chips in the New Workout composer. +- Uses the Add icon on the New Workout FAB for consistency. +- Stops the connect card from flashing on every screen open. +- Keeps Health Connect prefs off the main thread (StrictMode). +- Adds the Android 14+ Health Connect permission rationale activity-alias. +- Polishes the share-as-image screen. +- Fixes a Tor race: sets Active deterministically so the bootstrap callback can't race it. + +## Desktop + +- Ships a consistent macOS .icns and multi-size Windows .ico. +- Excludes leaked kotlinx-coroutines-test from the release dmg. + +## Translations + +- Promotes Kenyan Swahili to the base resource qualifier. +- New Crowdin translations. diff --git a/geode/build.gradle.kts b/geode/build.gradle.kts index 5b4ea35fa9..bc41967f3d 100644 --- a/geode/build.gradle.kts +++ b/geode/build.gradle.kts @@ -19,9 +19,34 @@ kotlin { } } +// Generate a BuildConfig.kt carrying the app version from the catalog so +// RelayInfo.VERSION (reported over NIP-11) tracks releases automatically +// instead of being a hand-bumped literal. +val generateVersionFile by tasks.registering { + val versionValue = libs.versions.app.get() + val outDir = layout.buildDirectory.dir("generated/version/kotlin") + inputs.property("version", versionValue) + outputs.dir(outDir) + doLast { + val file = outDir.get().file("com/vitorpamplona/geode/BuildConfig.kt").asFile + file.parentFile.mkdirs() + file.writeText( + buildString { + appendLine("// Generated by the :geode build — do not edit.") + appendLine("package com.vitorpamplona.geode") + appendLine() + appendLine("internal object BuildConfig {") + appendLine(" const val VERSION = \"$versionValue\"") + appendLine("}") + }, + ) + } +} + sourceSets { main { kotlin.srcDir("src/main/kotlin") + kotlin.srcDir(generateVersionFile) } test { kotlin.srcDir("src/test/kotlin") diff --git a/geode/src/main/kotlin/com/vitorpamplona/geode/RelayInfo.kt b/geode/src/main/kotlin/com/vitorpamplona/geode/RelayInfo.kt index 5ee2e5d772..5cde5a1989 100644 --- a/geode/src/main/kotlin/com/vitorpamplona/geode/RelayInfo.kt +++ b/geode/src/main/kotlin/com/vitorpamplona/geode/RelayInfo.kt @@ -38,10 +38,10 @@ data class RelayInfo( val json: String by lazy { JsonMapper.toJson(document) } companion object { - const val NAME = "geode" - const val DESCRIPTION = "Embedded Nostr relay from the Amethyst quartz library." + const val NAME = "Geode" + const val DESCRIPTION = "Nostr relay from Amethyst" const val SOFTWARE = "https://github.com/vitorpamplona/amethyst/tree/main/geode" - const val VERSION = "1.12.0" + const val VERSION = BuildConfig.VERSION /** * NIPs this relay implements out of the box. Single source of diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e7bfa05d5..a6fc33aa3b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,9 @@ [versions] # Amethyst app version — single source of truth consumed by both Android (amethyst/) -# and Desktop (desktopApp/). Android versionCode is bumped independently in -# amethyst/build.gradle because it must be a monotonic integer. -app = "1.12.0" +# and Desktop (desktopApp/). `appCode` is the Android versionCode: a monotonic +# integer that must increment on every release, even when `app` is unchanged. +app = "1.12.1" +appCode = "449" accompanistAdaptive = "0.37.3" cachemapVersion = "0.2.4" composeMultiplatform = "1.11.1" diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index cb70a4f5df..918277dc5a 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -449,7 +449,9 @@ mavenPublishing { coordinates( groupId = "com.vitorpamplona.quartz", artifactId = "quartz", - version = "1.12.0", + // Library version tracks the app version in gradle/libs.versions.toml, + // bumped in lockstep with each release. + version = libs.versions.app.get(), ) // Configure publishing to Maven Central