Merge pull request #3245 from vitorpamplona/release/1.12.1

Release 1.12.1
This commit is contained in:
Vitor Pamplona
2026-06-17 11:27:32 -04:00
committed by GitHub
17 changed files with 505 additions and 193 deletions
+17 -113
View File
@@ -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
+1 -1
View File
@@ -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"
```
@@ -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
+3 -3
View File
@@ -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")
}
```
@@ -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")
}
+140 -32
View File
@@ -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-<version>-<family>-<arch>.<ext>
Where:
| Field | Values |
|---|---------------------------------------------------------|
| `<version>` | Tag stripped of leading `v` (e.g. `1.12.0`) |
| `<family>` | `macos`, `windows`, `linux` |
| `<arch>` | `x64`, `arm64` |
| Field | Values |
|---|-------------------------------------------------------|
| `<version>` | Tag stripped of leading `vX.YY.ZZ` |
| `<family>` | `macos`, `windows`, `linux` |
| `<arch>` | `x64`, `arm64` |
| `<ext>` | `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 (~2530 min).
3. **Wait** for the `Create Release Assets` workflow to finish (~2530 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 <KEY_ID> # paste output into SIGNING_PRIVATE_KEY
```
`SONATYPE_USERNAME`/`SONATYPE_PASSWORD` are a **user token** from
<https://central.sonatype.com> (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-<version>.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/<versionCode>.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
+39 -17
View File
@@ -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 "<branch>-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
+204
View File
@@ -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", "\"<new-event-id-hex>\"")
```
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.
<!-- TODO(maintainer): document the exact command/account used to publish the
release-notes note (which signer, which relays). -->
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 (~2530 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:<version>` 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-<version>.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/<versionCode>.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): <https://f-droid.org/packages/com.vitorpamplona.amethyst/>.
### 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.
<!-- TODO(maintainer): document the push-server deploy steps + hosting, and
which app-side changes require a coordinated push-server deploy. -->
---
## 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).
<!-- TODO(maintainer): name the owner per secret and where backups live. -->
---
## 6. Post-release verification
- [ ] GH Release: expected asset count, Intel + ARM DMGs, sizes sane.
- [ ] Maven Central: `quartz:<version>` 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).
+4 -1
View File
@@ -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\"")
+1 -1
View File
@@ -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()
-13
View File
@@ -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.
+1
View File
@@ -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)
+55
View File
@@ -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.
+25
View File
@@ -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")
@@ -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
+4 -3
View File
@@ -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"
+3 -1
View File
@@ -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