Files
iris_x_hermes/CI-SETUP.md
T
ARIA 7a6d922d12
CI / Kotlin tests (android host + desktop) (push) Successful in 8m5s
CI / Gateway plugin tests (push) Successful in 9m47s
Add QR pairing (terminal QR, in-app scanner, iris://pair deep link)
2026-08-22 22:43:13 +02:00

19 KiB

CI / Release setup — manual edit list

Everything needed for the Gitea workflows (CI + manual release). Items marked DONE were already applied; the rest are copy-paste instructions.


1. DONE — no action needed

  • gateway-plugin/tests/test_android.py — vendored byte-identical mirror of hermes-agent/tests/gateway/test_android.py (the git-ignored hermes checkout is the canonical copy; keep the two in sync when you change that test).
  • .pi-lens.json — added "ignore": ["gateway-plugin/tests/test_android.py"] so the scanner doesn't flag the vendored mirror.

2. NEW FILE: .gitea/workflows/ci.yml

Runs on every push to master and on PRs: gateway plugin tests + Kotlin host tests (android + desktop).

name: CI

on:
  push:
    branches: [master]
  pull_request:

jobs:
  gateway:
    name: Gateway plugin tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      # The gateway tests run inside the hermes-agent test harness, which is
      # git-ignored in this repo (read-only research reference). CI clones the
      # upstream repo at a pinned commit and drops in the vendored test copy.
      # Bump the pinned SHA when you update the local hermes-agent checkout.
      - name: Clone hermes-agent (pinned)
        run: |
          git clone https://github.com/NousResearch/hermes-agent.git hermes-agent
          git -C hermes-agent fetch --depth 1 origin 31f62d76af068abde3c699f91190e8ded07fd05b
          git -C hermes-agent checkout 31f62d76af068abde3c699f91190e8ded07fd05b

      - name: Sync venv
        run: |
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
          cd hermes-agent
          uv sync

      - name: Run android gateway tests
        run: |
          cp gateway-plugin/tests/test_android.py hermes-agent/tests/gateway/test_android.py
          cd hermes-agent
          IRIS_PLUGIN_DIR="$GITHUB_WORKSPACE/gateway-plugin" \
            scripts/run_tests.sh tests/gateway/test_android.py

  kotlin:
    name: Kotlin tests (android host + desktop)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"

      # gradle.properties pins org.gradle.java.home to a local JDK path;
      # strip it so CI uses the JDK installed by setup-java.
      - name: Strip local JDK pin
        run: sed -i '/^org\.gradle\.java\.home/d' app/gradle.properties

      - name: Install Android SDK
        run: |
          export ANDROID_HOME="$HOME/android-sdk"
          mkdir -p "$ANDROID_HOME/cmdline-tools"
          curl -fsSL -o /tmp/ct.zip \
            https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
          unzip -q /tmp/ct.zip -d "$ANDROID_HOME/cmdline-tools"
          mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
          yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null
          echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
          echo "sdk.dir=$ANDROID_HOME" > app/local.properties

      # Host-side tests only (no device needed). AGP auto-downloads the
      # missing SDK platforms (licenses accepted above).
      - name: Run host tests
        working-directory: app
        run: ./gradlew :shared:testAndroidHostTest :shared:desktopTest

3. NEW FILE: .gitea/workflows/release.yml

Manual trigger: repo → Actions → Release → Run workflow, enter a version (e.g. 0.2.0) and a changelog. It runs the same tests as CI, builds a signed Android APK + AAB and the Linux desktop packages (jpackage, JRE bundled), then creates the Gitea release v<version> with all artifacts as download attachments.

Note: builds + release creation happen in ONE job because Gitea/act_runner does not implement the GitHub artifacts API (upload-artifact@v4+ fails with GHESNotSupportedError).

name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: "Release version (e.g. 0.2.0)"
        required: true
        type: string
      changelog:
        description: "Release notes (markdown, shown on the release page)"
        required: false
        type: string

jobs:
  gateway:
    name: Gateway plugin tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Clone hermes-agent (pinned)
        run: |
          git clone https://github.com/NousResearch/hermes-agent.git hermes-agent
          git -C hermes-agent fetch --depth 1 origin 31f62d76af068abde3c699f91190e8ded07fd05b
          git -C hermes-agent checkout 31f62d76af068abde3c699f91190e8ded07fd05b

      - name: Sync venv
        run: |
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
          cd hermes-agent
          # pytest lives in the `dev` extra — a plain `uv sync` leaves the
          # venv without it and run_tests.sh refuses to run.
          uv sync --extra dev

      - name: Run android gateway tests
        run: |
          cp gateway-plugin/tests/test_android.py hermes-agent/tests/gateway/test_android.py
          cd hermes-agent
          IRIS_PLUGIN_DIR="$GITHUB_WORKSPACE/gateway-plugin" \
            scripts/run_tests.sh tests/gateway/test_android.py

  kotlin:
    name: Kotlin tests (android host + desktop)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"

      - name: Strip local JDK pin
        run: sed -i '/^org\.gradle\.java\.home/d' app/gradle.properties

      - name: Install Android SDK
        run: |
          export ANDROID_HOME="$HOME/android-sdk"
          mkdir -p "$ANDROID_HOME/cmdline-tools"
          curl -fsSL -o /tmp/ct.zip \
            https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
          unzip -q /tmp/ct.zip -d "$ANDROID_HOME/cmdline-tools"
          mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
          # Finite input from a file: `yes | sdkmanager` dies with SIGPIPE
          # (exit 141) under Gitea's `bash -e -o pipefail` once sdkmanager
          # exits before `yes` is done writing.
          for i in $(seq 100); do echo y; done > /tmp/sdk_licenses_yes.txt
          "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses < /tmp/sdk_licenses_yes.txt > /dev/null
          echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
          echo "sdk.dir=$ANDROID_HOME" > app/local.properties

      - name: Run host tests
        working-directory: app
        run: ./gradlew :shared:testAndroidHostTest :shared:desktopTest

  # Gitea/act_runner does not implement the GitHub artifacts API
  # (upload-artifact@v4+ fails with GHESNotSupportedError), so the builds and
  # the release creation happen in ONE job — no artifact handoff between jobs.
  release:
    name: Build + create Gitea release
    runs-on: ubuntu-latest
    needs: [gateway, kotlin]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"

      - name: Strip local JDK pin
        run: sed -i '/^org\.gradle\.java\.home/d' app/gradle.properties

      - name: Install Android SDK
        run: |
          export ANDROID_HOME="$HOME/android-sdk"
          mkdir -p "$ANDROID_HOME/cmdline-tools"
          curl -fsSL -o /tmp/ct.zip \
            https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
          unzip -q /tmp/ct.zip -d "$ANDROID_HOME/cmdline-tools"
          mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
          # Finite input from a file: `yes | sdkmanager` dies with SIGPIPE
          # (exit 141) under Gitea's `bash -e -o pipefail` once sdkmanager
          # exits before `yes` is done writing.
          for i in $(seq 100); do echo y; done > /tmp/sdk_licenses_yes.txt
          "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses < /tmp/sdk_licenses_yes.txt > /dev/null
          echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
          echo "sdk.dir=$ANDROID_HOME" > app/local.properties

      # jpackage --type deb shells out to fakeroot, which the runner image
      # does not ship.
      - name: Install fakeroot (for jpackage --type deb)
        run: sudo apt-get update -qq && sudo apt-get install -y -qq fakeroot

      - name: Restore release keystore (from Gitea secrets)
        env:
          KS_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
        run: |
          if [ -n "$KS_B64" ]; then
            echo "$KS_B64" | base64 -d > app/release.keystore
            echo "ANDROID_KEYSTORE_FILE=$GITHUB_WORKSPACE/app/release.keystore" >> "$GITHUB_ENV"
            echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> "$GITHUB_ENV"
            echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> "$GITHUB_ENV"
            echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> "$GITHUB_ENV"
            echo "Building SIGNED release APK"
          else
            echo "::warning::ANDROID_KEYSTORE_BASE64 secret not set — falling back to a DEBUG apk (see CI-SETUP.md §5)"
          fi

      - name: Build APK + AAB
        run: |
          VERSION=$(jq -r '.inputs.version' "$GITHUB_EVENT_PATH")
          cd app
          if [ -n "$ANDROID_KEYSTORE_FILE" ]; then
            # APK for direct sideloading, AAB for Play Store uploads.
            ./gradlew :androidApp:assembleRelease :androidApp:bundleRelease -PappVersion="$VERSION"
            cp androidApp/build/outputs/apk/release/androidApp-release.apk \
              "$GITHUB_WORKSPACE/iris-android-v$VERSION.apk"
            cp androidApp/build/outputs/bundle/release/androidApp-release.aab \
              "$GITHUB_WORKSPACE/iris-android-v$VERSION.aab"
          else
            ./gradlew :androidApp:assembleDebug :androidApp:bundleDebug -PappVersion="$VERSION"
            cp androidApp/build/outputs/apk/debug/androidApp-debug.apk \
              "$GITHUB_WORKSPACE/iris-android-v$VERSION-debug.apk"
            cp androidApp/build/outputs/bundle/debug/androidApp-debug.aab \
              "$GITHUB_WORKSPACE/iris-android-v$VERSION-debug.aab"
          fi

      # jpackage cannot cross-compile: this only produces Linux packages.
      # When a Windows / macOS runner exists later, add a second build job
      # for it (jpackage picks the native type: msi on Windows, dmg on macOS).
      - name: Build desktop app-image + deb
        run: |
          VERSION=$(jq -r '.inputs.version' "$GITHUB_EVENT_PATH")
          cd app
          # Self-contained app image (JRE bundled via jlink).
          ./gradlew :desktopApp:jpackage -PappVersion="$VERSION"
          (cd desktopApp/build/jpackage && zip -qr \
            "$GITHUB_WORKSPACE/iris-desktop-linux-x64-v$VERSION.zip" iris)
          # .deb package (dpkg-deb ships with Ubuntu; fakeroot installed above).
          ./gradlew :desktopApp:jpackage -PjpackageType=deb -PappVersion="$VERSION"
          cp desktopApp/build/jpackage/*.deb \
            "$GITHUB_WORKSPACE/iris-desktop-linux-x64-v$VERSION.deb"

      - name: Create release + upload artifacts
        env:
          # Optional: create a personal access token (scope: Releases: write)
          # and store it as secret GITEA_TOKEN. Without it the workflow uses
          # the automatic GITHUB_TOKEN that Gitea Actions provides.
          RELEASE_TOKEN: ${{ secrets.GITEA_TOKEN }}
        run: |
          set -euo pipefail
          SERVER="${GITEA_SERVER_URL:-$GITHUB_SERVER_URL}"
          REPO="${GITEA_REPOSITORY:-$GITHUB_REPOSITORY}"
          TOKEN="${RELEASE_TOKEN:-$GITHUB_TOKEN}"
          VERSION=$(jq -r '.inputs.version' "$GITHUB_EVENT_PATH")
          CHANGELOG=$(jq -r '.inputs.changelog // ""' "$GITHUB_EVENT_PATH")
          TAG="v$VERSION"
          API="$SERVER/api/v1/repos/$REPO"
          AUTH="Authorization: token $TOKEN"

          # Re-run safety: drop a previous release (and its tag) for this version.
          OLD_ID=$(curl -sf -H "$AUTH" "$API/releases/tags/$TAG" | jq -r '.id // empty')
          if [ -n "$OLD_ID" ]; then
            curl -sf -X DELETE -H "$AUTH" "$API/releases/$OLD_ID" > /dev/null
          fi

          # Gitea creates the tag at the default branch HEAD automatically.
          RELEASE_ID=$(curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \
            "$API/releases" \
            -d "$(jq -n --arg tag "$TAG" --arg title "Iris $VERSION" --arg body "$CHANGELOG" \
                 '{tag_name:$tag, title:$title, body:$body}')" \
            | jq -r .id)
          echo "Created release $TAG (id $RELEASE_ID)"

          for f in "$GITHUB_WORKSPACE"/iris-android-v* "$GITHUB_WORKSPACE"/iris-desktop-*; do
            [ -f "$f" ] || continue
            echo "Uploading $(basename "$f")"
            curl -sf -X POST -H "$AUTH" -F "attachment=@$f" \
              "$API/releases/$RELEASE_ID/attachments" > /dev/null
          done
          echo "Done: $SERVER/$REPO/releases/tag/$TAG"

---

## 4. EDITS to existing Gradle files

### 4a. `app/androidApp/build.gradle.kts`

**Change 1** — in `defaultConfig`, replace:

```kotlin
        versionName = "0.1.0"

with:

        // CI passes -PappVersion=<version> (release workflow); local builds
        // keep the default.
        versionName = (project.findProperty("appVersion") as? String) ?: "0.1.0"

Change 2 — inside the android { } block, right after the buildTypes { } block, add:

    // CI release signing: .gitea/workflows/release.yml restores a keystore
    // from Gitea secrets and exports ANDROID_KEYSTORE_* env vars (see
    // scripts/make_release_keystore.sh). Without them the release build stays
    // unsigned and the workflow falls back to a debug APK.
    val ciKeystore = System.getenv("ANDROID_KEYSTORE_FILE")
    if (ciKeystore != null) {
        signingConfigs {
            create("release") {
                storeFile = file(ciKeystore)
                storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD")
                keyAlias = System.getenv("ANDROID_KEY_ALIAS")
                keyPassword = System.getenv("ANDROID_KEY_PASSWORD")
            }
        }
        buildTypes {
            release {
                signingConfig = signingConfigs.getByName("release")
            }
        }
    }

4b. app/desktopApp/build.gradle.kts

Change 1 — near the top (after val arch = ...), add:

// CI passes -PappVersion=<version> (release workflow); local builds keep the
// default. jpackage requires a plain semver (no leading "v").
val appVersion = (project.findProperty("appVersion") as? String) ?: "0.1.0"

Change 2 — in the jpackage Exec task's commandLine(...), replace:

        "--app-version", "0.1.0",

with:

        "--app-version", appVersion,

5. NEW FILE: scripts/make_release_keystore.sh (Android signing, one-time)

This is the noob-friendly signing setup. Run it once on your machine:

scripts/make_release_keystore.sh

It generates a keystore with random passwords and prints the four values to paste into Gitea. Then create the secrets in Gitea → repo → Settings → Actions → Secrets:

Secret Value
ANDROID_KEYSTORE_BASE64 the long base64 blob the script prints
ANDROID_KEYSTORE_PASSWORD printed by the script
ANDROID_KEY_ALIAS iris
ANDROID_KEY_PASSWORD printed by the script

Until the secrets exist, the release workflow still works but ships a debug APK (installable, but not suitable for updates).

#!/usr/bin/env bash
# One-time setup: create the Android release keystore and print the values to
# paste into Gitea (repo -> Settings -> Actions -> Secrets).
#
# Usage:  scripts/make_release_keystore.sh [output-file]
#         (default: ~/iris-release.keystore)
#
# WARNING: back up the keystore file immediately. If it is lost, the app can
# never be updated on users' phones (a new key = a brand-new app as far as
# Android is concerned).
set -euo pipefail

OUT="${1:-$HOME/iris-release.keystore}"
if [ -e "$OUT" ]; then
  echo "Refusing to overwrite existing file: $OUT" >&2
  exit 1
fi

STORE_PASS="$(openssl rand -base64 18 | tr -d '/+=')"
KEY_PASS="$(openssl rand -base64 18 | tr -d '/+=')"

keytool -genkeypair -v \
  -keystore "$OUT" -storetype PKCS12 \
  -alias iris -keyalg RSA -keysize 2048 -validity 10000 \
  -storepass "$STORE_PASS" -keypass "$KEY_PASS" \
  -dname "CN=Iris Release, OU=Mobile, O=Iris, C=DE"

echo
echo "Keystore written to: $OUT"
echo ">>> Back it up NOW (password manager / cloud storage). <<<"
echo
echo "Add these four secrets in Gitea (repo -> Settings -> Actions -> Secrets):"
echo
echo "  ANDROID_KEYSTORE_BASE64   = $(base64 -w0 "$OUT")"
echo
echo "  ANDROID_KEYSTORE_PASSWORD = $STORE_PASS"
echo "  ANDROID_KEY_ALIAS         = iris"
echo "  ANDROID_KEY_PASSWORD      = $KEY_PASS"

Don't forget: chmod +x scripts/make_release_keystore.sh


6. Prerequisites / checks before the first run

  1. act_runner must be registered on gitea.zephyre.one with the label ubuntu-latest (that's what both workflows request). Check under Gitea → (your user or the org) → Actions → Runners.
  2. The runner needs internet access (GitHub, Google, Maven Central, Gradle).
  3. No Gitea secrets are required — but add the four keystore secrets from §5 for a signed APK, and optionally a GITEA_TOKEN (Releases: write) if the automatic GITHUB_TOKEN doesn't have release permission.

7. Using it

  • CI: pushes to master / PRs run automatically.
  • Release: repo → Actions → Release → Run workflow → enter version (e.g. 0.2.0) + changelog → start. The release appears at https://gitea.zephyre.one/ARIA/iris_x_hermes/releases/tag/v0.2.0 with:
    • iris-android-v0.2.0.apk (signed, or -debug without keystore secrets)
    • iris-desktop-linux-x64-v0.2.0.zip (app image, JRE bundled)
    • iris-desktop-linux-x64-v0.2.0.deb
  • Re-running with the same version replaces the old release (old tag + attachments are deleted first).

8. Later: Windows / macOS desktop builds

When the Windows VM / MacBook runner exists:

  1. Register act_runner on that machine (labels e.g. windows-latest, macos-latest).
  2. In release.yml, copy the desktop job, change runs-on, and adjust the artifact names (iris-desktop-windows-x64-…, iris-desktop-macos-…). jpackage then produces msi/dmg natively — no other changes needed.
  3. Bump the pinned hermes-agent SHA in both workflows whenever you update the local hermes-agent/ checkout (git -C hermes-agent rev-parse HEAD).