da1037423c
Phase 1+2 of multi-platform distribution plan:
- gradle/libs.versions.toml: add `app = "1.08.0"` as single source of truth
- Root build.gradle: allprojects { version = libs.versions.app.get() }
- amethyst/build.gradle: versionName from catalog (versionCode stays local)
- desktopApp/build.gradle.kts: drop hardcoded "1.0.0"; inherit project.version;
add TargetFormat.Rpm; add linux DSL (menuGroup, appCategory, debMaintainer,
rpmLicenseType, rpmPackageVersion with dashes stripped)
- desktopApp/build.gradle.kts: new createReleaseAppImage task wrapping
createReleaseDistributable with linuxdeploy (TargetFormat.AppImage is
broken in Compose 1.10.x — CMP-7101)
- packaging/appimage/: AppRun launcher (sets LD_LIBRARY_PATH for bundled VLC),
amethyst.desktop XDG entry, 512x512 icon extracted from icon.icns
- scripts/asset-name.sh: single source for release asset naming contract
1025 lines
65 KiB
Markdown
1025 lines
65 KiB
Markdown
---
|
||
title: Desktop Multi-Platform Distribution
|
||
type: feat
|
||
status: active
|
||
date: 2026-04-16
|
||
origin: docs/brainstorms/2026-04-16-desktop-multiplatform-distribution-brainstorm.md
|
||
deepened: 2026-04-16
|
||
---
|
||
|
||
# Desktop Multi-Platform Distribution
|
||
|
||
> **Enhancement Summary (2026-04-16)** — deepened via 10 parallel research agents. Scope refined based on findings:
|
||
>
|
||
> - **Scope cut**: AUR + Scoop deferred to follow-up PR. This PR ships **Homebrew + Winget only** (+ all 8 release assets).
|
||
> - **Dropped**: `SHA256SUMS.txt` aggregation (Amethyst Android releases have no checksums; follow existing convention — no cosign/GPG on release).
|
||
> - **Dropped**: draft→publish flip (current `create-release.yml` already uses direct single-shot publish with `draft: false, prerelease: true`; align with existing pattern).
|
||
> - **Dropped**: `createPortableTarGz` / `createPortableZip` Gradle tasks (inline `tar`/`zip` in CI after `createReleaseDistributable`).
|
||
> - **Dropped**: `verify-version` as separate job (merged as first step in each matrix job).
|
||
> - **Dropped**: 8 of 11 template files — Homebrew cask rewritten by `action-homebrew-bump-cask` from live cask; Winget manifests generated by `winget-releaser`. Only **3** build-input files retained for AppImage.
|
||
> - **Added P0**: SHA-pin all third-party GH Actions (tj-actions March 2025 precedent); verify `appimagetool` SHA256 or commit binary to repo; re-assert release.prerelease inside each bump workflow.
|
||
> - **Added perf**: Upload directly to release from matrix jobs (skip artifact round-trip — saves 8-12 min + 1.5GB double-transfer).
|
||
> - **Added pattern**: `linuxdeploy` instead of raw `appimagetool` for JVM+VLC library bundling; build AppImage on `ubuntu-22.04` (glibc 2.35) for broad compat.
|
||
> - **Resolved P0 blocker**: VLC arm64 macOS concern was a false alarm — plugin fetches universal DMG; bundled dylibs already multi-arch. ARM DMG video playback is functional today.
|
||
> - **Renamed**: `createAppImage` → `createReleaseAppImage` (aligns with Compose's `createReleaseDistributable`). Secret names `HOMEBREW_PAT`/`WINGET_PAT` → `HOMEBREW_TOKEN`/`WINGET_TOKEN` (matches existing `SONATYPE_PASSWORD` pattern).
|
||
|
||
## Overview
|
||
|
||
Transform Amethyst Desktop's install story from "unsigned `.deb`/`.msi`/`.dmg` dumped on GH Releases" (only ARM-macOS, no Intel) into a multi-channel FOSS distribution: **8 release assets** covering every mainstream desktop OS/arch, **2 auto-bumping package-manager channels** (Homebrew + Winget), and an authoritative `BUILDING.md`. Ship as one PR. AUR and Scoop ship in a follow-up PR once maintainer resolves their open questions.
|
||
|
||
**Carried from brainstorm** (see brainstorm: `docs/brainstorms/2026-04-16-desktop-multiplatform-distribution-brainstorm.md`):
|
||
- **User choice over paternalism** — multiple install paths documented; users pick.
|
||
- **FOSS alignment** — no walled-garden stores (no Mac App Store, no MS Store, no Snap).
|
||
- **Low maintenance** — every channel auto-pulls from GH Releases; no per-release manual submissions after one-time bootstrap.
|
||
- **Frictionless-where-possible without signing budget** — Homebrew/Winget/Scoop/AUR CLI paths sidestep Gatekeeper/SmartScreen warnings without requiring signing.
|
||
|
||
## Problem Statement
|
||
|
||
### Current state (research-confirmed)
|
||
|
||
| Concern | Actual state | Source |
|
||
|---|---|---|
|
||
| Desktop packageVersion | Hardcoded `1.0.0` in `desktopApp/build.gradle.kts:90`; drift from Android `1.06.3` | `desktopApp/build.gradle.kts:90` |
|
||
| macOS DMG arch | Only ARM64 — `macos-latest` GH runner is arm64 since 2024. **Intel users get unusable DMG** | `.github/workflows/create-release.yml:260` |
|
||
| Linux formats | `.deb` only — no RPM, no AppImage, no tarball | `desktopApp/build.gradle.kts:87` |
|
||
| Windows formats | `.msi` only — no portable `.zip` | `desktopApp/build.gradle.kts:87` |
|
||
| Install channels | GH Releases direct download only. No Homebrew, Winget, Scoop, AUR | `.github/workflows/create-release.yml:264–305` |
|
||
| Install docs | README §Download lists Android-only links (Zap Store, Obtainium, Play, GH). No desktop install section | `README.md:22–36` |
|
||
| Build docs | No `BUILDING.md`, `CONTRIBUTING.md`, or `RELEASING.md` | repo root |
|
||
| Version sync | `versionCode` + `versionName` hardcoded in `amethyst/build.gradle:57–58`; desktop hardcoded separately | `amethyst/build.gradle:57–58` |
|
||
| Release action | Uses deprecated `actions/create-release@v1` + `actions/upload-release-asset@v1` (archived) | `.github/workflows/create-release.yml:19,297` |
|
||
| SHA256 | None published | none |
|
||
| Prerelease gating | All `v*` tags marked `prerelease: true`; no stable-vs-rc distinction | `.github/workflows/create-release.yml:25` |
|
||
|
||
### Why this matters
|
||
|
||
- **Intel Mac users are currently broken** — silently. Confirmed by research: `macos-latest` returns arm64, and jpackage cannot cross-compile.
|
||
- **Discovery bottleneck**: only users who find GH Releases install at all. Package-manager users (the largest FOSS desktop segment — Homebrew has 30M+ users, Winget ships in Windows 11) never encounter Amethyst Desktop.
|
||
- **Trust friction**: unsigned DMG on macOS triggers Gatekeeper ("damaged and can't be opened"); unsigned MSI triggers SmartScreen. Homebrew/Winget/Scoop CLI paths sidestep these warnings for CLI-comfortable users without requiring signing budget.
|
||
- **Deprecation risk**: `actions/create-release@v1` is archived; future GHA runner changes could break releases silently.
|
||
- **Version drift is visible**: if we ship to Homebrew showing `1.0.0` while Android is `1.06.3`, users perceive the project as abandoned.
|
||
|
||
## Proposed Solution [REFINED after deepen]
|
||
|
||
A single large PR landing:
|
||
|
||
1. **Version source-of-truth** in `gradle/libs.versions.toml` (`[versions] app = "1.06.3"`), consumed by Android + Desktop modules. Android `versionCode` stays locally bumped in `amethyst/build.gradle`; only `versionName` / `packageVersion` share the source. `project.version` set at root `allprojects{}` so subprojects inherit — avoids multi-module catalog-resolution drift.
|
||
2. **Expanded Gradle packaging** in `desktopApp/build.gradle.kts` — add `TargetFormat.Rpm`; add one custom Gradle task `createReleaseAppImage` (AppImage via `linuxdeploy` wrapping `createReleaseDistributable`). Portable tar.gz/zip produced by inline `tar`/`zip` in CI after `createReleaseDistributable` — no Gradle task needed.
|
||
3. **Rewritten `.github/workflows/create-release.yml`** — replace deprecated `actions/create-release@v1` + `actions/upload-release-asset@v1` with `softprops/action-gh-release@v2` (SHA-pinned). Expand desktop matrix to `macos-13` (Intel) + `macos-14` (ARM) + `windows-latest` + `ubuntu-latest`. **Matrix jobs upload directly to release via `softprops/action-gh-release@v2`** (no intermediate artifact round-trip — saves 8–12 min and 1.5GB double-transfer). Produce 8 desktop assets. No `SHA256SUMS.txt` (follows existing Amethyst convention — no checksums file on current releases). Release published directly (no draft→publish flip — follows existing `create-release.yml:25` single-shot pattern).
|
||
4. **Two new auto-bump workflows** (AUR + Scoop deferred):
|
||
- `.github/workflows/bump-homebrew.yml` — `action-homebrew-bump-cask` on `ubuntu-latest` (brew works on Linux; saves macOS runner quota)
|
||
- `.github/workflows/bump-winget.yml` — `vedantmgoyal9/winget-releaser` on `windows-latest`
|
||
- Both gated on `release.types: [released]` + `if: github.event.release.prerelease == false` at job level AND re-assert tag format (`^v\d+\.\d+\.\d+$`, rejecting `-rc|-beta|-alpha`) as first step at action boundary (defense-in-depth).
|
||
- Both use `workflow_run` trigger variant where possible, gating on `create-release` workflow success.
|
||
5. **Minimal `packaging/` tree** — 3 files only:
|
||
- `packaging/appimage/AppRun` — shell launcher script (for AppImage)
|
||
- `packaging/appimage/amethyst.desktop` — XDG desktop entry (for AppImage)
|
||
- `packaging/appimage/amethyst.png` — 512×512 icon (scaled from existing 100×100 `icon.png`)
|
||
- Homebrew cask: lives in `Homebrew/homebrew-cask` after initial manual PR; `action-homebrew-bump-cask` re-fetches and rewrites it. No `.tmpl` in our repo.
|
||
- Winget manifests: generated by `winget-releaser` from prior version on each release. No `.tmpl` in our repo.
|
||
6. **Composite action** `.github/actions/assert-stable-release/action.yml` — shared prerelease + tag-format re-assertion, called by both bump workflows. Prevents drift across workflows.
|
||
7. **New `BUILDING.md`** at repo root: prereqs, per-platform build commands, release runbook (maintainer-facing), bootstrap runbook (one-time), troubleshooting (Gatekeeper, SmartScreen), uninstall + state paths per OS.
|
||
8. **README install section** rewritten: per-OS install matrix with CLI + direct-download paths. AUR/Scoop rows marked "Coming soon (separate PR)".
|
||
|
||
## Technical Approach
|
||
|
||
### Architecture [REFINED]
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────────────────────┐
|
||
│ gradle/libs.versions.toml │
|
||
│ [versions] app = "1.06.3" │
|
||
└──────────────────┬──────────────────────────────────┬──────────────────────┘
|
||
│ │
|
||
┌─────────────▼─────────────┐ ┌───────────────▼──────────────┐
|
||
│ amethyst/build.gradle │ │ desktopApp/build.gradle.kts │
|
||
│ versionName = libs... │ │ project.version inherited │
|
||
│ versionCode = 435 (local)│ │ packageVersion = project.ver │
|
||
└───────────────────────────┘ └──────────────────────────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌──────────────────────────────────────────────────────────────────────┐
|
||
│ .github/workflows/create-release.yml (rewritten) │
|
||
│ Trigger: push tag v* │
|
||
│ build-desktop (4-way matrix): │
|
||
│ macos-13 → packageReleaseDmg (Intel .dmg) │
|
||
│ macos-14 → packageReleaseDmg (ARM .dmg) │
|
||
│ windows-latest → packageReleaseMsi + inline `zip` portable │
|
||
│ ubuntu-latest → packageReleaseDeb + packageReleaseRpm │
|
||
│ + createReleaseAppImage + inline `tar` portable │
|
||
│ Each matrix job uploads DIRECTLY to release via │
|
||
│ softprops/action-gh-release@v2 (no artifact round-trip) │
|
||
│ (android + quartz jobs unchanged) │
|
||
│ release-finalize job (needs: build-desktop, deploy-android): │
|
||
│ - sets prerelease flag (inferred from tag: -rc/-beta/-alpha) │
|
||
│ - auto-generated release notes │
|
||
│ - direct single-shot publish (no draft flip — matches existing │
|
||
│ create-release.yml:25 pattern) │
|
||
│ - no SHA256SUMS.txt (follows existing Amethyst convention) │
|
||
└──────────────────────────┬─────────────────────────────┬─────────────┘
|
||
│ release.released event │
|
||
│ (stable tags only — │
|
||
│ prerelease == false + │
|
||
│ tag re-asserted) │
|
||
▼ ▼
|
||
┌─────────────────────┐ ┌─────────────────────┐
|
||
│ bump-homebrew.yml │ │ bump-winget.yml │
|
||
│ ubuntu-latest │ │ windows-latest │
|
||
│ action-homebrew- │ │ vedantmgoyal9/ │
|
||
│ bump-cask │ │ winget-releaser │
|
||
└─────────────────────┘ └─────────────────────┘
|
||
|
||
[FOLLOW-UP PR]: bump-aur.yml + bump-scoop.yml once AUR owner +
|
||
Scoop bucket strategy decided (brainstorm Open Q1, Q2)
|
||
```
|
||
|
||
### Implementation Phases
|
||
|
||
All phases land as one PR. Phases are logical groupings within the PR for reviewer clarity.
|
||
|
||
#### Phase 1 — Version source-of-truth (foundation)
|
||
|
||
**Files:**
|
||
- `gradle/libs.versions.toml` — add `app = "1.06.3"` under `[versions]`
|
||
- `amethyst/build.gradle` — read `versionName` from catalog
|
||
- `desktopApp/build.gradle.kts` — read `packageVersion` from catalog; also set `rpmPackageVersion` with dashes stripped (RPM constraint)
|
||
- Root `build.gradle` — optionally set `allprojects { version = libs.versions.app.get() }`
|
||
|
||
**Pseudo-code** (`desktopApp/build.gradle.kts`):
|
||
```kotlin
|
||
// desktopApp/build.gradle.kts
|
||
val appVersion = libs.versions.app.get()
|
||
val appVersionRpm = appVersion.substringBefore("-") // RPM forbids '-'
|
||
|
||
project.version = appVersion
|
||
|
||
compose.desktop {
|
||
application {
|
||
nativeDistributions {
|
||
targetFormats(
|
||
TargetFormat.Dmg,
|
||
TargetFormat.Msi,
|
||
TargetFormat.Deb,
|
||
TargetFormat.Rpm,
|
||
)
|
||
packageName = "Amethyst"
|
||
packageVersion = appVersion
|
||
linux {
|
||
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
||
rpmPackageVersion = appVersionRpm
|
||
menuGroup = "Network"
|
||
appCategory = "Network"
|
||
debMaintainer = "Amethyst Contributors <contact@amethyst>" // open question: email
|
||
rpmLicenseType = "MIT"
|
||
}
|
||
// ... existing macOS + windows blocks unchanged
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`amethyst/build.gradle` wiring:
|
||
```groovy
|
||
// amethyst/build.gradle:57-58 replacement
|
||
def appVersion = libs.versions.app.get()
|
||
versionCode = 435 // bumped manually per release (Android requirement)
|
||
versionName = generateVersionName(appVersion) // keep branch-suffix logic
|
||
```
|
||
|
||
**Verification:** `./gradlew :desktopApp:packageDistributionForCurrentOS` produces an asset named `Amethyst-1.06.3.*` (not `Amethyst-1.0.0.*`).
|
||
|
||
#### Phase 2 — Expanded Gradle packaging
|
||
|
||
**New Gradle tasks in `desktopApp/build.gradle.kts`:**
|
||
|
||
1. **RPM** — add `TargetFormat.Rpm` to targetFormats list (done in Phase 1 pseudo-code above). Compose will generate `packageReleaseRpm` task. Ubuntu runner needs `apt-get install -y rpm` pre-step.
|
||
|
||
2. **AppImage** — custom task `createReleaseAppImage`. `TargetFormat.AppImage` in Compose 1.10.x is broken (CMP-7101) — do NOT use. Use `linuxdeploy` (not raw `appimagetool`) because it auto-scans `usr/lib/` for missing libraries, handles rpath for bundled JVM, and bundles VLC `.so` files reliably:
|
||
|
||
```kotlin
|
||
val createReleaseAppImage by tasks.registering(Exec::class) {
|
||
group = "compose desktop"
|
||
dependsOn("createReleaseDistributable")
|
||
|
||
val distDir = layout.buildDirectory.dir("compose/binaries/main-release/app/Amethyst")
|
||
val appDir = layout.buildDirectory.dir("appimage/Amethyst.AppDir")
|
||
val outFile = layout.buildDirectory.file("appimage/Amethyst-${project.version}-x86_64.AppImage")
|
||
val toolRoot = layout.projectDirectory.dir("packaging/appimage")
|
||
|
||
inputs.dir(distDir)
|
||
inputs.dir(toolRoot)
|
||
outputs.file(outFile)
|
||
|
||
doFirst {
|
||
val dir = appDir.get().asFile
|
||
dir.deleteRecursively()
|
||
dir.mkdirs()
|
||
copy {
|
||
from(distDir) { into("usr") }
|
||
from(toolRoot.file("AppRun")) { rename { "AppRun" }; fileMode = 0b111_101_101 /* 0755 */ }
|
||
from(toolRoot.file("amethyst.desktop"))
|
||
from(toolRoot.file("amethyst.png"))
|
||
into(dir)
|
||
}
|
||
file("${dir}/.DirIcon").writeText("amethyst.png")
|
||
}
|
||
|
||
// linuxdeploy bundles deps + calls appimagetool internally
|
||
commandLine(
|
||
"${rootDir}/packaging/appimage/linuxdeploy-x86_64.AppImage",
|
||
"--appdir", appDir.get().asFile.absolutePath,
|
||
"--output", "appimage",
|
||
"--desktop-file", "${appDir.get().asFile}/amethyst.desktop",
|
||
"--icon-file", "${appDir.get().asFile}/amethyst.png",
|
||
)
|
||
environment("OUTPUT", outFile.get().asFile.absolutePath)
|
||
environment("ARCH", "x86_64")
|
||
}
|
||
```
|
||
|
||
Supporting files (new, committed to repo under `packaging/appimage/`):
|
||
- `AppRun` — shell launcher. Sets `LD_LIBRARY_PATH` including `usr/lib/vlc` so `vlcj` finds libvlc at runtime:
|
||
```bash
|
||
#!/bin/bash
|
||
HERE="$(dirname "$(readlink -f "$0")")"
|
||
export LD_LIBRARY_PATH="${HERE}/usr/lib:${HERE}/usr/lib/vlc:${LD_LIBRARY_PATH}"
|
||
export PATH="${HERE}/usr/bin:${PATH}"
|
||
export APPDIR="${HERE}"
|
||
exec "${HERE}/usr/bin/Amethyst" "$@"
|
||
```
|
||
- `amethyst.desktop` — XDG Desktop Entry, includes `MimeType=x-scheme-handler/nostr;` for `nostr:` URI handling (future, non-breaking)
|
||
- `amethyst.png` — 512×512 icon (scale from existing 100×100 `icon.png` using ImageMagick `convert icon.png -resize 512x512 amethyst.png`)
|
||
|
||
**Build `linuxdeploy` fetch in CI** (SHA-pinned, not `continuous`):
|
||
```yaml
|
||
- name: Fetch linuxdeploy (pinned + SHA verified)
|
||
run: |
|
||
set -euo pipefail
|
||
curl -fsSL --retry 3 \
|
||
https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage \
|
||
-o packaging/appimage/linuxdeploy-x86_64.AppImage
|
||
echo "${LINUXDEPLOY_SHA256} packaging/appimage/linuxdeploy-x86_64.AppImage" | sha256sum -c -
|
||
chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage
|
||
```
|
||
Where `LINUXDEPLOY_SHA256` is a known-good hash committed to the workflow.
|
||
|
||
**Alternative**: commit `linuxdeploy-x86_64.AppImage` (~10 MB, GPL) to the repo. Eliminates network fetch risk. Recommended.
|
||
|
||
3. **Portable tar.gz (Linux) + zip (Windows)** — no Gradle tasks. Inline `tar` / `zip` in CI after `createReleaseDistributable`:
|
||
|
||
```yaml
|
||
# Linux runner
|
||
- run: ./gradlew :desktopApp:createReleaseDistributable
|
||
- run: |
|
||
cd desktopApp/build/compose/binaries/main-release/app
|
||
tar czf "../../../../../amethyst-desktop-${VER}-linux-x64.tar.gz" Amethyst/
|
||
|
||
# Windows runner
|
||
- run: ./gradlew :desktopApp:createReleaseDistributable
|
||
- run: |
|
||
cd desktopApp/build/compose/binaries/main-release/app
|
||
Compress-Archive -Path Amethyst -DestinationPath "../../../../../amethyst-desktop-${VER}-windows-x64.zip"
|
||
shell: pwsh
|
||
```
|
||
|
||
**Verification:**
|
||
- `./gradlew :desktopApp:packageReleaseRpm` on Ubuntu with `rpm` installed → valid `.rpm`
|
||
- `./gradlew :desktopApp:createReleaseAppImage` on Ubuntu 22.04 → valid `Amethyst-*-x86_64.AppImage` (glibc 2.35 target; `linuxdeploy` bundles deps for compat; test on Fedora 40 + Alpine)
|
||
- Inline `tar` on Linux → valid `amethyst-desktop-*-linux-x64.tar.gz`; extract + `./bin/Amethyst` runs
|
||
- Inline `zip` on Windows → valid `amethyst-desktop-*-windows-x64.zip`; extract + `Amethyst.exe` runs without installed JRE
|
||
|
||
#### Phase 3 — Release workflow rewrite [REFINED]
|
||
|
||
**File:** `.github/workflows/create-release.yml` (full rewrite of the desktop portions; keep Android portions intact)
|
||
|
||
Key changes from refinement:
|
||
- Replace `actions/create-release@v1` + `actions/upload-release-asset@v1` with `softprops/action-gh-release@v2`, **SHA-pinned**
|
||
- Expand desktop matrix to 4 runners (`macos-13`, `macos-14`, `windows-latest`, `ubuntu-latest`)
|
||
- **Each matrix job uploads directly to release via `softprops/action-gh-release@v2`** (no intermediate `upload-artifact` round-trip — saves 8–12 min + 1.5GB transfer per release)
|
||
- **No `SHA256SUMS.txt`** — follows existing Amethyst convention (no checksum files on current releases)
|
||
- **No draft→publish flip** — direct single-shot publish like existing `create-release.yml:25`
|
||
- `prerelease` inferred from tag regex: `-rc|-beta|-alpha` → prerelease, otherwise stable
|
||
- Tag-vs-catalog assertion: inline first step in each matrix job (no separate `verify-version` job — simplifies)
|
||
- Remove Gradle cache from release workflow entirely (release builds are monthly; cache poisoning risk > warmup savings per performance + security review). PR build workflow (`build.yml`) keeps its cache.
|
||
- Add per-asset size budget check: fail if any asset > 1 GB
|
||
- Add `timeout-minutes: 30` per matrix leg
|
||
- Split ubuntu job into two matrix legs (deb+rpm, then AppImage+tar.gz) — halves critical-path time
|
||
|
||
**Pseudo-code** (abbreviated, SHA placeholders as `<SHA>`):
|
||
|
||
```yaml
|
||
# .github/workflows/create-release.yml
|
||
name: Create Release
|
||
on:
|
||
push:
|
||
tags: ['v*']
|
||
permissions:
|
||
contents: write
|
||
|
||
jobs:
|
||
build-desktop:
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- { os: macos-13, tasks: "packageReleaseDmg", arch: x64, family: macos }
|
||
- { os: macos-14, tasks: "packageReleaseDmg", arch: arm64, family: macos }
|
||
- { os: windows-latest, tasks: "packageReleaseMsi createReleaseDistributable", arch: x64, family: windows }
|
||
- { os: ubuntu-latest, tasks: "packageReleaseDeb packageReleaseRpm", arch: x64, family: linux-installers }
|
||
- { os: ubuntu-latest, tasks: "createReleaseAppImage createReleaseDistributable", arch: x64, family: linux-portable }
|
||
runs-on: ${{ matrix.os }}
|
||
timeout-minutes: 30
|
||
defaults: { run: { shell: bash } }
|
||
steps:
|
||
- uses: actions/checkout@<SHA> # SHA-pinned; Dependabot-managed
|
||
- uses: actions/setup-java@<SHA>
|
||
with: { distribution: zulu, java-version: 21 }
|
||
- name: Assert tag matches libs.versions.toml
|
||
run: |
|
||
TOML_VER=$(./gradlew -q printAppVersion) # small Gradle task reads libs.versions.app
|
||
TAG_VER="${GITHUB_REF_NAME#v}"
|
||
[[ "$TOML_VER" == "$TAG_VER" ]] || { echo "::error::catalog=$TOML_VER tag=$TAG_VER"; exit 1; }
|
||
- name: Install rpm tooling (linux only)
|
||
if: startsWith(matrix.family, 'linux')
|
||
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot
|
||
- name: Fetch linuxdeploy (linux-portable only, SHA-pinned)
|
||
if: matrix.family == 'linux-portable'
|
||
run: |
|
||
set -euo pipefail
|
||
curl -fsSL --retry 3 \
|
||
"https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage" \
|
||
-o packaging/appimage/linuxdeploy-x86_64.AppImage
|
||
echo "${LINUXDEPLOY_SHA256} packaging/appimage/linuxdeploy-x86_64.AppImage" | sha256sum -c -
|
||
chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage
|
||
env:
|
||
LINUXDEPLOY_SHA256: <known-good-hash>
|
||
- uses: nick-fields/retry@<SHA>
|
||
with:
|
||
max_attempts: 3
|
||
timeout_minutes: 25
|
||
command: ./gradlew :desktopApp:${{ matrix.tasks }} --no-daemon
|
||
- name: Build inline portable archives
|
||
if: matrix.family == 'windows' || matrix.family == 'linux-portable'
|
||
run: |
|
||
set -euo pipefail
|
||
VER="${GITHUB_REF_NAME#v}"
|
||
APP="desktopApp/build/compose/binaries/main-release/app"
|
||
if [[ "${{ matrix.family }}" == "windows" ]]; then
|
||
(cd "$APP" && powershell -c "Compress-Archive -Path Amethyst -DestinationPath ../../../../../amethyst-desktop-${VER}-windows-x64.zip")
|
||
else
|
||
(cd "$APP" && tar czf "../../../../../amethyst-desktop-${VER}-linux-x64.tar.gz" Amethyst/)
|
||
fi
|
||
- name: Collect + rename assets
|
||
id: collect
|
||
run: |
|
||
set -euo pipefail
|
||
VER="${GITHUB_REF_NAME#v}"
|
||
mkdir -p dist
|
||
source scripts/asset-name.sh # single source of truth (arch review A1)
|
||
collect_assets "${{ matrix.family }}" "${{ matrix.arch }}" "$VER" dist/
|
||
- name: Enforce asset size budget
|
||
run: |
|
||
for f in dist/*; do
|
||
size=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f")
|
||
(( size <= 1073741824 )) || { echo "::error::$f is $(($size / 1048576)) MB (>1GB)"; exit 1; }
|
||
done
|
||
- name: Classify release
|
||
id: classify
|
||
run: |
|
||
if [[ "${GITHUB_REF_NAME}" =~ -(rc|beta|alpha) ]]; then
|
||
echo "is_prerelease=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "is_prerelease=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
- name: Upload to GH Release (direct)
|
||
uses: softprops/action-gh-release@<SHA>
|
||
with:
|
||
files: dist/*
|
||
prerelease: ${{ steps.classify.outputs.is_prerelease }}
|
||
draft: false
|
||
fail_on_unmatched_files: true
|
||
generate_release_notes: true
|
||
tag_name: ${{ github.ref_name }} # upsert — reruns are idempotent
|
||
|
||
deploy-android:
|
||
# unchanged from current workflow (keep existing logic + assert-tag step)
|
||
# ...
|
||
|
||
publish-quartz:
|
||
# unchanged
|
||
# ...
|
||
```
|
||
|
||
Key security & performance deltas:
|
||
- **All `uses:` pinned to 40-char SHA** (per security audit P0.1) — Dependabot-managed updates
|
||
- **`linuxdeploy` (not `appimagetool` with `continuous` tag)** — versioned, SHA-verified (per security audit P0.2; performance audit too)
|
||
- **`nick-fields/retry`** wraps Gradle — protects against transient VLC download / network flakes (performance audit §6)
|
||
- **Direct upload per matrix job** — saves artifact round-trip (performance audit §4, §8)
|
||
- **Split ubuntu into 2 legs** — halves Linux critical-path time; `createReleaseDistributable` runs once per leg but parallelizes (performance audit §2)
|
||
- **`scripts/asset-name.sh`** — single source for asset naming, consumed by workflow + bump jobs + BUILDING.md (arch review A1)
|
||
|
||
Asset naming contract (committed in `BUILDING.md`):
|
||
|
||
```
|
||
amethyst-desktop-<version>-<family>-<arch>.<ext>
|
||
```
|
||
Where:
|
||
- `<version>` = tag stripped of leading `v` (e.g. `1.06.3`)
|
||
- `<family>` ∈ `macos`, `windows`, `linux`
|
||
- `<arch>` ∈ `x64`, `arm64`
|
||
- `<ext>` ∈ `dmg`, `msi`, `zip`, `deb`, `rpm`, `AppImage`, `tar.gz`
|
||
|
||
Examples:
|
||
- `amethyst-desktop-1.06.3-macos-x64.dmg`
|
||
- `amethyst-desktop-1.06.3-macos-arm64.dmg`
|
||
- `amethyst-desktop-1.06.3-windows-x64.msi`
|
||
- `amethyst-desktop-1.06.3-windows-x64.zip`
|
||
- `amethyst-desktop-1.06.3-linux-x64.deb`
|
||
- `amethyst-desktop-1.06.3-linux-x64.rpm`
|
||
- `amethyst-desktop-1.06.3-linux-x64.AppImage`
|
||
- `amethyst-desktop-1.06.3-linux-x64.tar.gz`
|
||
|
||
Aggregate: `SHA256SUMS.txt`.
|
||
|
||
#### Phase 4 — Package-manager auto-bump workflows [REFINED: 2 workflows, not 4]
|
||
|
||
Two new workflows. Each gated on stable releases via `release.released` event (fires only for non-prereleases) AND explicit tag re-assertion at action boundary (defense-in-depth per security review).
|
||
|
||
**Shared composite action** `.github/actions/assert-stable-release/action.yml`:
|
||
```yaml
|
||
name: Assert Stable Release
|
||
description: Re-validate tag format + prerelease flag before running bump actions
|
||
runs:
|
||
using: composite
|
||
steps:
|
||
- shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
TAG="${{ github.event.release.tag_name }}"
|
||
# Defense-in-depth: reject prerelease suffix even if GH flag is false
|
||
if [[ "$TAG" =~ -(rc|beta|alpha|dev|snapshot) ]]; then
|
||
echo "::error::Tag $TAG contains prerelease suffix"; exit 1
|
||
fi
|
||
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||
echo "::error::Tag $TAG does not match vMAJOR.MINOR.PATCH"; exit 1
|
||
fi
|
||
if [[ "${{ github.event.release.draft }}" == "true" ]]; then
|
||
echo "::error::Release is draft"; exit 1
|
||
fi
|
||
```
|
||
|
||
**`.github/workflows/bump-homebrew.yml`:**
|
||
```yaml
|
||
name: Bump Homebrew Cask
|
||
on:
|
||
release:
|
||
types: [released] # only fires for non-prerelease
|
||
permissions: { contents: read }
|
||
concurrency:
|
||
group: bump-homebrew-${{ github.event.release.tag_name }}
|
||
cancel-in-progress: false
|
||
jobs:
|
||
bump:
|
||
if: github.event.release.prerelease == false
|
||
runs-on: ubuntu-latest # brew works on linux; saves macOS runner quota
|
||
steps:
|
||
- uses: actions/checkout@<SHA> # SHA-pinned; Dependabot-managed
|
||
- uses: ./.github/actions/assert-stable-release
|
||
- uses: macauley/action-homebrew-bump-cask@<SHA> # SHA-pinned
|
||
with:
|
||
token: ${{ secrets.HOMEBREW_TOKEN }}
|
||
tap: homebrew/cask
|
||
cask: amethyst-nostr
|
||
tag: ${{ github.ref }}
|
||
- name: Report failure
|
||
if: failure()
|
||
uses: actions/github-script@<SHA>
|
||
with:
|
||
script: |
|
||
github.rest.issues.create({
|
||
owner: context.repo.owner, repo: context.repo.repo,
|
||
title: `[release-ops] bump-homebrew failed for ${context.payload.release.tag_name}`,
|
||
body: `Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
|
||
labels: ['release-ops', 'bug']
|
||
})
|
||
```
|
||
|
||
**`.github/workflows/bump-winget.yml`:**
|
||
```yaml
|
||
name: Bump Winget Manifest
|
||
on:
|
||
release:
|
||
types: [released]
|
||
permissions: { contents: read }
|
||
concurrency:
|
||
group: bump-winget-${{ github.event.release.tag_name }}
|
||
cancel-in-progress: false
|
||
jobs:
|
||
bump:
|
||
if: github.event.release.prerelease == false
|
||
runs-on: windows-latest
|
||
steps:
|
||
- uses: actions/checkout@<SHA>
|
||
- uses: ./.github/actions/assert-stable-release
|
||
- uses: vedantmgoyal9/winget-releaser@<SHA> # SHA-pinned
|
||
with:
|
||
identifier: VitorPamplona.Amethyst
|
||
version: ${{ github.event.release.tag_name }}
|
||
installers-regex: '^amethyst-desktop-.*windows-x64\.msi$'
|
||
token: ${{ secrets.WINGET_TOKEN }}
|
||
- name: Report failure
|
||
if: failure()
|
||
uses: actions/github-script@<SHA>
|
||
with:
|
||
script: | # same issue-open pattern as above
|
||
```
|
||
|
||
**Deferred to follow-up PR:** `bump-aur.yml`, `bump-scoop.yml` — require maintainer to resolve AUR account owner + Scoop bucket strategy first (brainstorm Open Q1, Q2).
|
||
|
||
#### Phase 5 — Manifest files [REFINED: 3 files, not 11]
|
||
|
||
Only build-input files committed — package-manager manifests are generated by their respective bump actions from the live release:
|
||
|
||
| Path | Purpose |
|
||
|---|---|
|
||
| `packaging/appimage/AppRun` | AppImage launcher shell script (sets `LD_LIBRARY_PATH` incl. bundled VLC dylibs) |
|
||
| `packaging/appimage/amethyst.desktop` | AppImage XDG desktop entry |
|
||
| `packaging/appimage/amethyst.png` | 512×512 AppImage icon (scale from existing `icon.png`) |
|
||
|
||
**Homebrew cask**: lives in `Homebrew/homebrew-cask` after initial manual PR (bootstrap); subsequent releases rewrite it via `action-homebrew-bump-cask` which re-fetches asset URLs and computes SHA256 itself.
|
||
|
||
**Winget manifests**: generated by `vedantmgoyal9/winget-releaser` from prior version on each release.
|
||
|
||
**AppImage tooling**: use `linuxdeploy` instead of raw `appimagetool` for JVM+VLC library bundling (auto-scans `usr/lib/` and handles rpath). `linuxdeploy` binary pinned to a released version (not `continuous`) and SHA256-verified when fetched in CI — or committed to `packaging/appimage/linuxdeploy-x86_64.AppImage` for supply-chain hardening (GPL, redistributable).
|
||
|
||
#### Phase 6 — Documentation [REFINED]
|
||
|
||
**New file: `BUILDING.md`** (repo root) — sections:
|
||
|
||
1. Prerequisites — JDK 21 (Temurin/Zulu), Git, per-platform tools (`rpm`, `fakeroot`, `linuxdeploy`, WiX, Xcode CLI tools)
|
||
2. Cloning + initial build — `./gradlew :desktopApp:run` (dev), `./gradlew :desktopApp:packageDistributionForCurrentOS` (package)
|
||
3. Per-format build commands:
|
||
- macOS (Intel or ARM): `./gradlew :desktopApp:packageReleaseDmg`
|
||
- Windows MSI: `./gradlew :desktopApp:packageReleaseMsi`
|
||
- Windows portable zip: `./gradlew :desktopApp:createReleaseDistributable && (cd build/compose/binaries/main-release/app && zip -r ../../../../../amethyst-desktop-windows-x64.zip Amethyst/)`
|
||
- Linux DEB: `./gradlew :desktopApp:packageReleaseDeb`
|
||
- Linux RPM: `./gradlew :desktopApp:packageReleaseRpm`
|
||
- Linux AppImage: `./gradlew :desktopApp:createReleaseAppImage`
|
||
- Linux tar.gz: `./gradlew :desktopApp:createReleaseDistributable && (cd build/compose/binaries/main-release/app && tar czf ../../../../../amethyst-desktop-linux-x64.tar.gz Amethyst/)`
|
||
4. Asset naming contract — single source in `scripts/asset-name.sh` (architectural review A1/A5)
|
||
5. Release runbook (maintainer-facing): bump `libs.versions.toml` `app`, bump Android `versionCode` in `amethyst/build.gradle`, commit, tag, push; workflow auto-publishes
|
||
6. Bootstrap runbook (one-time, maintainer-facing) — Homebrew + Winget only in this PR:
|
||
- Create `HOMEBREW_TOKEN` (fine-grained PAT, `Homebrew/homebrew-cask` only, 90d expiry), manual first `brew bump-cask-pr amethyst-nostr`
|
||
- Create `WINGET_TOKEN` (classic PAT, `public_repo`, 90d expiry), manual first submission via `wingetcreate`
|
||
- 90-day rotation owner + calendar reminder (rotation runbook in BUILDING.md)
|
||
- AUR + Scoop bootstrap: deferred to follow-up PR
|
||
7. Troubleshooting: macOS Gatekeeper (`xattr -cr`, right-click Open), Windows SmartScreen ("More info → Run anyway"), Linux AppImage execute bit
|
||
8. Uninstall + state paths per OS (macOS `~/Library/Application Support/Amethyst`, Windows `%APPDATA%\Amethyst`, Linux `~/.config/amethyst`)
|
||
9. Incident response (per-channel recovery — security review P1.6): bad cask → fix-forward point release or revert PR; bad winget → removal PR to `microsoft/winget-pkgs`
|
||
10. Fallback plans:
|
||
- If `macos-13` Intel runner retires: cross-build on `macos-14` with explicit x64 JDK (runbook)
|
||
- If Homebrew main-cask rejects unsigned (Sept 2026 enforcement): pivot to private tap `vitorpamplona/homebrew-amethyst`
|
||
|
||
**Update: `README.md`** — replace current `## Download and Install` section:
|
||
|
||
```markdown
|
||
## Download and Install
|
||
|
||
### Android
|
||
[existing badges]
|
||
|
||
### Desktop
|
||
|
||
| OS | CLI install | Direct download |
|
||
|---|---|---|
|
||
| macOS (Apple Silicon) | `brew install --cask amethyst-nostr` | [.dmg](https://github.com/vitorpamplona/amethyst/releases/latest) (arm64) |
|
||
| macOS (Intel) | `brew install --cask amethyst-nostr` | [.dmg](https://github.com/vitorpamplona/amethyst/releases/latest) (x64) |
|
||
| Windows 10/11 | `winget install VitorPamplona.Amethyst` | [.msi](https://...) · [.zip](https://...) portable |
|
||
| Debian/Ubuntu | — | [.deb](https://...) |
|
||
| Fedora/RHEL/openSUSE | — | [.rpm](https://...) |
|
||
| Any Linux | — | [AppImage](https://...) · [.tar.gz](https://...) |
|
||
|
||
_Coming soon (separate PR): Scoop (Windows), AUR (Arch Linux)._
|
||
|
||
**Build from source:** see [BUILDING.md](BUILDING.md).
|
||
|
||
**Troubleshooting installs:** see [BUILDING.md § Troubleshooting](BUILDING.md#troubleshooting).
|
||
```
|
||
|
||
Update Deploying section (`README.md:250–267`) to reference `BUILDING.md § Release runbook`.
|
||
|
||
### Detailed File Change List
|
||
|
||
**Modify:**
|
||
|
||
| File | Change |
|
||
|---|---|
|
||
| `gradle/libs.versions.toml` | Add `[versions] app = "1.06.3"` |
|
||
| `amethyst/build.gradle` (L57–58) | `versionName = generateVersionName(libs.versions.app.get())` |
|
||
| `desktopApp/build.gradle.kts` | Wire `project.version = libs.versions.app.get()`; drop `packageVersion = "1.0.0"` hardcode (inherit from project.version); add `TargetFormat.Rpm`; add linux DSL (rpmPackageVersion, menuGroup, etc.); register `createAppImage`, `createPortableTarGz`, `createPortableZip` tasks |
|
||
| `.github/workflows/create-release.yml` | Rewrite desktop section per Phase 3; replace deprecated actions |
|
||
| `.github/workflows/build.yml` | Expand PR-build matrix to build new formats (optional but recommended so PRs catch packaging regressions) |
|
||
| `README.md` | Rewrite Download section; link BUILDING.md |
|
||
|
||
**Create:**
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `BUILDING.md` | Build + release + bootstrap docs |
|
||
| `.github/workflows/bump-homebrew.yml` | Homebrew cask auto-bump |
|
||
| `.github/workflows/bump-winget.yml` | Winget manifest auto-submit |
|
||
| `.github/workflows/bump-scoop.yml` | Scoop manifest auto-update |
|
||
| `.github/workflows/bump-aur.yml` | AUR PKGBUILD auto-push |
|
||
| `packaging/homebrew/amethyst-nostr.rb.tmpl` | Cask template |
|
||
| `packaging/winget/*.yaml.tmpl` | Winget manifest templates (3 files) |
|
||
| `packaging/scoop/amethyst.json.tmpl` | Scoop manifest template |
|
||
| `packaging/aur/PKGBUILD.tmpl` | AUR PKGBUILD template |
|
||
| `packaging/aur/amethyst.desktop` | Linux desktop entry (AUR) |
|
||
| `packaging/appimage/AppRun` | AppImage launcher |
|
||
| `packaging/appimage/amethyst.desktop` | AppImage desktop entry |
|
||
| `packaging/appimage/amethyst.png` | AppImage icon (512×512) |
|
||
|
||
## Alternative Approaches Considered
|
||
|
||
| Alternative | Rejected because |
|
||
|---|---|
|
||
| **Ad-hoc macOS codesign (`codesign --sign -`)** | Only prevents the "damaged" error on some macOS versions; Gatekeeper warning still shows. Brainstorm explicitly rejected (see brainstorm: Resolved Q2). |
|
||
| **Full Apple Developer Program + notarization** | $99/yr budget not committed. Brainstorm deferred (see brainstorm: Deferred). Revisit when sponsor commits. |
|
||
| **Flathub** | Moderate ongoing maintenance (manifest review cycle, sandboxing rules, Flatpak portals for filesystem access). Brainstorm deselected. |
|
||
| **Snap Store** | FOSS-community distaste (proprietary Snap backend, forced auto-updates). Brainstorm deselected. |
|
||
| **Mac App Store / MS Store** | Walled gardens conflict with FOSS alignment. Brainstorm deselected. |
|
||
| **Chocolatey** | Redundant with Winget/Scoop for the target Windows audience (both CLI-first; Chocolatey adds virus-scan requirement + more manual review). |
|
||
| **JReleaser** (all-in-one packager) | Heavy dependency that abstracts away control over `jpackage` + Compose Desktop plugin internals. Current Compose Desktop plugin does the heavy lifting; JReleaser would replace less than it adds. Revisit only if managing 4 separate bump workflows becomes painful. |
|
||
| **Sparkle / in-app auto-update** | Requires signing to be trustworthy. Brainstorm deferred. Future work: in-app "check for update" banner polling GH Releases API. |
|
||
| **Universal macOS DMG (via `lipo`)** | Compose Desktop's Skiko natives don't merge cleanly as universal binaries. Two smaller per-arch DMGs are simpler and smaller per-user. |
|
||
| **Big-bang PR vs layered phases** | Brainstorm selected big-bang (maintainer preference — one review, one landing). Phases within the PR provide reviewer structure (see brainstorm: Sequencing). |
|
||
| **Linux ARM64 / Windows ARM64 assets** | Niche demand; `ubuntu-24.04-arm` and `windows-11-arm` runners are public-repo-free but add matrix complexity. Park as future work; revisit on user demand. |
|
||
| **F-Droid desktop (via flatpak)** | Out of brainstorm scope. Park. |
|
||
|
||
## System-Wide Impact
|
||
|
||
### Interaction Graph
|
||
|
||
```
|
||
tag push (v1.06.3)
|
||
│
|
||
▼
|
||
workflow: create-release.yml
|
||
│
|
||
├─ verify-version (asserts tag == libs.versions.app)
|
||
├─ build-desktop (4-way matrix)
|
||
│ ├─ macos-13 → :desktopApp:packageReleaseDmg → dist/*-macos-x64.dmg
|
||
│ ├─ macos-14 → :desktopApp:packageReleaseDmg → dist/*-macos-arm64.dmg
|
||
│ ├─ windows → :desktopApp:packageReleaseMsi + createPortableZip
|
||
│ └─ ubuntu → :desktopApp:packageReleaseDeb + packageReleaseRpm
|
||
│ + createAppImage + createPortableTarGz
|
||
├─ deploy-android (existing logic; 12 APK/AAB assets)
|
||
├─ publish-quartz (existing; Maven Central)
|
||
└─ release (needs: all above)
|
||
├─ download all artifacts
|
||
├─ compute SHA256SUMS.txt
|
||
├─ classify prerelease from tag
|
||
└─ softprops/action-gh-release@v2 publishes
|
||
│
|
||
▼ release.published event (filtered: prerelease == false)
|
||
│
|
||
├─ workflow: bump-homebrew.yml → PR to Homebrew/homebrew-cask
|
||
├─ workflow: bump-winget.yml → PR to microsoft/winget-pkgs
|
||
├─ workflow: bump-aur.yml → push to aur.archlinux.org
|
||
└─ workflow: bump-scoop.yml → push to own bucket / Extras
|
||
```
|
||
|
||
### Error & Failure Propagation
|
||
|
||
| Failure | Behavior | Mitigation |
|
||
|---|---|---|
|
||
| `verify-version` fails (tag ≠ catalog) | Entire workflow halts before any build | Required fix before retag |
|
||
| One matrix job fails | `fail-fast: false` — other jobs continue; `release` job blocked by `needs:` | Fix the single failing job; rerun that job; `release` runs when all succeed |
|
||
| `release` job fails | Artifacts remain uploaded; no GH Release created | Rerun `release` job once fixed; artifacts retained 90 days |
|
||
| Bump-homebrew PR rejected upstream | Bump action logs error; no user-facing impact | Maintainer manually addresses; next release re-attempts |
|
||
| Bump-winget PR stuck in review | Release claims "available via winget" prematurely | Shadow-check via winget API and edit release notes (manual ops) |
|
||
| AUR SSH key failure | Bump fails; AUR stays on old version | Runbook in BUILDING.md for key rotation |
|
||
| VLC arm64 dylibs missing (plugin doesn't fetch) | ARM DMG builds but crashes at runtime on video playback | **Risk R2** — verify pre-merge by running `./gradlew :desktopApp:packageReleaseDmg` on macos-14 locally/CI and checking `file` output of dylibs in `appResources/macos/vlc` |
|
||
| VLC bundle exceeds 2GB GH asset limit | Upload step fails | **Risk R9** — measure pre-merge; if close, set `shouldIncludeAllVlcFiles = false` and curate minimal plugin list |
|
||
| Draft release created but CI cancelled mid-upload | Partial release with missing assets | Use `draft: false` only after all uploads complete; retry release job is idempotent |
|
||
|
||
### State Lifecycle Risks
|
||
|
||
| Step | State persisted | Cleanup | Risk |
|
||
|---|---|---|---|
|
||
| GH Release draft creation | Draft release on github.com | Draft deleted by release job on retry | Low — draft invisible to users |
|
||
| Matrix artifact upload | GH Actions artifacts (90-day TTL) | Auto-expire | Low |
|
||
| Homebrew PR creation | PR in Homebrew/homebrew-cask | Maintainer can close | Low |
|
||
| Winget PR creation | PR in microsoft/winget-pkgs | Can close | Low |
|
||
| AUR push | Irreversible — AUR repo updated | Can push revert commit | **Medium** — accidental push of broken v1.06.4 reaches Arch users within 1 `yay -Syu` cycle |
|
||
| User install from channel | Files under `/Applications` (macOS), `C:\Program Files\Amethyst` (Windows), `/opt/amethyst` (Linux), user state dirs | Uninstall per-channel | **Medium** — state dirs shared across channels; downgrade via different channel could corrupt schema. Doc "single-channel" policy |
|
||
|
||
### API Surface Parity
|
||
|
||
- **Install surface:** before this PR = GH Releases (single URL format). After = 4 channel install strings + direct-download matrix. Each channel exposes a different upgrade command (`brew upgrade --cask`, `winget upgrade`, `scoop update`, `yay -Syu`). Documented in README.
|
||
- **Version surface:** before = one place (Android `build.gradle`), with desktop drifting independently. After = single source (`libs.versions.toml`); Android `versionCode` still manual.
|
||
- **Artifact surface:** before = 3 desktop assets (one broken for Intel macOS users). After = 8 desktop assets + aggregate checksum file.
|
||
|
||
### Integration Test Scenarios
|
||
|
||
Scenarios that unit/build tests won't catch — require manual or CI-integration validation:
|
||
|
||
1. **Intel macOS DMG actually runs on Intel hardware**. `file Amethyst.app/Contents/MacOS/Amethyst` shows `Mach-O 64-bit executable x86_64` — not universal, not arm64. Manual: fresh Intel Mac, right-click Open, app launches, signs in to Nostr relay.
|
||
2. **ARM macOS DMG runs on Apple Silicon without Rosetta**. `file` shows `Mach-O 64-bit executable arm64`. Manual: fresh M-series Mac, VLC video note plays (validates VLC arm64 dylibs were bundled correctly — **Risk R2**).
|
||
3. **Homebrew cask install flow end-to-end**. Fresh Mac VM: `brew tap homebrew/cask && brew install --cask amethyst-nostr` → app appears in `/Applications` → opens without right-click → uninstall leaves no state in `~/Library/Application Support/Amethyst` unless user opts to preserve.
|
||
4. **Winget flow**. Fresh Windows 11 VM: `winget install VitorPamplona.Amethyst` → app appears in Start Menu → launches → uninstall via Control Panel leaves no registry remnants under `HKCU\Software\Amethyst`.
|
||
5. **AppImage on unknown distro**. Fresh Alpine/Void/NixOS container: `chmod +x Amethyst-*.AppImage && ./Amethyst-*.AppImage` works (validates AppImage self-containment + glibc 2.27 compat).
|
||
6. **Version contract**. Push tag `v1.06.4` where `libs.versions.toml` says `app = "1.06.3"` → `verify-version` job fails fast; no assets built.
|
||
7. **Prerelease gating**. Push `v1.06.3-rc1` → release marked prerelease → bump-homebrew/winget/aur workflows do NOT trigger.
|
||
8. **Matrix partial failure**. Simulate one runner failure → other 3 continue → `release` job blocked → retry of failed matrix job → release publishes successfully.
|
||
|
||
## Acceptance Criteria
|
||
|
||
### Functional Requirements
|
||
|
||
**Phase 1 — Version source-of-truth:**
|
||
- [ ] `gradle/libs.versions.toml` contains `[versions] app = "<semver>"`
|
||
- [ ] Root `allprojects { version = libs.versions.app.get() }` so subprojects inherit
|
||
- [ ] `./gradlew :desktopApp:packageDistributionForCurrentOS` produces asset with `packageVersion` matching catalog
|
||
- [ ] `./gradlew :amethyst:assembleRelease` produces APK with `versionName` matching catalog (plus branch suffix if applicable)
|
||
- [ ] Inline tag-vs-catalog assertion fails when tag ≠ catalog (first step in each matrix job)
|
||
|
||
**Phase 2 — Expanded packaging:**
|
||
- [ ] `./gradlew :desktopApp:packageReleaseRpm` on Ubuntu with `rpm` installed → valid `.rpm`; `rpm -qlp` lists bundled VLC
|
||
- [ ] `./gradlew :desktopApp:createReleaseAppImage` on Ubuntu 22.04 → valid `Amethyst-*-x86_64.AppImage`; `chmod +x` + run launches app
|
||
- [ ] Inline `tar` in CI produces valid `amethyst-desktop-*-linux-x64.tar.gz`; extract + `./bin/Amethyst` runs
|
||
- [ ] Inline `Compress-Archive` in CI produces valid `.zip`; extract + `Amethyst.exe` runs without installed JRE
|
||
- [ ] AppImage runs on Alpine/NixOS container (glibc compat; `linuxdeploy` bundles libs)
|
||
|
||
**Phase 3 — Release workflow:**
|
||
- [ ] `actions/create-release@v1` and `actions/upload-release-asset@v1` removed; `softprops/action-gh-release@v2` (SHA-pinned) used
|
||
- [ ] Matrix includes `macos-13`, `macos-14`, `windows-latest`, `ubuntu-latest` (× 2 for split deb/rpm + AppImage/tar.gz legs)
|
||
- [ ] On tag push: 8 desktop assets + existing Android assets appear on GH Release (**no** `SHA256SUMS.txt` — follows existing convention)
|
||
- [ ] Asset naming matches contract in `scripts/asset-name.sh` (single source of truth)
|
||
- [ ] Release published directly (no draft→publish flip; matches existing workflow pattern)
|
||
- [ ] `prerelease: true` iff tag matches `v*-(rc|beta|alpha)*`; stable tags publish as stable
|
||
- [ ] Per-asset size ≤ 1 GB (enforced in workflow)
|
||
- [ ] All third-party `uses:` SHA-pinned; Dependabot config added for `.github/workflows/`
|
||
- [ ] `linuxdeploy` fetch is SHA-verified (or binary committed to `packaging/appimage/`)
|
||
|
||
**Phase 4 — Auto-bump workflows (Homebrew + Winget only):**
|
||
- [ ] `bump-homebrew.yml` + `bump-winget.yml` present; gated on `release.types: [released]` + `prerelease == false`
|
||
- [ ] Shared composite action `.github/actions/assert-stable-release` re-asserts tag format at action boundary
|
||
- [ ] Failure auto-opens `[release-ops]` issue with run URL
|
||
- [ ] `concurrency:` group per tag prevents re-fire races
|
||
- [ ] Each workflow documented in `BUILDING.md § Bootstrap runbook`
|
||
- [ ] AUR + Scoop bump workflows tracked for follow-up PR (not in this PR)
|
||
|
||
**Phase 5 — Build-input files (3 files, not 11):**
|
||
- [ ] `packaging/appimage/AppRun` present (shellcheck clean)
|
||
- [ ] `packaging/appimage/amethyst.desktop` present (desktop-file-validate clean)
|
||
- [ ] `packaging/appimage/amethyst.png` present (≥ 512×512, valid PNG)
|
||
- [ ] (Optional) `packaging/appimage/linuxdeploy-x86_64.AppImage` committed for supply-chain hardening
|
||
|
||
**Phase 6 — Docs:**
|
||
- [ ] `BUILDING.md` at repo root; linked from README
|
||
- [ ] README `## Download and Install` includes per-OS desktop matrix; AUR/Scoop marked "Coming soon"
|
||
- [ ] README references `BUILDING.md` for troubleshooting
|
||
- [ ] Uninstall + state-dir paths documented per OS
|
||
- [ ] Incident response section per channel (fix-forward + revert PR patterns)
|
||
- [ ] macos-13 retirement fallback plan documented
|
||
|
||
### Non-Functional Requirements
|
||
|
||
- [ ] Release workflow end-to-end runtime ≤ 35 min cold / 25 min warm (revised per perf audit from +30% target)
|
||
- [ ] No asset > 1 GB (enforced step in matrix)
|
||
- [ ] VLC macOS dylib architecture verified on `macos-13` (x86_64) and `macos-14` (arm64) via `file` command in pre-merge dry-run
|
||
|
||
### Quality Gates
|
||
|
||
- [ ] All matrix OS builds pass on the PR branch
|
||
- [ ] Existing Android release flow unchanged in behavior (diff Android asset list before/after)
|
||
- [ ] `spotlessApply` clean on Kotlin changes
|
||
- [ ] README renders correctly on GH
|
||
- [ ] `BUILDING.md` verified by a second contributor on fresh macOS + Windows + Linux VMs
|
||
- [ ] Pre-merge matrix dry-run via `workflow_dispatch` succeeds end-to-end
|
||
|
||
## Success Metrics [REFINED]
|
||
|
||
| Metric | Baseline | Target (90 days post-merge) |
|
||
|---|---|---|
|
||
| Intel Mac install works | No (broken, `macos-latest` arm64 only) | Yes |
|
||
| Package-manager channels (this PR) | 0 | 2 (Homebrew, Winget) |
|
||
| GH Release asset count | 3 desktop + 12 Android | 8 desktop + 12 Android |
|
||
| Version drift incidents | Currently `1.0.0` vs `1.06.3` | 0 (enforced by CI) |
|
||
|
||
## Dependencies & Prerequisites
|
||
|
||
### Code dependencies
|
||
- Compose Multiplatform 1.10.3 (already pinned) — supports all needed `TargetFormat` values
|
||
- JDK 21 (already used)
|
||
- `ir.mahozad.vlc-setup` 0.1.0 (already used) — confirmed fetches `vlc-3.0.21-universal.dmg` with arm64+x86_64 multi-arch dylibs; works on both macos-13 and macos-14 runners
|
||
- `linuxdeploy` SHA-pinned (fetched per-CI-run OR committed to repo)
|
||
- `rpm` + `fakeroot` (apt-installed on Ubuntu runner)
|
||
|
||
### GH Actions dependencies (all SHA-pinned)
|
||
- `softprops/action-gh-release@<SHA>` (v2.x)
|
||
- `actions/checkout@<SHA>`, `actions/setup-java@<SHA>`
|
||
- `macauley/action-homebrew-bump-cask@<SHA>` (v1.x)
|
||
- `vedantmgoyal9/winget-releaser@<SHA>` (v2.x)
|
||
- `nick-fields/retry@<SHA>` (for transient VLC download retries)
|
||
- `actions/github-script@<SHA>` (failure issue auto-open)
|
||
- Dependabot config for `.github/workflows/` to auto-PR SHA updates
|
||
|
||
### Secrets to provision (one-time bootstrap by maintainer)
|
||
- `HOMEBREW_TOKEN` — fine-grained PAT (scoped to `Homebrew/homebrew-cask` only, `Contents: write` + `Pull requests: write`), 90d expiry
|
||
- `WINGET_TOKEN` — classic PAT with `public_repo` (winget-releaser requires classic), 90d expiry, dedicated bot account preferred
|
||
|
||
### External prerequisites (bootstrap runbook in BUILDING.md)
|
||
- Homebrew cask `amethyst-nostr` merged to `Homebrew/homebrew-cask` via manual `brew bump-cask-pr` once (then auto-bumped)
|
||
- Winget `VitorPamplona.Amethyst` submitted once via `wingetcreate` (then auto-bumped by `winget-releaser`)
|
||
- `LINUXDEPLOY_SHA256` hash constant committed to workflow (update when `linuxdeploy` version bumps)
|
||
|
||
## Risk Analysis & Mitigation [REFINED]
|
||
|
||
Structured from SpecFlow + brainstorm + security/perf/arch deepen reviews:
|
||
|
||
| # | Risk | Likelihood | Impact | Mitigation |
|
||
|---|---|---|---|---|
|
||
| R1 | Homebrew-cask unsigned-app enforcement Sept 1, 2026 | **Confirmed** | High — kills main macOS CLI path | **Time-boxed**: Budget $99/yr Apple Developer Program before Sept 2026 OR pivot to private tap `vitorpamplona/homebrew-amethyst` (private tap does NOT bypass Gatekeeper, but sidesteps Homebrew policy). Documented in BUILDING.md Fallbacks. |
|
||
| R2 | ~~VLC arm64 macOS dylibs missing~~ | **RESOLVED** | — | **False alarm.** Plugin fetches `vlc-3.0.21-universal.dmg` (85MB, 2-arch). Bundled `libvlc.dylib`/`libvlccore.dylib` in repo verified as `Mach-O universal binary with 2 architectures: [x86_64] [arm64]`. vlcj 4.8.3 auto-selects matching arch slice at runtime. Source: `VlcDownloadTask.kt` in mahozad/vlc-setup. |
|
||
| R3 | `macos-13` (Intel) runner retirement by GitHub | High eventually | Med — Intel DMG builds break | Track GH runner deprecation; fallback documented in BUILDING.md (cross-arch build on macos-14 with x64 JDK). |
|
||
| R4 | Tag must be pushed to prod to test full fan-out | High | Med — maintainer anxiety | Include `workflow_dispatch` with `dry_run: true` input that builds + creates a test-only release; skips bump workflows. |
|
||
| R5 | Asset naming change breaks auto-bump manifests | Low | High | Single source `scripts/asset-name.sh` consumed by workflow + bump jobs + BUILDING.md (arch review A1) |
|
||
| R6 | Supply chain — unsigned artifacts + no signed checksums | **Accepted** | Med | Matches existing Amethyst convention (Android is signed via APK signature; desktop releases have no parallel today). Sigstore/cosign revisit is future work. |
|
||
| R7 | ~~AUR account single-point-of-failure~~ | — | — | **Deferred to follow-up PR** |
|
||
| R8 | Winget moderator review latency | High | Low | README flags Winget as "Coming soon" until manifest is merged; 24–72h expected lag |
|
||
| R9 | VLC bundle pushes AppImage over GH 1GB/asset budget | Low | High — release fails | **Pre-flight benchmark**: local AppImage build before merge; workflow enforces ≤1GB per asset and fails early |
|
||
| R10 | Windows `upgradeUuid` hardcoded — change breaks MSI upgrades | Low | Med | Document "NEVER change" in BUILDING.md § Release runbook |
|
||
| R11 | GH Actions secret rotation — no owner | Med | Med — bumps stop working silently | 90-day rotation runbook in BUILDING.md; calendar reminder; each bump workflow auto-opens `[release-ops]` issue on failure |
|
||
| R12 | Prerelease gating bug pushes RC to stable channels | Med | High | Shared composite action `assert-stable-release` re-asserts tag format + draft flag at action boundary (security P0.4) |
|
||
| R13 | Cross-channel installs share state dir; downgrade corrupts | Low | Med | Document single-channel policy in BUILDING.md; startup version check is future work |
|
||
| R14 | Compromise of third-party GH Action (tj-actions Mar 2025 precedent) | Med | High | **All third-party actions SHA-pinned + Dependabot-managed** (security P0.1) |
|
||
| R15 | `appimagetool`/`linuxdeploy` fetched from `continuous` tag = unpinned | Med | High | Pin to released version + SHA256 verify OR commit binary to repo (security P0.2) |
|
||
| R16 | Matrix job partial success leaves release in inconsistent state | Low | Low | Each matrix job uploads direct (idempotent upsert via `tag_name:`); `fail_on_unmatched_files: true` |
|
||
| R17 | Cache poisoning across PR and release workflows | Low | High | Remove Gradle cache from release workflow entirely (cold cache cost ~4min << poisoning risk); keep cache only in `build.yml` PR workflow (security P1.3) |
|
||
|
||
## Resource Requirements
|
||
|
||
- **Engineer time**: 1 engineer (me/Claude) — phased work within a single PR; time estimate omitted per user instruction
|
||
- **Maintainer time** (@vitorpamplona):
|
||
- One-time bootstrap: ~2h (AUR account, Homebrew manual PR, Winget manual submission, PATs, Scoop decision)
|
||
- Per-release (post-bootstrap): ~5 min (bump `libs.versions.toml`, bump Android `versionCode`, tag, push — then monitor)
|
||
- **Infra**: free (all GH-hosted runners on public repo free tier); no paid services
|
||
- **External review**: second contributor on macOS + Windows + Linux VMs to verify BUILDING.md freshly
|
||
|
||
## Future Considerations [REFINED]
|
||
|
||
Out of scope for this PR — tracked as separate future work:
|
||
|
||
1. **Code signing** — Apple Developer Program ($99/yr) + macOS notarization + Windows Authenticode. **Time-boxed to Sept 1, 2026** per Homebrew Gatekeeper enforcement (Risk R1).
|
||
2. **AUR channel (`amethyst-desktop-bin`)** — separate follow-up PR once account ownership decided
|
||
3. **Scoop channel** — separate follow-up PR once bucket strategy decided
|
||
4. **In-app "check for update" banner** — poll GH Releases API; modest scope
|
||
5. **Sparkle / Squirrel auto-update** — requires signing
|
||
6. **Flathub** — sandboxed Linux app center
|
||
7. **Mac App Store / MS Store** — walled gardens
|
||
8. **Chocolatey** — redundant with Winget/Scoop
|
||
9. **Linux ARM64 + Windows ARM64 assets** — `ubuntu-24.04-arm` / `windows-11-arm` runners available; add on demand
|
||
10. **`.desktop` MIME handler for `nostr:` URIs** — cheap Linux-integration add
|
||
11. **Sigstore/cosign signing** — supply-chain hardening (Risk R6)
|
||
12. **SLSA build provenance attestation** — `actions/attest-build-provenance` (security P2.1)
|
||
13. **SBOM generation** — CycloneDX/SPDX per release (security P2.3)
|
||
14. **Weekly channel integrity cron** — detect package-mgr manifest drift (security P2.4)
|
||
15. **ScoopInstaller/Extras PR** (if starting with own bucket) — discoverability boost
|
||
16. **Localized install matrix** via Crowdin
|
||
|
||
## Research Insights (from deepen-plan)
|
||
|
||
This plan was deepened with 10 parallel agents. Key findings that shaped the refinements above:
|
||
|
||
### Architecture (architecture-strategist)
|
||
- **A1**: Asset naming contract is duplicated in 5+ places — extracted to `scripts/asset-name.sh` as single source of truth.
|
||
- **A4**: Prose said "draft → publish"; pseudo-code did single-shot. Aligned to single-shot (matches existing `create-release.yml:25`).
|
||
- **A5**: `packaging/` directory mixes build-inputs and publish-templates — refined to build-inputs only (templates generated by bump actions).
|
||
|
||
### Security (security-sentinel) — 4 P0 block-merge items
|
||
- **P0.1**: All third-party actions SHA-pinned (tj-actions March 2025 incident precedent).
|
||
- **P0.2**: `appimagetool` / `linuxdeploy` fetched with SHA256 verification (or committed to repo).
|
||
- **P0.3**: Checksums debate — followed existing Amethyst convention (no checksums file). Sigstore signing deferred as future work.
|
||
- **P0.4**: Bump workflows re-assert tag format + draft flag at action boundary via shared composite action.
|
||
- **P1.3**: Removed Gradle cache from release workflow (cache poisoning risk > warmup savings for monthly releases).
|
||
|
||
### Performance (performance-oracle)
|
||
- **§4, §8**: Direct upload per matrix job saves 8–12 min + 1.5GB double-transfer vs artifact round-trip.
|
||
- **§2**: Split ubuntu job into 2 matrix legs (deb+rpm, AppImage+tar.gz) — halves Linux critical-path time.
|
||
- **§5**: `appimagetool` "continuous" tag unpinned; use released version SHA-pinned.
|
||
- **SLO**: Revised from "+30% of current" to explicit "≤35 min cold / ≤25 min warm" based on asset-size modeling.
|
||
|
||
### Simplicity (code-simplicity-reviewer)
|
||
- Dropped 8 of 11 template files (Homebrew cask + Winget manifests generated by bump actions).
|
||
- Dropped Gradle tasks for tar.gz/zip (inline `tar`/`Compress-Archive` in CI).
|
||
- Dropped separate `verify-version` job (inline assertion in each matrix job).
|
||
- Deferred AUR + Scoop to follow-up PR (unresolved open questions were dragging scope).
|
||
|
||
### Deployment verification (deployment-verification-agent)
|
||
- Go/No-Go checklist with VLC arm64 dylib check, asset size enforcement, pre-merge dry-run.
|
||
- Rollback procedures per channel (fix-forward point release or revert PR).
|
||
- Alert channel chosen: GH Issue auto-open on bump failure (zero infra).
|
||
|
||
### Pattern consistency (pattern-recognition-specialist)
|
||
- Renamed `createAppImage` → `createReleaseAppImage` (matches `createReleaseDistributable` dependency).
|
||
- Renamed `HOMEBREW_PAT` / `WINGET_PAT` → `HOMEBREW_TOKEN` / `WINGET_TOKEN` (matches existing `SONATYPE_PASSWORD` pattern).
|
||
- Asset naming extracted to `scripts/asset-name.sh` single source.
|
||
- Bump workflow `assert-stable-release` composite action deduplicates prerelease re-check across workflows.
|
||
|
||
### External research
|
||
- **AppImage + Compose Desktop**: use `linuxdeploy` (not raw `appimagetool`) for JVM+VLC library bundling. Build on Ubuntu 22.04+ (glibc 2.35); `linuxdeploy` handles compat.
|
||
- **Homebrew 2026 reality**: unsigned casks will be disabled Sept 1, 2026. Private tap does NOT bypass Gatekeeper — macOS-OS-level. Signing budget decision time-boxed.
|
||
- **Gradle catalog pattern**: `libs.versions.toml [versions] app` consumed via root `allprojects { version = libs.versions.app.get() }` so subprojects inherit `project.version`. Avoids multi-module resolution drift.
|
||
- **VLC arm64 macOS**: **resolved — false alarm**. `ir.mahozad.vlc-setup:0.1.0` fetches `vlc-3.0.21-universal.dmg` (85MB, 2-arch) per `VlcDownloadTask.kt` source. Bundled `libvlc.dylib`/`libvlccore.dylib` in this repo verified as `Mach-O universal binary with 2 architectures: [x86_64] [arm64]`. vlcj 4.8.3 auto-selects matching arch slice at runtime. Current ARM DMG video playback is functional. Only issue was Intel Mac (addressed by matrix expansion).
|
||
|
||
## Documentation Plan
|
||
|
||
**New documentation:**
|
||
- `BUILDING.md` — authoritative source for build + release + bootstrap
|
||
- README desktop install matrix
|
||
|
||
**Updated documentation:**
|
||
- README Deploying section references `BUILDING.md § Release runbook`
|
||
- CHANGELOG entry summarizing the distribution expansion
|
||
|
||
**Not needed:**
|
||
- No API docs impact
|
||
- No user-facing feature docs (install story, not feature)
|
||
|
||
## Sources & References
|
||
|
||
### Origin
|
||
|
||
- **Brainstorm document:** [`docs/brainstorms/2026-04-16-desktop-multiplatform-distribution-brainstorm.md`](../brainstorms/2026-04-16-desktop-multiplatform-distribution-brainstorm.md)
|
||
- Key decisions carried forward from brainstorm:
|
||
- Ship unsigned + document workarounds (brainstorm: Resolved Q2)
|
||
- Lockstep desktop version with Android (brainstorm: Resolved Q5)
|
||
- Package-mgr push cadence: stable tags only (brainstorm: Resolved Q6)
|
||
- VLC bundled everywhere (brainstorm: Resolved Q7)
|
||
- AppImage via `appimagetool` wrapping `createDistributable` (brainstorm: Resolved Q3)
|
||
- Homebrew cask name `amethyst-nostr` (brainstorm: Resolved Q1)
|
||
- Winget `PackageIdentifier = VitorPamplona.Amethyst` (brainstorm: Resolved Q4)
|
||
- Sequencing: big-bang PR (brainstorm: Key Decisions)
|
||
- Out of scope: signing, Flathub, Snap, walled gardens, auto-update (brainstorm: Deferred)
|
||
|
||
### Internal References
|
||
|
||
- Current Compose Desktop config: `desktopApp/build.gradle.kts:1–124`
|
||
- Hardcoded version drift: `desktopApp/build.gradle.kts:90` (`packageVersion = "1.0.0"`)
|
||
- Current release workflow: `.github/workflows/create-release.yml:1–306`
|
||
- Current build workflow: `.github/workflows/build.yml:1–207`
|
||
- Android version logic: `amethyst/build.gradle:10–36, 57–58`
|
||
- Gradle version catalog: `gradle/libs.versions.toml:1–195`
|
||
- VLC plugin config: `desktopApp/build.gradle.kts:112–119`
|
||
- Current README install section: `README.md:22–36`
|
||
- Current README deploy section: `README.md:250–267`
|
||
|
||
### External References
|
||
|
||
- **Compose Multiplatform 1.10.x packaging DSL**: https://kotlinlang.org/docs/multiplatform/compose-native-distribution.html
|
||
- **TargetFormat enum (v1.10.3)**: https://github.com/JetBrains/compose-multiplatform/blob/v1.10.3/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/TargetFormat.kt
|
||
- **AppImage `TargetFormat` broken (CMP-7101)**: https://youtrack.jetbrains.com/issue/CMP-7101
|
||
- **jpackage spec (JDK 21)**: https://docs.oracle.com/en/java/javase/21/docs/specs/man/jpackage.html
|
||
- **JDK-8266179** (no cross-arch): https://bugs.openjdk.org/browse/JDK-8266179
|
||
- **softprops/action-gh-release**: https://github.com/softprops/action-gh-release
|
||
- **GitHub Actions runner reference**: https://docs.github.com/en/actions/reference/runners/github-hosted-runners
|
||
- **Homebrew Acceptable Casks**: https://docs.brew.sh/Acceptable-Casks
|
||
- **Homebrew 5.x `--no-quarantine` deprecation**: https://github.com/Homebrew/brew/issues/20755
|
||
- **`macauley/action-homebrew-bump-cask`**: https://github.com/macauley/action-homebrew-bump-cask
|
||
- **Winget manifest schema**: https://learn.microsoft.com/en-us/windows/package-manager/package/manifest
|
||
- **`vedantmgoyal9/winget-releaser`**: https://github.com/vedantmgoyal9/winget-releaser
|
||
- **Scoop App Manifest Autoupdate**: https://github.com/ScoopInstaller/Scoop/wiki/App-Manifest-Autoupdate
|
||
- **ArchWiki PKGBUILD**: https://wiki.archlinux.org/title/PKGBUILD
|
||
- **`KSXGitHub/github-actions-deploy-aur`**: https://github.com/KSXGitHub/github-actions-deploy-aur
|
||
- **AppImage Bundling Java apps**: https://github.com/AppImage/AppImageKit/wiki/Bundling-Java-apps
|
||
- **Gradle Version Catalogs**: https://docs.gradle.org/current/userguide/version_catalogs.html
|
||
- **Gossip (nostr) install docs** — precedent: https://github.com/mikedilger/gossip/blob/master/docs/INSTALLATION.md
|
||
|
||
### Related Work
|
||
|
||
- None open. No prior PRs/issues in Amethyst repo on packaging/signing/Flathub/Homebrew/AppImage.
|
||
|
||
## Open Questions (for @vitorpamplona resolution) [REFINED]
|
||
|
||
Split by resolution timing:
|
||
|
||
### Must resolve before merge
|
||
|
||
1. ~~VLC arm64 macOS verification~~ — **RESOLVED** (R2 false alarm; plugin fetches universal DMG; bundled dylibs already arm64+x86_64 multi-arch).
|
||
2. **`debMaintainer` email** — what contact email should appear in .deb metadata?
|
||
3. **AppImage icon scaling** — OK to scale existing 100×100 `icon.png` to 512×512 via ImageMagick, or commission a proper 512×512?
|
||
4. **Dry-run workflow dispatch** — include `workflow_dispatch` + `dry_run: true` input in this PR? Strongly recommended by deployment verification agent.
|
||
|
||
### Can resolve during implementation
|
||
|
||
5. **Secret rotation owner** — who owns 90-day rotation of `HOMEBREW_TOKEN`, `WINGET_TOKEN`? (Calendar reminder, runbook owner)
|
||
6. **Apple Developer Program signing budget** — time-boxed to Sept 2026 Gatekeeper enforcement. Decision: (a) commit $99/yr now and add signing/notarization in a follow-up, (b) pivot to private tap before Sept 2026, (c) abandon Homebrew cask path. (Risk R1)
|
||
7. **CHANGELOG entry wording** — auto-generated from commits via `generate_release_notes: true`, or hand-written summary?
|
||
|
||
### Deferred to follow-up PR (not in scope for this PR)
|
||
|
||
8. **AUR account ownership** — blocks AUR bootstrap entirely (brainstorm: Open Q1)
|
||
9. **Scoop bucket strategy** — own bucket vs Extras (brainstorm: Open Q2)
|