Merge pull request #2430 from nrobi144/feat/desktop-multiplatform-distribution
feat(desktop): multi-platform distribution — 8 assets, Homebrew + Winget
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
name: Assert Stable Release
|
||||||
|
description: >-
|
||||||
|
Defense-in-depth guard for package-manager bump workflows. Re-validates
|
||||||
|
tag format, prerelease flag, and draft status before invoking third-party
|
||||||
|
actions that hold write credentials to external package manager repos.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Tag to validate (e.g. v1.08.0)"
|
||||||
|
required: true
|
||||||
|
is_prerelease:
|
||||||
|
description: "Whether the release is marked as prerelease (empty string treated as false)"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
is_draft:
|
||||||
|
description: "Whether the release is a draft (empty string treated as false)"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Assert release is stable
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
IS_PRERELEASE: ${{ inputs.is_prerelease }}
|
||||||
|
IS_DRAFT: ${{ inputs.is_draft }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "tag=$TAG prerelease=$IS_PRERELEASE draft=$IS_DRAFT"
|
||||||
|
|
||||||
|
# Stable = exactly vMAJOR.MINOR.PATCH; reject everything else.
|
||||||
|
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::error::Tag $TAG does not match stable vMAJOR.MINOR.PATCH format; refusing bump"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Draft releases must never trigger bumps.
|
||||||
|
if [[ "$IS_DRAFT" == "true" ]]; then
|
||||||
|
echo "::error::Release is draft; refusing bump"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prerelease flag cross-check (belt-and-suspenders with workflow-level `if:`).
|
||||||
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||||
|
echo "::error::Release is prerelease; refusing bump"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Auto-update SHA-pinned GitHub Actions across all workflows.
|
||||||
|
# Required for supply-chain safety — SHA pins only age well with active bumps.
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
labels:
|
||||||
|
- release-ops
|
||||||
|
- dependencies
|
||||||
|
commit-message:
|
||||||
|
prefix: chore(actions)
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: Bump Homebrew Cask
|
||||||
|
|
||||||
|
# Fires when a GH Release is published (not draft, not prerelease).
|
||||||
|
# `release.types: [released]` event fires only for stable releases — still
|
||||||
|
# double-checked by .github/actions/assert-stable-release for defense-in-depth.
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag to bump (for manual recovery)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# Serialize bumps per tag; do not cancel in-progress bumps.
|
||||||
|
group: bump-homebrew-${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
|
||||||
|
runs-on: ubuntu-latest # brew runs on Linux — saves macOS runner quota
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Re-assert stable release
|
||||||
|
uses: ./.github/actions/assert-stable-release
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
is_prerelease: ${{ github.event.release.prerelease || 'false' }}
|
||||||
|
is_draft: ${{ github.event.release.draft || 'false' }}
|
||||||
|
|
||||||
|
- name: Bump cask (push-or-update PR)
|
||||||
|
uses: macauley/action-homebrew-bump-cask@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.HOMEBREW_TOKEN }}
|
||||||
|
tap: homebrew/cask
|
||||||
|
cask: amethyst-nostr
|
||||||
|
tag: ${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
|
||||||
|
- name: Report failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown';
|
||||||
|
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: `[release-ops] bump-homebrew failed for ${tag}`,
|
||||||
|
body: [
|
||||||
|
`Homebrew cask bump failed for release \`${tag}\`.`,
|
||||||
|
``,
|
||||||
|
`- Run: ${runUrl}`,
|
||||||
|
`- Channel: Homebrew Cask (\`amethyst-nostr\`)`,
|
||||||
|
``,
|
||||||
|
`Recovery options:`,
|
||||||
|
`1. Re-run the workflow once underlying issue is fixed`,
|
||||||
|
`2. Manually run \`brew bump-cask-pr amethyst-nostr --version ${tag.replace(/^v/, '')}\``,
|
||||||
|
`3. File PR directly against Homebrew/homebrew-cask`
|
||||||
|
].join('\n'),
|
||||||
|
labels: ['release-ops', 'bug']
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Bump Winget Manifest
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag to submit (for manual recovery)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: bump-winget-${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Re-assert stable release
|
||||||
|
uses: ./.github/actions/assert-stable-release
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
is_prerelease: ${{ github.event.release.prerelease || 'false' }}
|
||||||
|
is_draft: ${{ github.event.release.draft || 'false' }}
|
||||||
|
|
||||||
|
- name: Submit manifest to winget-pkgs
|
||||||
|
uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
|
||||||
|
with:
|
||||||
|
identifier: VitorPamplona.Amethyst
|
||||||
|
version: ${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
# Asset naming contract: scripts/asset-name.sh
|
||||||
|
installers-regex: '^amethyst-desktop-.*-windows-x64\.msi$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|
||||||
|
- name: Report failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown';
|
||||||
|
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: `[release-ops] bump-winget failed for ${tag}`,
|
||||||
|
body: [
|
||||||
|
`Winget manifest submission failed for release \`${tag}\`.`,
|
||||||
|
``,
|
||||||
|
`- Run: ${runUrl}`,
|
||||||
|
`- Channel: Winget (\`VitorPamplona.Amethyst\`)`,
|
||||||
|
``,
|
||||||
|
`Recovery options:`,
|
||||||
|
`1. Re-run the workflow once underlying issue is fixed`,
|
||||||
|
`2. Manually submit via \`wingetcreate update VitorPamplona.Amethyst -v ${tag.replace(/^v/, '')}\``,
|
||||||
|
`3. File PR directly against microsoft/winget-pkgs`
|
||||||
|
].join('\n'),
|
||||||
|
labels: ['release-ops', 'bug']
|
||||||
|
});
|
||||||
@@ -3,267 +3,184 @@ name: Create Release Assets
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: 'Dry run: build assets but do not publish to GH Release or trigger bump workflows'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
test_tag:
|
||||||
|
description: 'Synthetic tag name for dry-run (e.g. v1.08.0-dryrun); ignored on tag push'
|
||||||
|
type: string
|
||||||
|
default: 'v0.0.0-dryrun'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Asset naming contract: amethyst-desktop-<version>-<family>-<arch>.<ext>
|
||||||
|
# Single source of truth in scripts/asset-name.sh.
|
||||||
|
# linuxdeploy pinned release — bump via Dependabot, verify SHA256 via env var below.
|
||||||
|
LINUXDEPLOY_URL: https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage
|
||||||
|
LINUXDEPLOY_SHA256: c86d6540f1df31061f02f539a2d3445f8d7f85cc3994eee1e74cd1ac97b76df0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
# ---------------------------------------------------------------------------
|
||||||
runs-on: ubuntu-latest
|
# Desktop build matrix. Each leg uploads directly to the GH Release via
|
||||||
outputs:
|
# softprops/action-gh-release@v2 (upsert by tag_name). No artifact round-trip.
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
# ---------------------------------------------------------------------------
|
||||||
steps:
|
build-desktop:
|
||||||
- name: Create Release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: Release ${{ github.ref }}
|
|
||||||
draft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
deploy-android:
|
|
||||||
needs: create-release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: 21
|
|
||||||
|
|
||||||
- name: Cache gradle
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
|
|
||||||
- name: Build AAB
|
|
||||||
run: ./gradlew clean bundleRelease --stacktrace
|
|
||||||
|
|
||||||
- name: Sign AAB (Google Play)
|
|
||||||
uses: r0adkll/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDirectory: amethyst/build/outputs/bundle/playRelease
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.KEY_ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
env:
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
|
||||||
|
|
||||||
- name: Sign AAB (F-Droid)
|
|
||||||
uses: r0adkll/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDirectory: amethyst/build/outputs/bundle/fdroidRelease
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.KEY_ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
env:
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
|
||||||
|
|
||||||
- name: Build APK
|
|
||||||
run: ./gradlew assembleRelease --stacktrace
|
|
||||||
|
|
||||||
- name: Sign APK (Google Play)
|
|
||||||
uses: r0adkll/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDirectory: amethyst/build/outputs/apk/play/release
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.KEY_ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
env:
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
|
||||||
|
|
||||||
- name: Sign APK (F-Droid)
|
|
||||||
uses: r0adkll/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDirectory: amethyst/build/outputs/apk/fdroid/release
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.KEY_ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
env:
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
|
||||||
|
|
||||||
# Google Play APK
|
|
||||||
- name: Upload Play APK Universal Asset
|
|
||||||
id: upload-release-asset-play-universal-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK x86 Asset
|
|
||||||
id: upload-release-asset-play-x86-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK x86_64 Asset
|
|
||||||
id: upload-release-asset-play-x86-64-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK arm64-v8a Asset
|
|
||||||
id: upload-release-asset-play-arm64-v8a-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK armeabi-v7a Asset
|
|
||||||
id: upload-release-asset-play-armeabi-v7a-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
# F-Droid APK
|
|
||||||
- name: Upload F-Droid APK Universal Asset
|
|
||||||
id: upload-release-asset-fdroid-universal-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload F-Droid APK x86 Asset
|
|
||||||
id: upload-release-asset-fdroid-x86-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload F-Droid APK x86_64 Asset
|
|
||||||
id: upload-release-asset-fdroid-x86-64-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload F-Droid APK arm64-v8a Asset
|
|
||||||
id: upload-release-asset-fdroid-arm64-v8a-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload F-Droid APK armeabi-v7a Asset
|
|
||||||
id: upload-release-asset-fdroid-armeabi-v7a-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
# Google Play AAB
|
|
||||||
- name: Upload Google Play AAB Asset
|
|
||||||
id: upload-release-asset-play-aab
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
|
|
||||||
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
# FDroid AAB
|
|
||||||
- name: Upload F-Droid AAB Asset
|
|
||||||
id: upload-release-asset-fdroid-aab
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
|
|
||||||
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Publish Quartz Lib
|
|
||||||
run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
|
|
||||||
env:
|
|
||||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
|
|
||||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
|
||||||
|
|
||||||
deploy-desktop:
|
|
||||||
needs: create-release
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- { os: macos-13, arch: x64, family: macos, tasks: "packageReleaseDmg" }
|
||||||
task: packageDeb
|
- { os: macos-14, arch: arm64, family: macos, tasks: "packageReleaseDmg" }
|
||||||
format: deb
|
- { os: windows-latest, arch: x64, family: windows, tasks: "packageReleaseMsi createReleaseDistributable" }
|
||||||
platform: linux
|
- { os: ubuntu-latest, arch: x64, family: linux, tasks: "packageReleaseDeb packageReleaseRpm" }
|
||||||
- os: macos-latest
|
- { os: ubuntu-latest, arch: x64, family: linux-portable, tasks: "createReleaseAppImage createReleaseDistributable" }
|
||||||
task: packageDmg
|
|
||||||
format: dmg
|
|
||||||
platform: macos
|
|
||||||
- os: windows-latest
|
|
||||||
task: packageMsi
|
|
||||||
format: msi
|
|
||||||
platform: windows
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 45
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Resolve tag + version
|
||||||
|
id: ver
|
||||||
|
env:
|
||||||
|
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||||
|
TEST_TAG: ${{ github.event.inputs.test_tag || '' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
||||||
|
TAG="${TEST_TAG:-v0.0.0-dryrun}"
|
||||||
|
else
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
fi
|
||||||
|
VER="${TAG#v}"
|
||||||
|
TOML_VER=$(grep -E '^app\s*=' gradle/libs.versions.toml | head -1 | cut -d'"' -f2)
|
||||||
|
# On dry-run we only require that TOML has a version; on real tag push we require exact match.
|
||||||
|
if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then
|
||||||
|
if [[ "$TOML_VER" != "$VER" ]]; then
|
||||||
|
echo "::error::gradle/libs.versions.toml app=$TOML_VER but tag is $TAG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "toml=$TOML_VER" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Install RPM tooling (deb+rpm leg only)
|
||||||
|
if: matrix.family == 'linux'
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot
|
||||||
|
|
||||||
|
- name: Fetch linuxdeploy (linux-portable only, SHA-verified)
|
||||||
|
if: matrix.family == 'linux-portable'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL --retry 3 "$LINUXDEPLOY_URL" -o packaging/appimage/linuxdeploy-x86_64.AppImage
|
||||||
|
actual=$(sha256sum packaging/appimage/linuxdeploy-x86_64.AppImage | awk '{print $1}')
|
||||||
|
if [[ "$actual" != "$LINUXDEPLOY_SHA256" ]]; then
|
||||||
|
echo "::error::linuxdeploy SHA256 mismatch. Expected $LINUXDEPLOY_SHA256, got $actual"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage
|
||||||
|
|
||||||
|
- name: Build desktop artifacts
|
||||||
|
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
|
||||||
|
with:
|
||||||
|
max_attempts: 2
|
||||||
|
timeout_minutes: 15
|
||||||
|
command: ./gradlew --no-daemon :desktopApp:${{ matrix.tasks }}
|
||||||
|
|
||||||
|
- name: Build portable archives (windows + linux-portable)
|
||||||
|
if: matrix.family == 'windows' || matrix.family == 'linux-portable'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VER="${{ steps.ver.outputs.version }}"
|
||||||
|
APP="desktopApp/build/compose/binaries/main-release/app"
|
||||||
|
mkdir -p desktopApp/build/portable
|
||||||
|
if [[ "${{ matrix.family }}" == "windows" ]]; then
|
||||||
|
( cd "$APP" && 7z a -tzip "../../../../portable/amethyst-desktop-${VER}-windows-x64.zip" Amethyst/ )
|
||||||
|
else
|
||||||
|
( cd "$APP" && tar czf "../../../../portable/amethyst-desktop-${VER}-linux-x64.tar.gz" Amethyst/ )
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Collect + rename assets
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# shellcheck source=scripts/asset-name.sh
|
||||||
|
source scripts/asset-name.sh
|
||||||
|
# collect_assets normalizes linux-portable → linux internally.
|
||||||
|
collect_assets "${{ matrix.family }}" "${{ matrix.arch }}" "${{ steps.ver.outputs.version }}" dist
|
||||||
|
|
||||||
|
- name: Enforce asset size budget (1 GB per asset)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
fail=0
|
||||||
|
for f in dist/*; do
|
||||||
|
if [[ -f "$f" ]]; then
|
||||||
|
size=$(wc -c < "$f")
|
||||||
|
mb=$(( size / 1048576 ))
|
||||||
|
if (( size > 1073741824 )); then
|
||||||
|
echo "::error file=$f::asset is ${mb} MB — exceeds 1 GB budget"
|
||||||
|
fail=1
|
||||||
|
else
|
||||||
|
echo "OK: $f — ${mb} MB"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ "$fail" == 0 ]]
|
||||||
|
|
||||||
|
- name: Classify release
|
||||||
|
id: classify
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.ver.outputs.tag }}"
|
||||||
|
# Stable = exactly vMAJOR.MINOR.PATCH; everything else is prerelease.
|
||||||
|
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload to GH Release (skip on dry-run)
|
||||||
|
if: github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true'
|
||||||
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
||||||
|
with:
|
||||||
|
files: dist/*
|
||||||
|
tag_name: ${{ steps.ver.outputs.tag }}
|
||||||
|
prerelease: ${{ steps.classify.outputs.prerelease }}
|
||||||
|
draft: false
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
generate_release_notes: false # Android job writes release notes (last-writer-wins race)
|
||||||
|
|
||||||
|
- name: Dry-run summary
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true'
|
||||||
|
run: |
|
||||||
|
echo "### Dry-run: ${{ matrix.family }}/${{ matrix.arch }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
ls -la dist >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Android build + sign + direct-upload. Logic preserved from previous workflow;
|
||||||
|
# uses softprops/action-gh-release@v2 instead of deprecated upload-release-asset.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
deploy-android:
|
||||||
|
if: github.event_name != 'workflow_dispatch' # dry-run skips Android (tag-push only)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -280,26 +197,110 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
key: ${{ runner.os }}-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-android-gradle-
|
||||||
|
|
||||||
- name: Build Desktop Distribution
|
- name: Build AAB
|
||||||
run: ./gradlew :desktopApp:${{ matrix.task }}
|
run: ./gradlew clean bundleRelease --stacktrace
|
||||||
|
|
||||||
- name: Find distribution file
|
- name: Sign AAB (Google Play)
|
||||||
id: find-dist
|
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||||
run: |
|
|
||||||
DIST_FILE=$(find desktopApp/build/compose/binaries/main/${{ matrix.format }} -type f \( -name "*.deb" -o -name "*.dmg" -o -name "*.msi" \) | head -1)
|
|
||||||
echo "path=$DIST_FILE" >> $GITHUB_OUTPUT
|
|
||||||
echo "name=$(basename $DIST_FILE)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload Desktop Distribution to Release
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
releaseDirectory: amethyst/build/outputs/bundle/playRelease
|
||||||
asset_path: ${{ steps.find-dist.outputs.path }}
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
asset_name: amethyst-desktop-${{ matrix.platform }}-${{ github.ref_name }}.${{ matrix.format }}
|
alias: ${{ secrets.KEY_ALIAS }}
|
||||||
asset_content_type: application/octet-stream
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
|
- name: Sign AAB (F-Droid)
|
||||||
|
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||||
|
with:
|
||||||
|
releaseDirectory: amethyst/build/outputs/bundle/fdroidRelease
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.KEY_ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
|
- name: Build APK
|
||||||
|
run: ./gradlew assembleRelease --stacktrace
|
||||||
|
|
||||||
|
- name: Sign APK (Google Play)
|
||||||
|
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||||
|
with:
|
||||||
|
releaseDirectory: amethyst/build/outputs/apk/play/release
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.KEY_ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
|
- name: Sign APK (F-Droid)
|
||||||
|
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||||
|
with:
|
||||||
|
releaseDirectory: amethyst/build/outputs/apk/fdroid/release
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.KEY_ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
|
- name: Collect Android assets (rename to canonical scheme)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
# Play APKs (5 variants)
|
||||||
|
for variant in universal x86 x86_64 arm64-v8a armeabi-v7a; do
|
||||||
|
cp "amethyst/build/outputs/apk/play/release/amethyst-play-${variant}-release-unsigned-signed.apk" \
|
||||||
|
"dist/amethyst-googleplay-${variant}-${TAG}.apk"
|
||||||
|
done
|
||||||
|
|
||||||
|
# F-Droid APKs (5 variants)
|
||||||
|
for variant in universal x86 x86_64 arm64-v8a armeabi-v7a; do
|
||||||
|
cp "amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-${variant}-release-unsigned-signed.apk" \
|
||||||
|
"dist/amethyst-fdroid-${variant}-${TAG}.apk"
|
||||||
|
done
|
||||||
|
|
||||||
|
# AABs
|
||||||
|
cp "amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab" \
|
||||||
|
"dist/amethyst-googleplay-${TAG}.aab"
|
||||||
|
cp "amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab" \
|
||||||
|
"dist/amethyst-fdroid-${TAG}.aab"
|
||||||
|
ls -la dist
|
||||||
|
|
||||||
|
- name: Classify release
|
||||||
|
id: classify
|
||||||
|
run: |
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
# Stable = exactly vMAJOR.MINOR.PATCH; everything else is prerelease.
|
||||||
|
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload Android assets to GH Release
|
||||||
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
||||||
|
with:
|
||||||
|
files: dist/*
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
prerelease: ${{ steps.classify.outputs.prerelease }}
|
||||||
|
draft: false
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
generate_release_notes: true
|
||||||
|
|
||||||
|
- name: Publish Quartz Lib
|
||||||
|
run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
|
||||||
|
env:
|
||||||
|
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
|
||||||
|
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||||
|
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
||||||
|
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
||||||
|
|||||||
@@ -164,6 +164,11 @@ desktopApp/src/jvmMain/appResources/linux/
|
|||||||
desktopApp/src/jvmMain/appResources/macos/
|
desktopApp/src/jvmMain/appResources/macos/
|
||||||
desktopApp/src/jvmMain/appResources/windows/
|
desktopApp/src/jvmMain/appResources/windows/
|
||||||
|
|
||||||
|
# CI-fetched AppImage tooling (downloaded by create-release workflow; not committed)
|
||||||
|
packaging/appimage/linuxdeploy-x86_64.AppImage
|
||||||
|
packaging/appimage/linuxdeploy-extracted/
|
||||||
|
packaging/appimage/squashfs-root/
|
||||||
|
|
||||||
# Git worktrees
|
# Git worktrees
|
||||||
.worktrees/
|
.worktrees/
|
||||||
.claude/worktrees/
|
.claude/worktrees/
|
||||||
|
|||||||
+395
@@ -0,0 +1,395 @@
|
|||||||
|
# Building Amethyst Desktop
|
||||||
|
|
||||||
|
This guide covers building Amethyst Desktop from source, the release pipeline,
|
||||||
|
and one-time bootstrap steps for distribution channels.
|
||||||
|
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Clone + first build](#clone--first-build)
|
||||||
|
- [Per-format build commands](#per-format-build-commands)
|
||||||
|
- [Asset naming contract](#asset-naming-contract)
|
||||||
|
- [Release runbook](#release-runbook)
|
||||||
|
- [Bootstrap runbook (one-time)](#bootstrap-runbook-one-time)
|
||||||
|
- [Troubleshooting installs](#troubleshooting-installs)
|
||||||
|
- [Uninstall + state paths](#uninstall--state-paths)
|
||||||
|
- [Incident response](#incident-response)
|
||||||
|
- [Fallback plans](#fallback-plans)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
All platforms:
|
||||||
|
|
||||||
|
- **JDK 21** (Zulu or Temurin recommended)
|
||||||
|
- **Git**
|
||||||
|
|
||||||
|
Platform-specific:
|
||||||
|
|
||||||
|
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
|
||||||
|
- **Windows**: WiX Toolset 3.x on PATH (for MSI). `winget install WiXToolset.WiXToolset`
|
||||||
|
- **Linux (all)**: nothing extra for `.deb`; `rpm` + `fakeroot` for `.rpm`; `linuxdeploy` for AppImage
|
||||||
|
|
||||||
|
Install Linux RPM tooling:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt-get install -y rpm fakeroot
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install -y rpm-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Install linuxdeploy locally (CI fetches its own — SHA-verified):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL -o packaging/appimage/linuxdeploy-x86_64.AppImage \
|
||||||
|
https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage
|
||||||
|
chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clone + first build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vitorpamplona/amethyst.git
|
||||||
|
cd amethyst
|
||||||
|
|
||||||
|
# Dev loop (launches Amethyst Desktop)
|
||||||
|
./gradlew :desktopApp:run
|
||||||
|
|
||||||
|
# Package for current OS
|
||||||
|
./gradlew :desktopApp:packageDistributionForCurrentOS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-format build commands
|
||||||
|
|
||||||
|
| Artifact | Command | Output |
|
||||||
|
|---|---|---|
|
||||||
|
| macOS DMG (host arch) | `./gradlew :desktopApp:packageReleaseDmg` | `desktopApp/build/compose/binaries/main-release/dmg/Amethyst-*.dmg` |
|
||||||
|
| Windows MSI | `./gradlew :desktopApp:packageReleaseMsi` | `desktopApp/build/compose/binaries/main-release/msi/Amethyst-*.msi` |
|
||||||
|
| Linux `.deb` | `./gradlew :desktopApp:packageReleaseDeb` | `desktopApp/build/compose/binaries/main-release/deb/amethyst_*.deb` |
|
||||||
|
| Linux `.rpm` | `./gradlew :desktopApp:packageReleaseRpm` | `desktopApp/build/compose/binaries/main-release/rpm/amethyst-*.rpm` |
|
||||||
|
| Linux AppImage | `./gradlew :desktopApp:createReleaseAppImage` | `desktopApp/build/appimage/Amethyst-*-x86_64.AppImage` |
|
||||||
|
| Windows `.zip` portable | See below (inline `7z`) | — |
|
||||||
|
| Linux `.tar.gz` portable | See below (inline `tar`) | — |
|
||||||
|
|
||||||
|
**Inline portable archives** (run after `createReleaseDistributable`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :desktopApp:createReleaseDistributable
|
||||||
|
|
||||||
|
# Linux tar.gz
|
||||||
|
VER=$(grep -E '^app\s*=' gradle/libs.versions.toml | head -1 | cut -d'"' -f2)
|
||||||
|
( cd desktopApp/build/compose/binaries/main-release/app \
|
||||||
|
&& tar czf "../../../../portable/amethyst-desktop-${VER}-linux-x64.tar.gz" Amethyst/ )
|
||||||
|
|
||||||
|
# Windows .zip (PowerShell)
|
||||||
|
Compress-Archive -Path desktopApp\build\compose\binaries\main-release\app\Amethyst `
|
||||||
|
-DestinationPath "desktopApp\build\portable\amethyst-desktop-$env:VER-windows-x64.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-platform architecture note: **`jpackage` cannot cross-compile**. An Intel
|
||||||
|
DMG must be built on `macos-13` (x64); an ARM DMG must be built on `macos-14`
|
||||||
|
or later. CI runs both.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Asset naming contract
|
||||||
|
|
||||||
|
All GH Release assets follow:
|
||||||
|
|
||||||
|
```
|
||||||
|
amethyst-desktop-<version>-<family>-<arch>.<ext>
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
|
||||||
|
| Field | Values |
|
||||||
|
|---|---|
|
||||||
|
| `<version>` | Tag stripped of leading `v` (e.g. `1.08.0`) |
|
||||||
|
| `<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).
|
||||||
|
Package manager manifests (Homebrew cask, Winget) depend on this exact scheme —
|
||||||
|
any change is a breaking contract.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `amethyst-desktop-1.08.0-macos-x64.dmg`
|
||||||
|
- `amethyst-desktop-1.08.0-macos-arm64.dmg`
|
||||||
|
- `amethyst-desktop-1.08.0-windows-x64.msi`
|
||||||
|
- `amethyst-desktop-1.08.0-linux-x64.AppImage`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release runbook
|
||||||
|
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[versions]
|
||||||
|
app = "1.08.1" # new semver
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Bump Android `versionCode`** in `amethyst/build.gradle` (monotonic integer,
|
||||||
|
must increment even for same `versionName`):
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
versionCode = 443
|
||||||
|
versionName = generateVersionName(libs.versions.app.get())
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Commit + tag + push**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -am "chore(release): 1.08.1"
|
||||||
|
git tag -s v1.08.1 -m "Release 1.08.1"
|
||||||
|
git push && git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Wait** for the `Create Release Assets` workflow to finish (~25–30 min).
|
||||||
|
|
||||||
|
5. **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`,
|
||||||
|
or `-snapshot` is auto-classified as prerelease. Stable tags trigger the
|
||||||
|
Homebrew + Winget bump workflows.
|
||||||
|
|
||||||
|
### Dry-run (no tag push)
|
||||||
|
|
||||||
|
Use `workflow_dispatch` to exercise the full matrix without publishing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run create-release.yml \
|
||||||
|
-f dry_run=true \
|
||||||
|
-f test_tag=v0.0.0-dryrun \
|
||||||
|
--ref feat/my-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
Assets are built and size-checked, but not uploaded; bump workflows do not
|
||||||
|
fire. Use for pre-merge validation of workflow changes.
|
||||||
|
|
||||||
|
### Version constraint: tag must match `libs.versions.toml`
|
||||||
|
|
||||||
|
The first step in each build-desktop matrix job asserts:
|
||||||
|
|
||||||
|
```
|
||||||
|
tag (stripped of 'v') == gradle/libs.versions.toml [versions] app
|
||||||
|
```
|
||||||
|
|
||||||
|
If they drift, the workflow fails fast. Always bump the TOML first, then tag.
|
||||||
|
|
||||||
|
### NEVER change Windows `upgradeUuid`
|
||||||
|
|
||||||
|
`desktopApp/build.gradle.kts:upgradeUuid` is the MSI product family GUID.
|
||||||
|
Changing it breaks in-place upgrades for existing Windows users — they must
|
||||||
|
uninstall before a new release. Leave it alone forever.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bootstrap runbook (one-time)
|
||||||
|
|
||||||
|
### Secrets to provision in GitHub repo settings
|
||||||
|
|
||||||
|
| 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`
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Homebrew cask (one-time initial PR)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew bump-cask-pr amethyst-nostr \
|
||||||
|
--version 1.08.0 \
|
||||||
|
--url "https://github.com/vitorpamplona/amethyst/releases/download/v1.08.0/amethyst-desktop-1.08.0-macos-arm64.dmg"
|
||||||
|
```
|
||||||
|
|
||||||
|
The cask filename is `amethyst-nostr` (not `amethyst` — that's taken by a
|
||||||
|
tiling window manager). After the first PR is merged, `bump-homebrew.yml`
|
||||||
|
auto-submits new version bumps on each stable release.
|
||||||
|
|
||||||
|
### Winget (one-time initial submission)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wingetcreate new \
|
||||||
|
https://github.com/vitorpamplona/amethyst/releases/download/v1.08.0/amethyst-desktop-1.08.0-windows-x64.msi
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `PackageIdentifier = VitorPamplona.Amethyst`. After the first manifest is
|
||||||
|
merged into `microsoft/winget-pkgs`, `bump-winget.yml` auto-submits new
|
||||||
|
version manifests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting installs
|
||||||
|
|
||||||
|
### macOS — Gatekeeper "damaged and can't be opened"
|
||||||
|
|
||||||
|
Amethyst Desktop is currently unsigned. First-time launch requires:
|
||||||
|
|
||||||
|
1. **Right-click → Open** on the app (don't double-click) — then click **Open** on the Gatekeeper dialog
|
||||||
|
2. Or: `xattr -cr /Applications/Amethyst.app` to strip quarantine
|
||||||
|
3. Or: System Settings → Privacy & Security → "Open Anyway" after a blocked launch
|
||||||
|
|
||||||
|
Recommended path: install via Homebrew (`brew install --cask amethyst-nostr`)
|
||||||
|
— cask flow handles this seamlessly.
|
||||||
|
|
||||||
|
### Windows — SmartScreen "Windows protected your PC"
|
||||||
|
|
||||||
|
Amethyst Desktop is currently unsigned (no Authenticode). First-time launch:
|
||||||
|
|
||||||
|
1. Click **More info** on the SmartScreen dialog
|
||||||
|
2. Click **Run anyway**
|
||||||
|
|
||||||
|
Alternatively use `winget install VitorPamplona.Amethyst` — winget install
|
||||||
|
bypasses the UI dialog after accepting the installer's inherent trust.
|
||||||
|
|
||||||
|
### Linux AppImage won't execute
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x Amethyst-*.AppImage
|
||||||
|
./Amethyst-*.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
On Fedora Silverblue / very minimal distros, FUSE might be missing. Use
|
||||||
|
`--appimage-extract-and-run`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./Amethyst-*.AppImage --appimage-extract-and-run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uninstall + state paths
|
||||||
|
|
||||||
|
State is shared across install channels (DMG, Homebrew, MSI, Winget, .deb,
|
||||||
|
.rpm, AppImage, tar.gz). Switching channels does not duplicate data but may
|
||||||
|
expose downgrade migration risks — **prefer a single install channel per
|
||||||
|
machine**.
|
||||||
|
|
||||||
|
| OS | App location | State directories |
|
||||||
|
|---|---|---|
|
||||||
|
| macOS | `/Applications/Amethyst.app` | `~/Library/Application Support/Amethyst`<br>`~/Library/Preferences/com.vitorpamplona.amethyst.desktop.plist`<br>`~/Library/Caches/Amethyst` |
|
||||||
|
| Windows | `%LOCALAPPDATA%\Amethyst` or `C:\Program Files\Amethyst` | `%APPDATA%\Amethyst`<br>`%LOCALAPPDATA%\Amethyst` |
|
||||||
|
| Linux (deb/rpm) | `/opt/amethyst` | `~/.config/amethyst`<br>`~/.local/share/amethyst`<br>`~/.cache/amethyst` |
|
||||||
|
| Linux (AppImage/tar.gz) | user-chosen | Same as above |
|
||||||
|
|
||||||
|
Uninstall:
|
||||||
|
|
||||||
|
- Homebrew: `brew uninstall --cask amethyst-nostr && brew zap amethyst-nostr`
|
||||||
|
- Winget: `winget uninstall VitorPamplona.Amethyst`
|
||||||
|
- .deb: `sudo apt remove amethyst`
|
||||||
|
- .rpm: `sudo dnf remove amethyst`
|
||||||
|
- AppImage / tar.gz: delete the file / extracted directory
|
||||||
|
- macOS `.dmg`: drag from `/Applications` to Trash, then delete state dirs manually
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Incident response
|
||||||
|
|
||||||
|
### Bad GH Release asset
|
||||||
|
|
||||||
|
1. Immediately mark release as prerelease (pauses bump workflows):
|
||||||
|
```bash
|
||||||
|
gh release edit v1.08.1 --prerelease
|
||||||
|
```
|
||||||
|
2. Delete the bad asset:
|
||||||
|
```bash
|
||||||
|
gh release delete-asset v1.08.1 amethyst-desktop-1.08.1-macos-arm64.dmg --yes
|
||||||
|
```
|
||||||
|
3. Rebuild locally or rerun the failing matrix job:
|
||||||
|
```bash
|
||||||
|
gh run rerun <run-id> --failed
|
||||||
|
```
|
||||||
|
4. Flip back to stable once verified (re-fires bump workflows — confirm fix first):
|
||||||
|
```bash
|
||||||
|
gh release edit v1.08.1 --prerelease=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bad build reached Homebrew
|
||||||
|
|
||||||
|
**Preferred**: ship a point release (e.g. v1.08.2) — users on v1.08.1 get the
|
||||||
|
fix via `brew upgrade`.
|
||||||
|
|
||||||
|
**Alternative**: close the open PR in `Homebrew/homebrew-cask` before merge,
|
||||||
|
or file a revert PR if already merged. Typical Homebrew turn-around: 1–2 days.
|
||||||
|
|
||||||
|
### Bad build reached Winget
|
||||||
|
|
||||||
|
Winget manifests are append-only — no hard unpublish. Options:
|
||||||
|
|
||||||
|
1. Ship a point release (preferred — users upgrade via `winget upgrade`)
|
||||||
|
2. File a manifest-removal PR against `microsoft/winget-pkgs`. Moderator
|
||||||
|
review: 24–72h.
|
||||||
|
|
||||||
|
### User-facing communication
|
||||||
|
|
||||||
|
On any incident:
|
||||||
|
|
||||||
|
1. Edit the release body on GitHub with a warning banner + workaround
|
||||||
|
2. Pin a GH Issue with downgrade instructions per channel
|
||||||
|
3. Announce via Nostr relay + project social channels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fallback plans
|
||||||
|
|
||||||
|
### macOS Intel runner retirement
|
||||||
|
|
||||||
|
GitHub's `macos-13` runner will eventually be deprecated. Monitor
|
||||||
|
<https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners>
|
||||||
|
for the deprecation date. When it hits:
|
||||||
|
|
||||||
|
1. Drop the `macos-13` matrix entry from `.github/workflows/create-release.yml`
|
||||||
|
2. Add a cross-arch build step on `macos-14` using a bundled x64 JDK + `jpackage --mac-signing-prefix` shenanigans, OR accept that only Apple Silicon DMGs ship and direct Intel users to `winget` on a Parallels VM or to rebuild from source.
|
||||||
|
3. Update README install matrix to reflect the change.
|
||||||
|
|
||||||
|
### Homebrew main-cask rejects unsigned app (post-Sept 1 2026)
|
||||||
|
|
||||||
|
Homebrew has committed to disabling unsigned casks in `Homebrew/homebrew-cask`
|
||||||
|
on 2026-09-01. Before that date:
|
||||||
|
|
||||||
|
**Option A**: Commit budget to Apple Developer Program ($99/yr), add
|
||||||
|
`signing { sign.set(true) }` + `notarization {}` blocks to
|
||||||
|
`desktopApp/build.gradle.kts`, wire Developer ID + notary creds into CI.
|
||||||
|
|
||||||
|
**Option B**: Pivot to a private Homebrew tap:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create repo: vitorpamplona/homebrew-amethyst
|
||||||
|
# Update bump-homebrew.yml:
|
||||||
|
# tap: vitorpamplona/amethyst
|
||||||
|
# cask: amethyst-nostr
|
||||||
|
# Users install: brew tap vitorpamplona/amethyst && brew install --cask amethyst-nostr
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: a private tap does NOT bypass Gatekeeper itself (macOS OS-level) — users
|
||||||
|
still see the "unsigned developer" dialog. Tap only sidesteps Homebrew's
|
||||||
|
internal policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Follow-up channels (separate PRs)
|
||||||
|
|
||||||
|
- **AUR** (`amethyst-desktop-bin`) — blocked on AUR account ownership decision
|
||||||
|
- **Scoop** (Windows) — blocked on bucket strategy (own vs Extras)
|
||||||
|
- **Flathub** — deferred (moderate ongoing maintenance)
|
||||||
@@ -21,6 +21,8 @@ Join the social network you control.
|
|||||||
|
|
||||||
## Download and Install
|
## Download and Install
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
[<img src="./docs/design/zapstore.svg"
|
[<img src="./docs/design/zapstore.svg"
|
||||||
alt="Get it on Zap Store"
|
alt="Get it on Zap Store"
|
||||||
height="70">](https://github.com/zapstore/zapstore/releases)
|
height="70">](https://github.com/zapstore/zapstore/releases)
|
||||||
@@ -33,6 +35,24 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
alt="Get it on Google Play"
|
alt="Get it on Google Play"
|
||||||
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
|
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
|
||||||
|
| OS | CLI install | Direct download |
|
||||||
|
|---|---|---|
|
||||||
|
| macOS (Apple Silicon) | `brew install --cask amethyst-nostr` | [.dmg arm64](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| macOS (Intel) | `brew install --cask amethyst-nostr` | [.dmg x64](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Windows 10/11 | `winget install VitorPamplona.Amethyst` | [.msi](https://github.com/vitorpamplona/amethyst/releases/latest) · [.zip portable](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Debian/Ubuntu | — | [.deb](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Fedora/RHEL/openSUSE | — | [.rpm](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Any Linux | — | [AppImage](https://github.com/vitorpamplona/amethyst/releases/latest) · [.tar.gz](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
|
||||||
|
_Coming soon (separate PR): Scoop (Windows), AUR (Arch Linux)._
|
||||||
|
|
||||||
|
**Build from source:** see [BUILDING.md](BUILDING.md).
|
||||||
|
|
||||||
|
**Install troubleshooting** (Gatekeeper / SmartScreen / AppImage): see
|
||||||
|
[BUILDING.md § Troubleshooting installs](BUILDING.md#troubleshooting-installs).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Supported Features
|
## Supported Features
|
||||||
@@ -249,22 +269,18 @@ For the Play build:
|
|||||||
|
|
||||||
## Deploying
|
## Deploying
|
||||||
|
|
||||||
1. Generate a new signing key
|
Full release + bootstrap runbooks (Android AAB upload, desktop packaging,
|
||||||
```
|
Homebrew cask, Winget manifest, Apple Developer signing budget time-box) live
|
||||||
keytool -genkey -v -keystore <my-release-key.keystore> -alias <alias_name> -keyalg RSA -keysize 2048 -validity 10000
|
in [BUILDING.md § Release runbook](BUILDING.md#release-runbook) and
|
||||||
openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.jks.base64.txt
|
[BUILDING.md § Bootstrap runbook (one-time)](BUILDING.md#bootstrap-runbook-one-time).
|
||||||
```
|
|
||||||
2. Create four Secret Key variables on your GitHub repository and fill in the signing key information
|
TL;DR for cutting a release:
|
||||||
- `KEY_ALIAS` <- `<alias_name>`
|
|
||||||
- `KEY_PASSWORD` <- `<your password>`
|
1. Bump `app` in `gradle/libs.versions.toml` (e.g. `"1.08.1"`)
|
||||||
- `KEY_STORE_PASSWORD` <- `<your key store password>`
|
2. Bump `versionCode` in `amethyst/build.gradle`
|
||||||
- `SIGNING_KEY` <- the data from `<my-release-key.keystore>`
|
3. `git commit -am "chore(release): 1.08.1" && git tag -s v1.08.1 && git push --tags`
|
||||||
3. Change the `versionCode` and `versionName` on `amethyst/build.gradle`
|
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
|
||||||
4. Commit and push.
|
5. Upload AAB to Play Store manually (existing step)
|
||||||
5. Tag the commit with `v{x.x.x}`
|
|
||||||
6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file.
|
|
||||||
7. Add your CHANGE LOG to the description of the new release
|
|
||||||
8. Download the `aab` file and upload it to the PlayStore.
|
|
||||||
|
|
||||||
## Using the Quartz library
|
## Using the Quartz library
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ android {
|
|||||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||||
versionCode = 442
|
versionCode = 442
|
||||||
versionName = generateVersionName("1.08.0")
|
versionName = generateVersionName(libs.versions.app.get())
|
||||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"be99e8c8d4df0f54b44eb6c96976ccb38baeea0192436a1c6fc8bc5e930da6b0\""
|
buildConfigField "String", "RELEASE_NOTES_ID", "\"be99e8c8d4df0f54b44eb6c96976ccb38baeea0192436a1c6fc8bc5e930da6b0\""
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ plugins {
|
|||||||
alias(libs.plugins.serialization)
|
alias(libs.plugins.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared app version for all subprojects — read from gradle/libs.versions.toml.
|
||||||
|
// Android versionCode stays local in amethyst/build.gradle (must be monotonic int).
|
||||||
|
// Desktop packageVersion inherits via project.version in desktopApp/build.gradle.kts.
|
||||||
allprojects {
|
allprojects {
|
||||||
|
version = libs.versions.app.get()
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
|
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.jetbrainsKotlinJvm)
|
alias(libs.plugins.jetbrainsKotlinJvm)
|
||||||
@@ -7,6 +9,10 @@ plugins {
|
|||||||
id("ir.mahozad.vlc-setup") version "0.1.0"
|
id("ir.mahozad.vlc-setup") version "0.1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RPM rejects dashes in version strings — replace with tilde (~) which RPM uses
|
||||||
|
// for prerelease ordering: 1.08.0~rc1 < 1.08.0 per RPM version comparison rules.
|
||||||
|
val appVersion: String = project.version.toString()
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
kotlin.srcDir("src/jvmMain/kotlin")
|
kotlin.srcDir("src/jvmMain/kotlin")
|
||||||
@@ -88,11 +94,11 @@ compose.desktop {
|
|||||||
|
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
|
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
|
||||||
modules("java.management") // Required by kmp-tor TorRuntime
|
modules("java.management") // Required by kmp-tor TorRuntime
|
||||||
|
|
||||||
packageName = "Amethyst"
|
packageName = "Amethyst"
|
||||||
packageVersion = "1.0.0"
|
packageVersion = appVersion
|
||||||
description = "Nostr client for desktop"
|
description = "Nostr client for desktop"
|
||||||
vendor = "Amethyst Contributors"
|
vendor = "Amethyst Contributors"
|
||||||
|
|
||||||
@@ -109,6 +115,12 @@ compose.desktop {
|
|||||||
|
|
||||||
linux {
|
linux {
|
||||||
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
||||||
|
menuGroup = "Network"
|
||||||
|
appCategory = "Network"
|
||||||
|
debMaintainer = "vitor@vitorpamplona.com"
|
||||||
|
rpmLicenseType = "MIT"
|
||||||
|
// RPM version: replace dashes with tilde (1.08.0~rc1 < 1.08.0 per RPM ordering).
|
||||||
|
rpmPackageVersion = appVersion.replace("-", "~")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,3 +138,69 @@ vlcSetup {
|
|||||||
tasks.named("spotlessKotlin") {
|
tasks.named("spotlessKotlin") {
|
||||||
mustRunAfter("vlcSetup")
|
mustRunAfter("vlcSetup")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- AppImage packaging (Linux) ---
|
||||||
|
//
|
||||||
|
// Compose Multiplatform's TargetFormat.AppImage is known-broken in 1.10.x (CMP-7101).
|
||||||
|
// Instead: wrap `createReleaseDistributable` output with `linuxdeploy` (which
|
||||||
|
// auto-bundles libraries, handles rpath, and calls appimagetool internally).
|
||||||
|
//
|
||||||
|
// Build inputs live in packaging/appimage/:
|
||||||
|
// - AppRun shell launcher (sets LD_LIBRARY_PATH including bundled VLC)
|
||||||
|
// - amethyst.desktop XDG desktop entry
|
||||||
|
// - amethyst.png 512x512 icon
|
||||||
|
//
|
||||||
|
// linuxdeploy binary is fetched by CI (SHA-verified) into packaging/appimage/
|
||||||
|
// as linuxdeploy-x86_64.AppImage. BUILDING.md documents local-dev fetch.
|
||||||
|
val createReleaseAppImage by tasks.registering(Exec::class) {
|
||||||
|
group = "compose desktop"
|
||||||
|
description = "Bundle createReleaseDistributable output into a Linux AppImage via linuxdeploy."
|
||||||
|
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-$appVersion-x86_64.AppImage")
|
||||||
|
val toolRoot = layout.projectDirectory.dir("../packaging/appimage")
|
||||||
|
val linuxdeployTool = toolRoot.file("linuxdeploy-x86_64.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" }
|
||||||
|
filePermissions { unix("0755") }
|
||||||
|
}
|
||||||
|
from(toolRoot.file("amethyst.desktop"))
|
||||||
|
from(toolRoot.file("amethyst.png"))
|
||||||
|
into(dir)
|
||||||
|
}
|
||||||
|
// DirIcon is used by desktop integrations (file managers, AppImageLauncher)
|
||||||
|
val dirIcon = File(dir, ".DirIcon")
|
||||||
|
if (dirIcon.exists()) dirIcon.delete()
|
||||||
|
Files.createSymbolicLink(dirIcon.toPath(), File("amethyst.png").toPath())
|
||||||
|
|
||||||
|
if (!linuxdeployTool.asFile.canExecute()) {
|
||||||
|
linuxdeployTool.asFile.setExecutable(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commandLine(
|
||||||
|
linuxdeployTool.asFile.absolutePath,
|
||||||
|
"--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")
|
||||||
|
// Bypass FUSE requirement on CI runners (ubuntu-latest lacks libfuse.so.2).
|
||||||
|
// AppImage standard env var: extracts + runs without mounting.
|
||||||
|
environment("APPIMAGE_EXTRACT_AND_RUN", "1")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
[versions]
|
[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.08.0"
|
||||||
accompanistAdaptive = "0.37.3"
|
accompanistAdaptive = "0.37.3"
|
||||||
cachemapVersion = "0.2.4"
|
cachemapVersion = "0.2.4"
|
||||||
composeMultiplatform = "1.10.3"
|
composeMultiplatform = "1.10.3"
|
||||||
|
|||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# AppImage launcher for Amethyst Desktop.
|
||||||
|
# Sets LD_LIBRARY_PATH so vlcj finds bundled libvlc.so at runtime.
|
||||||
|
# jpackage puts app resources at usr/lib/app/<platform>/vlc/ inside the AppDir.
|
||||||
|
set -eu
|
||||||
|
HERE="$(dirname "$(readlink -f "${0}")")"
|
||||||
|
export LD_LIBRARY_PATH="${HERE}/usr/lib/app/linux/vlc:${HERE}/usr/lib:${HERE}/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH:-}"
|
||||||
|
export VLC_PLUGIN_PATH="${HERE}/usr/lib/app/linux/vlc/plugins"
|
||||||
|
export PATH="${HERE}/usr/bin:${PATH}"
|
||||||
|
export APPDIR="${HERE}"
|
||||||
|
exec "${HERE}/usr/bin/Amethyst" "$@"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Amethyst
|
||||||
|
GenericName=Nostr Client
|
||||||
|
Comment=Nostr client for desktop
|
||||||
|
Exec=Amethyst %u
|
||||||
|
Icon=amethyst
|
||||||
|
Categories=Network;InstantMessaging;
|
||||||
|
Terminal=false
|
||||||
|
StartupNotify=true
|
||||||
|
StartupWMClass=Amethyst
|
||||||
|
Keywords=nostr;client;social;chat;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Single source of truth for Amethyst Desktop release asset naming.
|
||||||
|
#
|
||||||
|
# Contract: amethyst-desktop-<version>-<family>-<arch>.<ext>
|
||||||
|
# <version> = tag stripped of leading 'v' (e.g. "1.08.0")
|
||||||
|
# <family> = macos | windows | linux
|
||||||
|
# <arch> = x64 | arm64
|
||||||
|
# <ext> = dmg | msi | zip | deb | rpm | AppImage | tar.gz
|
||||||
|
#
|
||||||
|
# Consumed by: .github/workflows/create-release.yml, .github/workflows/bump-*.yml,
|
||||||
|
# BUILDING.md, local release runbooks.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source scripts/asset-name.sh
|
||||||
|
# collect_assets <family> <arch> <version> <dest_dir>
|
||||||
|
#
|
||||||
|
# Expected examples:
|
||||||
|
# amethyst-desktop-1.08.0-macos-x64.dmg
|
||||||
|
# amethyst-desktop-1.08.0-macos-arm64.dmg
|
||||||
|
# amethyst-desktop-1.08.0-windows-x64.msi
|
||||||
|
# amethyst-desktop-1.08.0-windows-x64.zip
|
||||||
|
# amethyst-desktop-1.08.0-linux-x64.deb
|
||||||
|
# amethyst-desktop-1.08.0-linux-x64.rpm
|
||||||
|
# amethyst-desktop-1.08.0-linux-x64.AppImage
|
||||||
|
# amethyst-desktop-1.08.0-linux-x64.tar.gz
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Print the canonical asset filename for a given family/arch/version/extension.
|
||||||
|
# Usage: asset_name <family> <arch> <version> <ext>
|
||||||
|
asset_name() {
|
||||||
|
local family="$1" arch="$2" version="$3" ext="$4"
|
||||||
|
printf 'amethyst-desktop-%s-%s-%s.%s' "$version" "$family" "$arch" "$ext"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy + rename build outputs into <dest_dir> using the canonical naming scheme.
|
||||||
|
# Usage: collect_assets <family> <arch> <version> <dest_dir>
|
||||||
|
# Expects build outputs under desktopApp/build/... (Compose binaries + custom tasks + portable archives).
|
||||||
|
collect_assets() {
|
||||||
|
local family="$1" arch="$2" version="$3" dest="$4"
|
||||||
|
# Normalize internal matrix family aliases to canonical asset-name families.
|
||||||
|
[[ "$family" == "linux-portable" ]] && family="linux"
|
||||||
|
mkdir -p "$dest"
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
# Compose Desktop jpackage outputs (main-release/<format>/*.ext)
|
||||||
|
local src ext base dst
|
||||||
|
for src in \
|
||||||
|
desktopApp/build/compose/binaries/main-release/dmg/*.dmg \
|
||||||
|
desktopApp/build/compose/binaries/main-release/msi/*.msi \
|
||||||
|
desktopApp/build/compose/binaries/main-release/deb/*.deb \
|
||||||
|
desktopApp/build/compose/binaries/main-release/rpm/*.rpm \
|
||||||
|
desktopApp/build/appimage/*.AppImage \
|
||||||
|
desktopApp/build/portable/*.tar.gz \
|
||||||
|
desktopApp/build/portable/*.zip \
|
||||||
|
; do
|
||||||
|
[ -f "$src" ] || continue
|
||||||
|
base="$(basename "$src")"
|
||||||
|
case "$base" in
|
||||||
|
*.tar.gz) ext="tar.gz" ;;
|
||||||
|
*) ext="${base##*.}" ;;
|
||||||
|
esac
|
||||||
|
dst="$dest/$(asset_name "$family" "$arch" "$version" "$ext")"
|
||||||
|
cp "$src" "$dst"
|
||||||
|
echo "Collected: $dst"
|
||||||
|
done
|
||||||
|
shopt -u nullglob
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user