feat(geode): add a release & distribution pipeline

geode was runnable only via ./gradlew :geode:run and was absent from CI.
Give it the same release process as the amy CLI (it's the same kind of
application-plugin JVM module), plus the pieces a long-running server
daemon needs that a one-shot CLI does not.

- Main.kt: add terminal --version/-V and --help/-h flags so a packaged
  binary has a fast, exit-0 command (Homebrew test block, package smoke
  checks, Docker healthcheck).
- build.gradle.kts: jlinkRuntime + geodeImage (portable flat app-image
  with a bundled JRE, plus config.example.toml + geode.service under
  share/) + jpackageDeb/jpackageRpm, mirroring cli/. No Compose to
  exclude — geode depends only on :quartz.
- Dockerfile + .dockerignore: multi-stage image (gradle installDist ->
  temurin JRE), the primary channel for relay operators.
- packaging/: systemd unit, macOS hardened-runtime entitlements, and a
  reference Homebrew formula.
- scripts/asset-name.sh: geode_asset_name/collect_geode_assets under the
  canonical geode-<version>-<family>-<arch>.<ext> scheme.
- create-release.yml: build-geode matrix (tarball + deb/rpm + no-JRE jvm
  bundle, with a serve+NIP-11 smoke test of the jlink image) and a
  docker-geode job pushing ghcr.io/<owner>/geode:<version> (+ :latest).
- bump-homebrew-geode-formula.yml: auto-sync the reference formula on
  stable releases.
