diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..9dce94ec97 --- /dev/null +++ b/.dockerignore @@ -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. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbb3d82f62..f7edd36f3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/bump-homebrew-geode-formula.yml b/.github/workflows/bump-homebrew-geode-formula.yml new file mode 100644 index 0000000000..b55d926245 --- /dev/null +++ b/.github/workflows/bump-homebrew-geode-formula.yml @@ -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--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= --sha256=` 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'] + }); diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 4236fa6955..3a10ca3e3c 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -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--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---.. 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--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//geode: (+ :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. diff --git a/geode/Dockerfile b/geode/Dockerfile new file mode 100644 index 0000000000..4486190a11 --- /dev/null +++ b/geode/Dockerfile @@ -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: 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"] diff --git a/geode/README.md b/geode/README.md new file mode 100644 index 0000000000..231d1c5794 --- /dev/null +++ b/geode/README.md @@ -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--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---.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). diff --git a/geode/build.gradle.kts b/geode/build.gradle.kts index ee636965c7..70254af99c 100644 --- a/geode/build.gradle.kts +++ b/geode/build.gradle.kts @@ -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---.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 = + 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("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 is deprecated; use zip-. + "--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("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 = emptyList(), +) = tasks.register(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", + ), +) diff --git a/geode/packaging/homebrew/geode.rb b/geode/packaging/homebrew/geode.rb new file mode 100644 index 0000000000..bbb27ea6bc --- /dev/null +++ b/geode/packaging/homebrew/geode.rb @@ -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 /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--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--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 diff --git a/geode/packaging/macos/geode.entitlements b/geode/packaging/macos/geode.entitlements new file mode 100644 index 0000000000..5898ba94ab --- /dev/null +++ b/geode/packaging/macos/geode.entitlements @@ -0,0 +1,27 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/geode/packaging/systemd/geode.service b/geode/packaging/systemd/geode.service new file mode 100644 index 0000000000..92cf35d321 --- /dev/null +++ b/geode/packaging/systemd/geode.service @@ -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 diff --git a/geode/plans/2026-07-24-geode-release.md b/geode/plans/2026-07-24-geode-release.md new file mode 100644 index 0000000000..1240138892 --- /dev/null +++ b/geode/plans/2026-07-24-geode-release.md @@ -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---`) | `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:` + `: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. diff --git a/geode/src/main/kotlin/com/vitorpamplona/geode/Main.kt b/geode/src/main/kotlin/com/vitorpamplona/geode/Main.kt index 704b5665b7..729b95b3a3 100644 --- a/geode/src/main/kotlin/com/vitorpamplona/geode/Main.kt +++ b/geode/src/main/kotlin/com/vitorpamplona/geode/Main.kt @@ -111,6 +111,18 @@ import java.io.File * per-event tokenization cost on ingest. */ fun main(args: Array) { + // 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) { } } +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 TOML config (see config.example.toml) + --host bind address (default 0.0.0.0) + --port tcp port (default 7447, 0 to autobind) + --path

websocket path (default /) + --db sqlite db path (persistent storage) + --store 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 diff --git a/scripts/asset-name.sh b/scripts/asset-name.sh index 908e51d565..d419b10c0d 100755 --- a/scripts/asset-name.sh +++ b/scripts/asset-name.sh @@ -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---. (Compose Desktop app) # amy---. (Amy CLI — cli/ module) +# geode---. (geode relay — geode/ module) # # = tag stripped of leading 'v' (e.g. "1.08.0") # = 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 / 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 / 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 +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 using the canonical naming scheme. # Usage: collect_assets # 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 using the canonical +# geode---. 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 +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---.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 +}