- build.yml: run :geode:test in CI (it ran in no workflow before).
- README.md + plans/2026-07-24-geode-release.md: operator docs + design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCdJwdhGtmLZ12ViS56S3k
This commit is contained in:
Claude
2026-07-24 17:56:50 +00:00
parent 94210d202d
commit 0a30231196
13 changed files with 1169 additions and 6 deletions
+17
View File
@@ -0,0 +1,17 @@
# Keep the Docker build context small + reproducible: exclude VCS metadata,
# Gradle/build outputs, IDE files, and local caches. The build stage runs a
# fresh `./gradlew :geode:installDist` inside the image, so nothing under any
# build/ directory needs to travel with the context.
.git
.github
**/build/
.gradle/
**/.gradle/
.idea/
*.iml
.kotlin/
**/.kotlin/
local.properties
# Docs + non-geode app modules aren't needed to build the relay, but the Gradle
# settings reference every module, so we can't drop the module source trees;
# only their build artifacts (covered above) are excluded.
+1 -1
View File
@@ -87,7 +87,7 @@ jobs:
- name: Test + Build Desktop (gradle)
run: |
CMD="./gradlew :quartz:jvmTest :commons:jvmTest :nestsClient:jvmTest :cli:test :desktopApp:test :desktopApp:${{ matrix.desktop-task }}"
CMD="./gradlew :quartz:jvmTest :commons:jvmTest :nestsClient:jvmTest :geode:test :cli:test :desktopApp:test :desktopApp:${{ matrix.desktop-task }}"
if [ "${{ runner.os }}" = "Linux" ]; then
xvfb-run --auto-servernum $CMD
else
@@ -0,0 +1,155 @@
name: Bump Homebrew Formula (geode relay)
# Sibling of bump-homebrew-formula.yml (the amy CLI). Same mechanism, different
# artifact:
# - bump-homebrew-formula.yml -> Formula `amy` (the headless CLI)
# - this workflow -> Formula `geode` (the standalone relay)
#
# After a stable release, download the published `geode-<version>-jvm.tar.gz`
# bundle, compute its sha256, and open a PR that syncs
# `geode/packaging/homebrew/geode.rb`'s url + sha256 to that release. Keeping the
# in-repo reference formula accurate makes the eventual homebrew-core submission a
# copy-paste.
#
# What it does NOT do yet: open a PR against Homebrew/homebrew-core. `brew
# bump-formula-pr` can only bump a formula that already EXISTS in homebrew-core,
# and `geode` has never been submitted there — that first submission is a manual,
# human-reviewed new-formula PR (the one-time bootstrap). Once it lands, wire the
# auto-bump here — see the "TODO(bootstrap)" note at the bottom of this file.
on:
release:
types: [released]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to sync (for manual recovery)'
required: true
type: string
permissions:
contents: write
pull-requests: write
# The "Report failure" step opens a [release-ops] issue via
# github.rest.issues.create, which needs issues:write.
issues: write
concurrency:
# Serialize per tag; do not cancel in-progress runs.
group: bump-homebrew-geode-formula-${{ github.event.release.tag_name || inputs.tag }}
cancel-in-progress: false
jobs:
sync-formula:
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v7
- 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: Resolve version
id: ver
run: |
set -euo pipefail
TAG="${{ github.event.release.tag_name || inputs.tag }}"
VER="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "ver=$VER" >> "$GITHUB_OUTPUT"
- name: Download jvm bundle and compute sha256
id: asset
run: |
set -euo pipefail
TAG="${{ steps.ver.outputs.tag }}"
VER="${{ steps.ver.outputs.ver }}"
URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/geode-${VER}-jvm.tar.gz"
echo "Fetching $URL"
# The `released` event can fire a hair before every matrix leg finishes
# uploading; retry with backoff (mirrors the repo's push/pull retry ethos).
ok=0
for i in 1 2 3 4 5; do
if curl -fsSL -o geode-jvm.tar.gz "$URL"; then ok=1; break; fi
wait=$(( 2 ** i ))
echo "attempt $i failed; retrying in ${wait}s"
sleep "$wait"
done
[[ "$ok" == 1 ]] || { echo "::error::could not download $URL"; exit 1; }
test -s geode-jvm.tar.gz
SHA=$(shasum -a 256 geode-jvm.tar.gz | awk '{print $1}')
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
echo "geode-${VER}-jvm.tar.gz -> $SHA"
- name: Update reference formula
run: |
set -euo pipefail
FORMULA=geode/packaging/homebrew/geode.rb
URL="${{ steps.asset.outputs.url }}"
SHA="${{ steps.asset.outputs.sha256 }}"
# Rewrite the two indented lines in the formula block. Anchoring on the
# 2-space indent avoids touching the header comment's example curl url.
sed -i -E "s|^( url ).*|\1\"${URL}\"|" "$FORMULA"
sed -i -E "s|^( sha256 ).*|\1\"${SHA}\"|" "$FORMULA"
echo "----- $FORMULA -----"
grep -E "^ (url|sha256) " "$FORMULA"
- name: Open or update the formula-sync PR
# peter-evans/create-pull-request is MIT-licensed CI-only tooling (not
# linked into any shipped artifact). It no-ops when there is no diff.
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: main
branch: chore/bump-geode-formula-${{ steps.ver.outputs.tag }}
add-paths: geode/packaging/homebrew/geode.rb
commit-message: 'chore: sync geode Homebrew formula to ${{ steps.ver.outputs.tag }}'
title: 'chore: sync geode Homebrew formula to ${{ steps.ver.outputs.tag }}'
body: |
Auto-synced `geode/packaging/homebrew/geode.rb` to the
`${{ steps.ver.outputs.tag }}` release:
- `url` -> `${{ steps.asset.outputs.url }}`
- `sha256` -> `${{ steps.asset.outputs.sha256 }}`
Opened by `.github/workflows/bump-homebrew-geode-formula.yml`. Merge to
keep the reference formula ready for the homebrew-core submission/bump.
# TODO(bootstrap): once `geode` is accepted into Homebrew/homebrew-core, add
# a step here that opens the homebrew-core bump PR automatically (a pinned
# macauley/action-homebrew-bump-formula, or `brew bump-formula-pr geode
# --url=<url> --sha256=<sha>` with a HOMEBREW_TOKEN). It is intentionally
# omitted until then because bump-formula-pr errors on a formula that is not
# yet in the tap.
- name: Report failure
if: failure()
uses: actions/github-script@v9
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-geode-formula failed for ${tag}`,
body: [
`geode Homebrew formula sync failed for release \`${tag}\`.`,
``,
`- Run: ${runUrl}`,
`- Channel: Homebrew Formula (\`geode\` relay)`,
``,
`Recovery options:`,
`1. Re-run the workflow once the underlying issue is fixed`,
`2. Manually update \`geode/packaging/homebrew/geode.rb\` (url + sha256) from the release asset`,
`3. Check the release actually published \`geode-${tag.replace(/^v/, '')}-jvm.tar.gz\``
].join('\n'),
labels: ['release-ops', 'bug']
});
+299
View File
@@ -518,6 +518,305 @@ jobs:
ls -la dist >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
# ---------------------------------------------------------------------------
# geode relay build matrix. Same shape as build-cli — geode is the same kind
# of `application`-plugin JVM module — producing a self-contained bundle with a
# minimal jlink'd JRE (no system Java required) plus native Linux packages.
#
# geodeImage task (all legs): geode/build/geode-image/geode/ → geode-*.tar.gz
# jpackageDeb / jpackageRpm: geode/build/jpackage/geode_*.deb + geode-*.rpm
# no-JRE jvm bundle (linux): geode-<ver>-jvm.tar.gz for the Homebrew formula
#
# Unlike build-cli there is no "no Compose UI" assertion — geode depends only on
# :quartz and never pulls the Compose render stack. The GHCR Docker image is a
# separate job (docker-geode) below.
#
# Asset naming: geode-<version>-<family>-<arch>.<ext>. See scripts/asset-name.sh.
# ---------------------------------------------------------------------------
build-geode:
strategy:
fail-fast: false
matrix:
include:
- { os: macos-14, arch: arm64, family: macos, tasks: "geodeImage" }
- { os: ubuntu-latest, arch: x64, family: linux, tasks: "geodeImage jpackageDeb jpackageRpm" }
runs-on: ${{ matrix.os }}
timeout-minutes: 45 # macOS leg also codesigns + notarizes the jlink image
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
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)
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"
- name: Install RPM tooling (linux only)
if: matrix.family == 'linux'
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot
- name: Build geode artifacts
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
max_attempts: 2
timeout_minutes: 15
command: ./gradlew --no-daemon :geode:${{ matrix.tasks }}
# Boot the bundled jlink image before shipping it. `--version` proves the
# JVM starts and the main class loads; the serve leg proves the jlink
# module list is complete for the real relay path (Ktor CIO + SQLite +
# NIP-11 serialization) — a too-tight module list links fine but fails
# here with NoClassDefFound instead of on an operator's machine.
- name: Smoke-test the geode image
run: |
set -euo pipefail
IMG="geode/build/geode-image/geode"
"$IMG/bin/geode" --version
"$IMG/bin/geode" --port 17447 &
PID=$!
ok=0
for i in $(seq 1 20); do
if curl -fsS -H 'Accept: application/nostr+json' http://127.0.0.1:17447/ -o /tmp/nip11.json; then ok=1; break; fi
sleep 1
done
kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
[[ "$ok" == 1 ]] || { echo "::error::geode image did not serve NIP-11 within 20s"; exit 1; }
echo "NIP-11 doc:"; head -c 400 /tmp/nip11.json; echo
grep -q '"supported_nips"' /tmp/nip11.json || { echo "::error::NIP-11 doc missing supported_nips"; exit 1; }
# macOS only: import the Developer ID cert (no-op without the secret) so
# the next step can codesign the jlink image. The jvm bundle for
# Homebrew-core is NOT signed here — Homebrew strips quarantine itself.
- name: Import Apple Developer ID certificate (macOS leg, if configured)
if: matrix.family == 'macos'
id: mac_keychain
uses: ./.github/actions/import-macos-cert
with:
certificate-p12-base64: ${{ secrets.MAC_CERTIFICATE_P12 }}
certificate-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
# Codesign + notarize the macOS jlink image (geode-<ver>-macos-arm64.tar.gz)
# for operators who download it directly. A loose tarball can't be stapled
# (stapler only does .app/.dmg/.pkg), so Gatekeeper verifies notarization
# online on first run. Runs before "Collect" so the tarred image is signed.
- name: Sign + notarize geode image (macOS leg, if configured)
if: matrix.family == 'macos' && steps.mac_keychain.outputs.signing == 'true'
env:
SIGN_IDENTITY: ${{ secrets.MAC_SIGN_IDENTITY }}
NOTARY_APPLE_ID: ${{ secrets.MAC_NOTARY_APPLE_ID }}
NOTARY_PASSWORD: ${{ secrets.MAC_NOTARY_PASSWORD }}
NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }}
run: |
set -euo pipefail
IMG="geode/build/geode-image/geode"
ENTITLEMENTS="geode/packaging/macos/geode.entitlements"
# Sign the macOS Mach-O natives buried INSIDE the bundled jars
# (secp256k1/sqlite) first — Apple's notary recurses into jars and
# rejects any unsigned Mach-O, so this must run before notarize.
SIGN_IDENTITY="$SIGN_IDENTITY" scripts/sign-macos-jar-natives.sh "$IMG"
# Sign every loose Mach-O binary in the bundled JRE. Executables get
# the hardened-runtime entitlements; dylibs don't.
while IFS= read -r f; do
case "$(file -b "$f")" in
*Mach-O*executable*)
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" --sign "$SIGN_IDENTITY" "$f" ;;
*Mach-O*)
codesign --force --options runtime --timestamp \
--sign "$SIGN_IDENTITY" "$f" ;;
esac
done < <(find "$IMG" -type f)
codesign --verify --strict --verbose=2 "$IMG/runtime/bin/java"
# Notarize: zip the signed image, submit, wait for Apple's verdict.
ZIP="$RUNNER_TEMP/geode-notarize.zip"
OUT="$RUNNER_TEMP/notary-submit.json"
ditto -c -k --keepParent "$IMG" "$ZIP"
if ! xcrun notarytool submit "$ZIP" \
--apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" \
--team-id "$NOTARY_TEAM_ID" --wait --output-format json > "$OUT"; then
echo "::warning::notarytool submit exited non-zero"
fi
cat "$OUT"
STATUS="$(jq -r '.status // "Unknown"' "$OUT" 2>/dev/null || echo Unknown)"
SUBMISSION_ID="$(jq -r '.id // empty' "$OUT" 2>/dev/null || true)"
if [ "$STATUS" != "Accepted" ]; then
echo "::error::Notarization status: $STATUS"
if [ -n "$SUBMISSION_ID" ]; then
echo "----- notary log -----"
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" \
--team-id "$NOTARY_TEAM_ID" || true
fi
exit 1
fi
# jpackage pins libicu to the build host's version (libicu74 on
# ubuntu-24.04). Rewrite the .deb so it installs across Debian/Ubuntu.
- name: Relax libicu dependency in .deb
if: matrix.family == 'linux'
run: |
set -euo pipefail
chmod +x scripts/relax-deb-libicu.sh
scripts/relax-deb-libicu.sh geode/build/jpackage/*.deb
- name: Collect + rename assets
run: |
set -euo pipefail
# shellcheck source=scripts/asset-name.sh
source scripts/asset-name.sh
collect_geode_assets "${{ matrix.family }}" "${{ matrix.arch }}" "${{ steps.ver.outputs.version }}" dist
# Homebrew-core ships a no-JRE jar bundle and depends_on "openjdk" — it
# cannot use the jlink tarball above (bundled runtime) nor build from
# source (its sandbox blocks Gradle's Maven downloads). installDist
# (bin/geode + lib/*.jar, no runtime/) is exactly that bundle. It is pure
# JVM bytecode, so one platform-independent asset serves every OS; we cut
# it on the linux leg only. geodeImage depends on installDist, so the
# geode/build/install/geode tree already exists here.
- name: Package no-JRE jvm bundle for Homebrew (linux leg only)
if: matrix.family == 'linux'
run: |
set -euo pipefail
VER="${{ steps.ver.outputs.version }}"
SRC="geode/build/install/geode"
test -x "$SRC/bin/geode"
( cd "$SRC" && tar czf "$OLDPWD/dist/geode-${VER}-jvm.tar.gz" bin lib )
echo "Collected: dist/geode-${VER}-jvm.tar.gz"
- name: Enforce geode size budget (200 MB 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 > 209715200 )); then
echo "::error file=$f::asset is ${mb} MB — exceeds 200 MB geode 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 }}"
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@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
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: geode ${{ 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"
# ---------------------------------------------------------------------------
# geode Docker image → GitHub Container Registry (GHCR). A relay is most often
# deployed as a container, so this is geode's primary distribution channel.
# Builds geode/Dockerfile (multi-stage: gradle installDist → temurin JRE) and
# pushes ghcr.io/<owner>/geode:<version> (+ :latest on a stable release).
# Tag-push only — skipped on the workflow_dispatch dry-run.
# ---------------------------------------------------------------------------
docker-geode:
if: github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Resolve version + tags
id: meta
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME}"
VER="${TAG#v}"
# Lowercase owner — GHCR repository paths must be lowercase.
OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
IMAGE="ghcr.io/${OWNER}/geode"
TAGS="${IMAGE}:${VER}"
# Only move :latest for a stable vX.Y.Z tag, never a prerelease.
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TAGS="${TAGS},${IMAGE}:latest"
fi
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: geode/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ---------------------------------------------------------------------------
# Android build + sign + direct-upload. Logic preserved from previous workflow;
# uses softprops/action-gh-release@v2 instead of deprecated upload-release-asset.
+61
View File
@@ -0,0 +1,61 @@
# Multi-stage image for the geode Nostr relay.
#
# Build from the REPO ROOT (the build needs the whole Gradle project), pointing
# at this file:
#
# docker build -f geode/Dockerfile -t geode:local .
# docker run --rm -p 7447:7447 geode:local
#
# That runs the relay on 0.0.0.0:7447 with the built-in in-memory store — events
# vanish on restart. For a persistent, configured deployment, mount a config and
# a data volume:
#
# docker run -d --name geode -p 7447:7447 \
# -v $PWD/geode.toml:/etc/geode/geode.toml:ro \
# -v geode-data:/var/lib/geode \
# geode:local --config /etc/geode/geode.toml
#
# with `file = "/var/lib/geode/events.db"` and `in_memory = false` in geode.toml.
#
# Published to GHCR on every release by .github/workflows/create-release.yml
# (docker-geode job) as ghcr.io/vitorpamplona/geode:<version> and :latest.
# --- build stage ------------------------------------------------------------
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
# Copy the whole repo (geode depends only on :quartz, but the Gradle build needs
# settings.gradle.kts, the version catalog, and the wrapper). .dockerignore keeps
# build outputs and VCS metadata out of the context.
COPY . .
# installDist emits bin/geode + lib/*.jar under geode/build/install/geode.
RUN ./gradlew --no-daemon :geode:installDist
# --- runtime stage ----------------------------------------------------------
FROM eclipse-temurin:21-jre
LABEL org.opencontainers.image.source="https://github.com/vitorpamplona/amethyst"
LABEL org.opencontainers.image.description="geode — a standalone Nostr relay"
LABEL org.opencontainers.image.licenses="MIT"
# Run as a non-root user that owns the data dir.
RUN groupadd --system geode \
&& useradd --system --gid geode --home-dir /var/lib/geode --shell /usr/sbin/nologin geode \
&& mkdir -p /var/lib/geode \
&& chown geode:geode /var/lib/geode
COPY --from=build /src/geode/build/install/geode /opt/geode
# Ship the example config for reference inside the image.
COPY --from=build /src/geode/config.example.toml /opt/geode/share/geode/config.example.toml
ENV PATH="/opt/geode/bin:${PATH}"
VOLUME /var/lib/geode
EXPOSE 7447
USER geode
# Liveness: the default WebSocket port accepts a TCP connection. Uses bash's
# /dev/tcp (the temurin image is Ubuntu-based) so no curl/nc is needed. Override
# the port with --port and this HEALTHCHECK together if you remap it.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD bash -c 'exec 3<>/dev/tcp/127.0.0.1/7447' || exit 1
ENTRYPOINT ["/opt/geode/bin/geode"]
CMD ["--host", "0.0.0.0", "--port", "7447"]
+122
View File
@@ -0,0 +1,122 @@
# geode
A standalone [Nostr](https://github.com/nostr-protocol/nips) relay for the JVM,
built on Quartz's relay-server code (Ktor CIO). It speaks the core relay
protocol plus NIP-11 (info doc), NIP-42 (AUTH), NIP-45 (COUNT), NIP-50
(full-text search), NIP-77 (Negentropy sync), and NIP-86 (relay management),
stores events in SQLite (or a filesystem backend), and can mirror upstream
relays strfry-router style.
geode depends only on `:quartz` — no Android, no Compose. `amy serve` (the
Amethyst CLI) embeds the same engine in-process.
## Install
Pick the channel that matches how you deploy. For a real relay, the **Docker
image** or the **`.deb`/`.rpm` + systemd** are the two production paths;
Homebrew and the portable tarball are handy for local testing.
### Docker (recommended for servers)
Images are published to GHCR on every release:
```bash
# Ephemeral — in-memory store, events vanish on restart:
docker run --rm -p 7447:7447 ghcr.io/vitorpamplona/geode:latest
# Persistent + configured:
docker run -d --name geode -p 7447:7447 \
-v $PWD/geode.toml:/etc/geode/geode.toml:ro \
-v geode-data:/var/lib/geode \
ghcr.io/vitorpamplona/geode:latest --config /etc/geode/geode.toml
```
with `in_memory = false` and `file = "/var/lib/geode/events.db"` in your
`geode.toml`. Pin a version (`:1.12.6`) instead of `:latest` for reproducible
deploys. To build the image yourself, from the repo root:
```bash
docker build -f geode/Dockerfile -t geode:local .
```
### Debian/Ubuntu (`.deb`) and Fedora/RHEL (`.rpm`)
Download `geode-<version>-linux-x64.deb` (or `.rpm`) from the
[release page](https://github.com/vitorpamplona/amethyst/releases) and install
it — a minimal JRE is bundled, so no system Java is required. It installs to
`/opt/geode/` with the launcher at `/opt/geode/bin/geode`.
```bash
sudo dpkg -i geode-1.12.6-linux-x64.deb # or: sudo rpm -i geode-1.12.6-linux-x64.rpm
```
To run it as a managed service, wire up the shipped systemd unit
(`/opt/geode/share/geode/geode.service`) — the unit file's header comment has
the copy-paste steps (create the `geode` user, drop your config in
`/etc/geode/geode.toml`, `systemctl enable --now geode`).
### Homebrew
```bash
brew install vitorpamplona/tap/geode # from the tap, once published
```
The formula depends on `openjdk` and installs the no-JRE jar bundle. Reference
formula: [`packaging/homebrew/geode.rb`](packaging/homebrew/geode.rb).
### Portable tarball (any Linux/macOS, no system Java)
Download `geode-<version>-<os>-<arch>.tar.gz`, unpack, and run:
```bash
tar xzf geode-1.12.6-linux-x64.tar.gz
./geode/bin/geode --config geode/share/geode/config.example.toml
```
The tarball bundles a jlink'd JRE plus `share/geode/config.example.toml` and
`share/geode/geode.service`.
### From source
```bash
./gradlew :geode:run --args="--config /etc/geode.toml"
```
## Configure
geode runs with zero config (binds `0.0.0.0:7447`, in-memory SQLite). For
anything real, copy [`config.example.toml`](config.example.toml) and edit it —
the sections mirror nostr-rs-relay's `config.toml` so existing configs port
almost verbatim. Precedence is **CLI flags > TOML > built-in defaults**.
```bash
geode --config /etc/geode/geode.toml
geode --help # full flag list
geode --version
```
Key sections: `[info]` (NIP-11 doc), `[network]` (bind + thread pools),
`[database]` (SQLite path/tuning), `[options]` (AUTH / verify / search),
`[authorization]` (allow/deny lists), `[[mirror]]` (upstream mirroring), and
`[admin]` (NIP-86 management). See the example file for every knob.
## Verbs
```
geode [relay] [flags] serve the relay (default)
geode import [flags] [FILE…] bulk-load NDJSON events into the store
geode export [flags] dump the store as NDJSON to stdout
```
`import`/`export` are geode's `strfry import` / `strfry export` — one JSON event
per line, for seeding, migrating, or backing up a relay. Both stream, so a
multi-million-event corpus round-trips in roughly constant memory.
## Release process
geode ships on the same tag-driven pipeline as the rest of Amethyst: pushing a
`v*` tag runs `.github/workflows/create-release.yml`, whose `build-geode` job
produces the tarball + `.deb`/`.rpm` + Homebrew jvm bundle, `docker-geode`
builds and pushes the GHCR image, and `bump-homebrew-geode-formula.yml` syncs
the reference formula. Design notes:
[`plans/2026-07-24-geode-release.md`](plans/2026-07-24-geode-release.md).
+205
View File
@@ -137,3 +137,208 @@ dependencies {
testImplementation(libs.secp256k1.kmp.jni.jvm)
testImplementation(libs.okhttp)
}
// ---------------------------------------------------------------------------
// Native distribution (jlink + jpackage)
//
// geode is a long-running server daemon, so unlike a desktop app the useful
// artifacts are a bundled-runtime tarball and native Linux packages an operator
// can drop onto a box. This mirrors the amy CLI's pipeline (cli/build.gradle.kts)
// — geode is the same kind of `application`-plugin JVM module — but geode drags
// in no Compose UI (it depends only on :quartz), so there's no render-stack to
// exclude and no "no Compose" CI assertion to guard.
//
// Outputs land under geode/build/:
// - geode-image/geode/ portable, flat directory (bin/ + lib/ + runtime/ +
// share/ with config.example.toml + geode.service).
// tar this on every OS → geode-<ver>-<fam>-<arch>.tar.gz
// - jpackage/geode_*.deb Debian/Ubuntu package (Linux runners only)
// - jpackage/geode-*.rpm Fedora/RHEL package (Linux runners only)
//
// We build our own flat app-image instead of `jpackage --type app-image` because
// on macOS jpackage buries the binary inside an `.app` bundle — wrong for a CLI-
// launched daemon. The flat tree matches the Linux jpackage layout on every OS.
//
// Release wiring lives in .github/workflows/create-release.yml (build-geode job)
// and geode/Dockerfile (the GHCR image). See the plan at
// geode/plans/2026-07-24-geode-release.md.
// ---------------------------------------------------------------------------
val appVersion: String = project.version.toString()
// RPM rejects dashes in version strings — replace with tilde (~), which RPM
// treats as prerelease-lower-than: 1.08.0~rc1 < 1.08.0.
val rpmVersion: String = appVersion.replace("-", "~")
val mainJarName: String = "geode-$appVersion.jar"
val mainClassName: String = "com.vitorpamplona.geode.MainKt"
// Minimal JDK 21 module set for geode. Keep this tight — every module adds
// megabytes. geode needs more than the CLI: java.management (Ktor CIO / JVM
// metrics), java.sql (the bundled SQLite driver), java.net.http +
// java.security.jgss (okhttp mirror WebSockets over TLS), jdk.crypto.ec (EC
// used by the TLS handshake), jdk.unsupported (sun.misc.Unsafe, touched by
// coroutines/SQLite/CIO). If a transitive dep needs more, `jlink` succeeds at
// link time but the server fails at runtime with NoClassDefFound — the CI
// dry-run (workflow_dispatch) boots the image to catch that early.
val jlinkModules: String = listOf(
"java.base",
"java.logging",
"java.management",
"java.naming",
"java.net.http",
"java.security.jgss",
"java.sql",
"java.xml",
"jdk.crypto.ec",
"jdk.unsupported",
).joinToString(",")
fun javaToolBin(name: String): Provider<String> =
javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
}.map {
val exe = if (org.gradle.internal.os.OperatingSystem.current().isWindows) "$name.exe" else name
it.metadata.installationPath.file("bin/$exe").asFile.absolutePath
}
val jlinkRuntimeDir = layout.buildDirectory.dir("jlink-runtime")
val geodeImageDir = layout.buildDirectory.dir("geode-image/geode")
val installLibDir = layout.buildDirectory.dir("install/geode/lib")
val jpackageOutDir = layout.buildDirectory.dir("jpackage")
val jlinkRuntime =
tasks.register<Exec>("jlinkRuntime") {
group = "distribution"
description = "Build a minimal JRE for geode via jlink."
outputs.dir(jlinkRuntimeDir)
val jlinkBin = javaToolBin("jlink")
val outDir = jlinkRuntimeDir
val modules = jlinkModules
doFirst {
// jlink refuses to write into an existing directory.
outDir.get().asFile.deleteRecursively()
executable = jlinkBin.get()
args(
"--add-modules", modules,
"--no-header-files",
"--no-man-pages",
"--strip-debug",
// JDK 21+: --compress <int> is deprecated; use zip-<level>.
"--compress", "zip-6",
"--output", outDir.get().asFile.absolutePath,
)
}
}
// Flat app-image: bin/geode launcher + lib/*.jar + runtime/ (the jlink'd JRE) +
// share/geode/ (config.example.toml + the systemd unit). Cross-platform — the
// release workflow tars this up on every OS.
val geodeImage =
tasks.register<Sync>("geodeImage") {
group = "distribution"
description = "Assemble a portable geode app-image (bin/ + lib/ + runtime/ + share/)."
dependsOn(tasks.named("installDist"), jlinkRuntime)
into(geodeImageDir)
// jars from installDist
from(installLibDir) {
into("lib")
}
// jlink'd JRE
from(jlinkRuntimeDir) {
into("runtime")
}
// Operator seed files: an example config and the systemd unit.
from(layout.projectDirectory.file("config.example.toml")) {
into("share/geode")
}
from(layout.projectDirectory.file("packaging/systemd/geode.service")) {
into("share/geode")
}
val mainJar = mainJarName
val mainClass = mainClassName
val unixLauncher =
"""
#!/bin/sh
# geode launcher — uses the bundled jlink'd JRE so no system Java is required.
DIR="${'$'}(cd "${'$'}(dirname "${'$'}0")/.." && pwd)"
exec "${'$'}DIR/runtime/bin/java" -cp "${'$'}DIR/lib/*" $mainClass "${'$'}@"
""".trimIndent() + "\n"
doLast {
val binDir = geodeImageDir.get().asFile.resolve("bin")
binDir.mkdirs()
val launcher = binDir.resolve("geode")
launcher.writeText(unixLauncher)
launcher.setExecutable(true, false)
}
}
fun registerJpackage(
taskName: String,
type: String,
extraArgs: List<String> = emptyList(),
) = tasks.register<Exec>(taskName) {
group = "distribution"
description = "Run jpackage --type $type for geode."
dependsOn(tasks.named("installDist"), jlinkRuntime)
inputs.dir(installLibDir)
inputs.dir(jlinkRuntimeDir)
outputs.dir(jpackageOutDir)
val jpackageBin = javaToolBin("jpackage")
val inDir = installLibDir
val runtimeDir = jlinkRuntimeDir
val outDir = jpackageOutDir
val versionArg = if (type == "rpm") rpmVersion else appVersion
val extra = extraArgs
doFirst {
outDir.get().asFile.mkdirs()
executable = jpackageBin.get()
args(
"--type", type,
"--name", "geode",
"--app-version", versionArg,
"--vendor", "Amethyst Contributors",
"--description", "geode — a standalone Nostr relay.",
"--input", inDir.get().asFile.absolutePath,
"--runtime-image", runtimeDir.get().asFile.absolutePath,
"--main-jar", mainJarName,
"--main-class", mainClassName,
"--dest", outDir.get().asFile.absolutePath,
)
args(extra)
}
}
// .deb for Debian/Ubuntu. Installs under /opt/geode/ with /opt/geode/bin/geode
// as the launcher. No .desktop entry — geode is a headless daemon. The systemd
// unit + example config are shipped for the operator to wire up (see README).
registerJpackage(
"jpackageDeb",
"deb",
extraArgs = listOf(
"--linux-package-name", "geode",
"--linux-deb-maintainer", "vitor@vitorpamplona.com",
),
)
// .rpm for Fedora/RHEL/openSUSE.
registerJpackage(
"jpackageRpm",
"rpm",
extraArgs = listOf(
"--linux-package-name", "geode",
"--linux-rpm-license-type", "MIT",
),
)
+51
View File
@@ -0,0 +1,51 @@
# Reference Homebrew formula for `geode`, the standalone Nostr relay.
#
# Submit this to Homebrew/homebrew-core (new-formula PR) or drop it into a
# personal tap (`Formula/geode.rb`) for an instant `brew install <tap>/geode`.
#
# The url + sha256 below are kept in sync automatically on every stable release
# by .github/workflows/bump-homebrew-geode-formula.yml (it downloads the
# published `geode-<version>-jvm.tar.gz`, recomputes the sha256, and opens a
# PR). To refresh by hand instead:
# curl -fsSL -o geode-jvm.tar.gz \
# https://github.com/vitorpamplona/amethyst/releases/download/vX.Y.Z/geode-X.Y.Z-jvm.tar.gz
# shasum -a 256 geode-jvm.tar.gz
#
# Why a pre-built jar bundle instead of building from source:
# homebrew-core builds inside a network sandbox, so a Gradle build cannot
# resolve its Maven dependencies there. The accepted pattern for JVM tools is
# to download a pre-built, no-JRE jar bundle and depend on the system openjdk.
# We publish exactly that as `geode-<version>-jvm.tar.gz` (bin/geode +
# lib/*.jar, no bundled runtime) from .github/workflows/create-release.yml.
#
# Note: geode is a long-running relay daemon. Homebrew is a convenient way to
# INSTALL it on a workstation for local testing; for a production deployment
# prefer the Docker image or the .deb/.rpm + systemd unit (see geode/README.md).
class Geode < Formula
desc "Standalone Nostr relay from the Amethyst project"
homepage "https://github.com/vitorpamplona/amethyst"
url "https://github.com/vitorpamplona/amethyst/releases/download/v1.12.6/geode-1.12.6-jvm.tar.gz"
sha256 "0000000000000000000000000000000000000000000000000000000000000000"
license "MIT"
# Lets homebrew-core's BrewTestBot auto-open version-bump PRs when a new
# stable GitHub release appears.
livecheck do
url :stable
strategy :github_latest
end
depends_on "openjdk"
def install
# Tarball top level is bin/ and lib/ (the Gradle installDist layout).
libexec.install Dir["*"]
# Wrapper on PATH that pins JAVA_HOME to Homebrew's openjdk so geode runs
# regardless of the user's own Java setup.
(bin/"geode").write_env_script libexec/"bin/geode", JAVA_HOME: Formula["openjdk"].opt_prefix
end
test do
assert_match "geode", shell_output("#{bin}/geode --version 2>&1")
end
end
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
Hardened-runtime entitlements for geode's bundled JVM (runtime/bin/java).
Required when codesigning + notarizing the macOS jlink image:
- allow-jit / allow-unsigned-executable-memory: the JIT compiler writes
and executes generated machine code.
- disable-library-validation: geode loads the secp256k1 native .dylib that
secp256k1-kmp-jni-jvm extracts from a jar at runtime (Schnorr signature
verification), plus the bundled SQLite native; those dylibs are not
signed by our Team ID, so library validation would otherwise block them.
- allow-dyld-environment-variables: the launcher sets JVM env.
Applied only to Mach-O *executables* in the image; plain dylibs are signed
without entitlements. See .github/workflows/create-release.yml (build-geode).
-->
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
+50
View File
@@ -0,0 +1,50 @@
# systemd unit for the geode Nostr relay.
#
# Install (paths assume the .deb/.rpm or the portable tarball unpacked to
# /opt/geode — adjust ExecStart if you put it elsewhere):
#
# sudo useradd --system --home /var/lib/geode --shell /usr/sbin/nologin geode
# sudo mkdir -p /var/lib/geode /etc/geode
# sudo cp /opt/geode/share/geode/config.example.toml /etc/geode/geode.toml # then edit
# sudo cp /opt/geode/share/geode/geode.service /etc/systemd/system/geode.service
# sudo chown -R geode:geode /var/lib/geode
# sudo systemctl daemon-reload
# sudo systemctl enable --now geode
#
# Logs: journalctl -u geode -f
[Unit]
Description=geode Nostr relay
Documentation=https://github.com/vitorpamplona/amethyst/blob/main/geode/README.md
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=geode
Group=geode
# Point --config at your edited copy. Drop the flag to run on built-in
# defaults (0.0.0.0:7447, in-memory store — events vanish on restart).
ExecStart=/opt/geode/bin/geode --config /etc/geode/geode.toml
Restart=on-failure
RestartSec=5
# A relay holds one file descriptor per WebSocket; the distro default of 1024
# caps it well below 1k connections. Raise to match [network] tuning.
LimitNOFILE=65536
# systemd creates + owns /var/lib/geode (0750, User:Group above). Point
# [database].file there, e.g. file = "/var/lib/geode/events.db".
StateDirectory=geode
WorkingDirectory=/opt/geode
# Hardening — geode needs no elevated access. Loosen only if a custom
# IEventStore writes outside its StateDirectory.
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/geode
[Install]
WantedBy=multi-user.target
+76
View File
@@ -0,0 +1,76 @@
# geode release & distribution
**Status:** shipped (2026-07-24)
## Goal
Give geode a real release process so people can run it without cloning the repo
and invoking Gradle. Reuse the amy CLI's proven pipeline wherever possible —
geode is the same kind of module (a Gradle `application`-plugin JVM app) — and
add the pieces a long-running **server daemon** needs that a one-shot CLI does
not (a container image, a systemd unit).
## Why the CLI pipeline maps onto geode
`cli/build.gradle.kts` already models everything geode needs for the
tarball/deb/rpm channels: `jlinkRuntime` (minimal JRE) → an image task
(portable flat `bin/`+`lib/`+`runtime/`) → `jpackageDeb`/`jpackageRpm`, driven
by the `build-cli` matrix in `create-release.yml` and named through
`scripts/asset-name.sh`. geode is `application`-plugin + JVM 21 too, so the same
task shapes drop in.
Two simplifications vs. the CLI:
- **No Compose to exclude.** amy drags the Compose UI render stack in
transitively via `:commons` and has to exclude skiko/compose.ui from the
runtime classpath (plus a CI assertion). geode depends only on `:quartz`, so
there's nothing to strip and no assertion to add.
- **A wider jlink module set.** amy is compute-only; geode is a network server
with a SQLite store, so its module list adds `java.management`, `java.sql`,
`java.net.http`, and `java.security.jgss`. A too-tight list links fine but
fails at runtime, so the `build-geode` job **boots the image** (`--version`
plus a serve + NIP-11 curl) as a smoke test before shipping.
## What shipped
| Piece | File |
| --- | --- |
| `--version` / `--help` (testable exit-0 command) | `geode/src/main/kotlin/.../Main.kt` |
| jlink + jpackage + `geodeImage` tasks | `geode/build.gradle.kts` |
| Portable app-image seeds (config + unit under `share/geode/`) | `geode/build.gradle.kts` (geodeImage) |
| Multi-stage Docker image | `geode/Dockerfile`, `.dockerignore` |
| systemd unit | `geode/packaging/systemd/geode.service` |
| macOS hardened-runtime entitlements | `geode/packaging/macos/geode.entitlements` |
| Reference Homebrew formula | `geode/packaging/homebrew/geode.rb` |
| Asset naming (`geode-<ver>-<fam>-<arch>`) | `scripts/asset-name.sh` |
| Release matrix + GHCR push | `.github/workflows/create-release.yml` (`build-geode`, `docker-geode`) |
| Homebrew formula auto-sync | `.github/workflows/bump-homebrew-geode-formula.yml` |
| geode tests in CI | `.github/workflows/build.yml` (`:geode:test`) |
| Operator docs | `geode/README.md` |
## Distribution channels
1. **Docker → GHCR** (`ghcr.io/vitorpamplona/geode:<version>` + `:latest`) —
the primary channel for relay operators. Built from `geode/Dockerfile`.
2. **`.deb` / `.rpm`** with a bundled JRE, install to `/opt/geode`, plus the
shipped systemd unit for `systemctl enable --now geode`.
3. **Portable tarball** with a jlink'd JRE — no system Java needed.
4. **Homebrew** (no-JRE jar bundle + `depends_on openjdk`) for local testing.
All four (except the container) are named by `scripts/asset-name.sh` and
uploaded to the GitHub Release on `v*` tags.
## Follow-ups / not done
- **Multi-arch Docker.** `docker-geode` builds `linux/amd64` only. arm64 under
QEMU emulation would run a full Gradle build per arch (slow / timeout-prone);
add a native arm64 runner leg when one is available.
- **jpackage doesn't auto-install the systemd unit or create the `geode`
user.** The `.deb`/`.rpm` install the binary; service wiring is a documented
manual step (unit ships in the package under `share/geode/`). A post-install
scriptlet via jpackage `--resource-dir` could automate it later.
- **homebrew-core bootstrap.** The first `geode` submission to homebrew-core is
a manual new-formula PR; the auto-bump workflow only keeps the in-repo
reference formula synced until then (same state as `amy`).
- **CI size budget** is set to 200 MB per asset (matches amy). geode should be
well under that — tighten once the first release confirms the real size.
@@ -111,6 +111,18 @@ import java.io.File
* per-event tokenization cost on ingest.
*/
fun main(args: Array<String>) {
// Terminal flags handled before verb dispatch so a packaged binary has a
// fast, exit-0 command that never opens a store or binds a port — the smoke
// test the Homebrew formula, the .deb/.rpm post-install check, and the
// Docker image healthcheck all invoke.
if (args.any { it == "--version" || it == "-V" }) {
println("geode ${BuildConfig.VERSION}")
return
}
if (args.any { it == "--help" || it == "-h" }) {
printHelp()
return
}
when (args.firstOrNull()?.takeUnless { it.startsWith("--") }) {
"import" -> runImport(args.copyOfRange(1, args.size))
"export" -> runExport(args.copyOfRange(1, args.size))
@@ -121,6 +133,36 @@ fun main(args: Array<String>) {
}
}
private fun printHelp() {
println(
"""
geode ${BuildConfig.VERSION} - a standalone Nostr relay.
Usage:
geode [relay] [flags] serve the relay (default when no verb is given)
geode import [flags] [FILE...] bulk-load NDJSON events into the store
geode export [flags] dump the store as NDJSON to stdout
Common flags:
--config <file> TOML config (see config.example.toml)
--host <addr> bind address (default 0.0.0.0)
--port <n> tcp port (default 7447, 0 to autobind)
--path <p> websocket path (default /)
--db <file> sqlite db path (persistent storage)
--store <backend> event-store backend: sqlite (default), fs, or a
fully-qualified IEventStore class name
--auth require NIP-42 AUTH
--optional-auth advertise NIP-42 AUTH but don't require it
--no-verify do NOT verify event signatures (trusted input only)
--no-search disable NIP-50 full-text search
--version, -V print the version and exit
--help, -h print this help and exit
Docs: https://github.com/vitorpamplona/amethyst/blob/main/geode/README.md
""".trimIndent(),
)
}
/**
* Opens the store the `import`/`export` verbs operate on, honoring the same
* `--db`/`--config`/`--no-search` selection the relay uses so a verb targets the
+63 -5
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env bash
# Single source of truth for Amethyst release asset naming.
#
# Two product lines share this contract:
# Three product lines share this contract:
# amethyst-desktop-<version>-<family>-<arch>.<ext> (Compose Desktop app)
# amy-<version>-<family>-<arch>.<ext> (Amy CLI — cli/ module)
# geode-<version>-<family>-<arch>.<ext> (geode relay — geode/ module)
#
# <version> = tag stripped of leading 'v' (e.g. "1.08.0")
# <family> = macos | windows | linux
@@ -33,13 +34,18 @@
# amy-1.08.0-linux-x64.tar.gz
# amy-1.08.0-linux-x64.deb
# amy-1.08.0-linux-x64.rpm
# geode-1.08.0-macos-arm64.tar.gz
# geode-1.08.0-linux-x64.tar.gz
# geode-1.08.0-linux-x64.deb
# geode-1.08.0-linux-x64.rpm
#
# One CLI asset breaks the family/arch shape on purpose: the no-JRE jar bundle
# for Homebrew-core is pure JVM bytecode (no bundled runtime), so a single
# Two assets break the family/arch shape on purpose: the no-JRE jar bundles for
# Homebrew-core are pure JVM bytecode (no bundled runtime), so a single
# platform-independent artifact serves every OS:
# amy-1.08.0-jvm.tar.gz
# It is packaged inline in create-release.yml (linux leg), not via the helpers
# below, since it has no <family>/<arch> dimension.
# geode-1.08.0-jvm.tar.gz
# They are packaged inline in create-release.yml (linux leg), not via the helpers
# below, since they have no <family>/<arch> dimension.
set -euo pipefail
@@ -57,6 +63,13 @@ cli_asset_name() {
printf 'amy-%s-%s-%s.%s' "$version" "$family" "$arch" "$ext"
}
# geode relay variant of asset_name — same shape, different product prefix.
# Usage: geode_asset_name <family> <arch> <version> <ext>
geode_asset_name() {
local family="$1" arch="$2" version="$3" ext="$4"
printf 'geode-%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).
@@ -136,3 +149,48 @@ collect_cli_assets() {
done
shopt -u nullglob
}
# Copy + rename geode relay build outputs into <dest_dir> using the canonical
# geode-<version>-<family>-<arch>.<ext> scheme.
#
# Expected inputs:
# geode/build/geode-image/geode/ flat app-image built by :geode:geodeImage
# (bin/geode + lib/*.jar + runtime/ + share/)
# geode/build/jpackage/*.deb from :geode:jpackageDeb (Linux only)
# geode/build/jpackage/*.rpm from :geode:jpackageRpm (Linux only)
#
# Usage: collect_geode_assets <family> <arch> <version> <dest_dir>
collect_geode_assets() {
local family="$1" arch="$2" version="$3" dest="$4"
mkdir -p "$dest"
local abs_dest
abs_dest="$(cd "$dest" && pwd)"
shopt -s nullglob
# 1. Tar the flat app-image into geode-<version>-<family>-<arch>.tar.gz.
# This is the portable-across-OS asset — macOS runners produce only
# this one.
local app_image="geode/build/geode-image/geode"
if [ -d "$app_image" ]; then
local tarball="$abs_dest/$(geode_asset_name "$family" "$arch" "$version" tar.gz)"
( cd "$(dirname "$app_image")" && tar czf "$tarball" "$(basename "$app_image")" )
echo "Collected: $tarball"
fi
# 2. Linux native installers (.deb, .rpm). jpackage writes them directly
# to geode/build/jpackage/ with its own naming, so we re-copy under the
# canonical scheme.
local src base ext dst
for src in \
geode/build/jpackage/*.deb \
geode/build/jpackage/*.rpm \
; do
[ -f "$src" ] || continue
base="$(basename "$src")"
ext="${base##*.}"
dst="$abs_dest/$(geode_asset_name "$family" "$arch" "$version" "$ext")"
cp "$src" "$dst"
echo "Collected: $dst"
done
shopt -u nullglob
}