Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/scripts/build-native-distribution.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
set -euo pipefail

# Builds a local native distribution bundle around RuntimeLauncher and the
# executable bot jar produced by Maven. The resulting archive mirrors the
# layout shipped by GitHub Releases.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TARGET_DIR="${ROOT_DIR}/target"
NATIVE_DIST_DIR="${TARGET_DIR}/native-dist"
BUILD_DIR="${NATIVE_DIST_DIR}/build"
JPACKAGE_INPUT_DIR="${BUILD_DIR}/jpackage-input"
APP_IMAGE_OUTPUT_DIR="${BUILD_DIR}/app-image"
APP_NAME="golemcore-bot"
LAUNCHER_MAIN_CLASS="me.golemcore.bot.launcher.RuntimeLauncher"

# The native bundle relies on jpackage because it produces an app-image with
# platform-specific launchers while still allowing us to ship the real runtime jar.
if ! command -v jpackage >/dev/null 2>&1; then
echo "jpackage is required to build native distributions." >&2
exit 1
fi

# The release jar is the actual Spring Boot runtime that RuntimeLauncher should
# restart into after an update.
RUNTIME_JAR_PATH="$(find "${TARGET_DIR}" -maxdepth 1 -type f -name 'bot-*-exec.jar' | sort | head -n 1)"
if [[ -z "${RUNTIME_JAR_PATH}" ]]; then
echo "Executable runtime jar not found in target/. Run ./mvnw clean package first." >&2
exit 1
fi

# The launcher classes are packaged separately so jpackage can bootstrap the
# app-image with the lightweight restart-aware entry point.
if [[ ! -d "${TARGET_DIR}/classes/me/golemcore/bot/launcher" ]]; then
echo "RuntimeLauncher classes not found in target/classes. Run ./mvnw clean package first." >&2
exit 1
fi

RUNTIME_JAR_NAME="$(basename "${RUNTIME_JAR_PATH}")"
VERSION="${RUNTIME_JAR_NAME#bot-}"
VERSION="${VERSION%-exec.jar}"
APP_VERSION="$(printf '%s' "${VERSION}" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')"
if [[ -z "${APP_VERSION}" ]]; then
APP_VERSION="0.0.0"
fi

# jpackage app-image layouts differ between Linux and macOS, so capture the
# platform-specific root and app directories once up front.
UNAME_S="$(uname -s)"
UNAME_M="$(uname -m)"
case "${UNAME_S}" in
Linux)
PLATFORM="linux"
APP_IMAGE_ROOT_NAME="${APP_NAME}"
APP_IMAGE_APP_DIR="${APP_NAME}"
;;
Darwin)
PLATFORM="macos"
APP_IMAGE_ROOT_NAME="${APP_NAME}.app"
APP_IMAGE_APP_DIR="${APP_NAME}.app/Contents/app"
;;
*)
echo "Unsupported operating system for native distribution: ${UNAME_S}" >&2
exit 1
;;
esac

case "${UNAME_M}" in
x86_64|amd64)
ARCH="x64"
;;
arm64|aarch64)
ARCH="arm64"
;;
*)
echo "Unsupported CPU architecture for native distribution: ${UNAME_M}" >&2
exit 1
;;
esac

ASSET_BASENAME="${APP_NAME}-${VERSION}-${PLATFORM}-${ARCH}"
ARCHIVE_PATH="${NATIVE_DIST_DIR}/${ASSET_BASENAME}.tar.gz"
LAUNCHER_JAR_PATH="${JPACKAGE_INPUT_DIR}/${APP_NAME}-launcher.jar"

rm -rf "${BUILD_DIR}"
rm -f "${ARCHIVE_PATH}"
mkdir -p "${JPACKAGE_INPUT_DIR}" "${APP_IMAGE_OUTPUT_DIR}" "${NATIVE_DIST_DIR}"

# Package only the launcher classes into a tiny bootstrap jar for jpackage.
jar --create \
--file "${LAUNCHER_JAR_PATH}" \
--main-class "${LAUNCHER_MAIN_CLASS}" \
-C "${TARGET_DIR}/classes" me/golemcore/bot/launcher

# Point the launcher at the bundled runtime jar inside the app-image so the
# local native build behaves like the released bundle.
jpackage \
--type app-image \
--name "${APP_NAME}" \
--dest "${APP_IMAGE_OUTPUT_DIR}" \
--input "${JPACKAGE_INPUT_DIR}" \
--main-jar "$(basename "${LAUNCHER_JAR_PATH}")" \
--main-class "${LAUNCHER_MAIN_CLASS}" \
--app-version "${APP_VERSION}" \
--vendor "GolemCore" \
--description "GolemCore Bot local launcher" \
--java-options '-Dfile.encoding=UTF-8' \
--java-options "-Dgolemcore.launcher.bundled-jar=\$APPDIR/lib/runtime/${RUNTIME_JAR_NAME}"

APP_IMAGE_DIR="${APP_IMAGE_OUTPUT_DIR}/${APP_IMAGE_APP_DIR}"
if [[ ! -d "${APP_IMAGE_DIR}" ]]; then
echo "Expected app-image directory not found: ${APP_IMAGE_DIR}" >&2
exit 1
fi

# Ship the real executable runtime jar alongside the launcher inside a dedicated
# runtime directory so restarts can jump into the jar directly.
RUNTIME_DIR="${APP_IMAGE_DIR}/lib/runtime"
mkdir -p "${RUNTIME_DIR}"
cp "${RUNTIME_JAR_PATH}" "${RUNTIME_DIR}/${RUNTIME_JAR_NAME}"

# Archive the platform-specific app-image as a release asset.
tar -czf "${ARCHIVE_PATH}" -C "${APP_IMAGE_OUTPUT_DIR}" "${APP_IMAGE_ROOT_NAME}"

echo "Created native distribution: ${ARCHIVE_PATH}"
29 changes: 23 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ jobs:
if: steps.release-flag.outputs.should-release == 'true' && steps.tag-push.outputs.should-release == 'true'
run: ./mvnw clean package -DskipTests -DskipGitHooks=true

# Build the local native bundles from the freshly produced runtime jar so
# the release carries the same launcher-aware packaging users build locally.
- name: Build native distribution bundles
if: steps.release-flag.outputs.should-release == 'true' && steps.tag-push.outputs.should-release == 'true'
run: ./.github/scripts/build-native-distribution.sh

- name: Prepare release assets
if: steps.release-flag.outputs.should-release == 'true' && steps.tag-push.outputs.should-release == 'true'
id: assets
Expand All @@ -212,9 +218,20 @@ jobs:
echo "Release jar not found (expected target/bot-*.jar)." >&2
exit 1
fi
JAR_NAME=$(basename "${JAR_PATH}")
SHA_FILE=sha256sums.txt
sha256sum "${JAR_PATH}" | awk -v jar="${JAR_NAME}" '{print $1 " " jar}' > "${SHA_FILE}"
JAR_NAME=$(basename "${JAR_PATH}")
sha256sum "${JAR_PATH}" | awk -v file="${JAR_NAME}" '{print $1 " " file}' > "${SHA_FILE}"

# Publish checksums for each native bundle as well so local installers
# can verify the exact archive they downloaded.
while IFS= read -r bundlePath; do
if [[ -z "${bundlePath}" ]]; then
continue
fi
bundleName=$(basename "${bundlePath}")
sha256sum "${bundlePath}" | awk -v file="${bundleName}" '{print $1 " " file}' >> "${SHA_FILE}"
done < <(find target/native-dist -maxdepth 1 -type f | sort)

echo "jar-path=${JAR_PATH}" >> "${GITHUB_OUTPUT}"
echo "sha-file=${SHA_FILE}" >> "${GITHUB_OUTPUT}"

Expand All @@ -223,13 +240,13 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ steps.release.outputs.version }}
RELEASE_JAR: ${{ steps.assets.outputs.jar-path }}
RELEASE_SHA: ${{ steps.assets.outputs.sha-file }}
run: |
RELEASE_FILES=$(find target/native-dist -maxdepth 1 -type f | sort)
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
gh release upload "${RELEASE_TAG}" "${RELEASE_JAR}" "${RELEASE_SHA}" --clobber
gh release upload "${RELEASE_TAG}" ${{ steps.assets.outputs.jar-path }} ${RELEASE_FILES} "${RELEASE_SHA}" --clobber
else
gh release create "${RELEASE_TAG}" "${RELEASE_JAR}" "${RELEASE_SHA}" \
gh release create "${RELEASE_TAG}" ${{ steps.assets.outputs.jar-path }} ${RELEASE_FILES} "${RELEASE_SHA}" \
--title "${RELEASE_TAG}" \
--generate-notes
fi
Expand Down Expand Up @@ -263,7 +280,7 @@ jobs:
echo "## Release ${{ steps.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Tag \`${{ steps.release.outputs.version }}\` pushed (main push intentionally disabled by branch rules)." >> $GITHUB_STEP_SUMMARY
echo "GitHub Release with \`bot-*.jar\` and \`sha256sums.txt\` was published." >> $GITHUB_STEP_SUMMARY
echo "GitHub Release with the executable JAR, native local bundles, and \`sha256sums.txt\` was published." >> $GITHUB_STEP_SUMMARY
echo "Artifacts published to Maven Central." >> $GITHUB_STEP_SUMMARY

- name: Summary (no release)
Expand Down
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,91 @@ Need a local build, Compose setup, or production deployment path? See **[Quick S

---

## Local native app-image bundle (experimental)

Besides Docker and the plain executable JAR, the release workflow now also publishes a **local app-image bundle** for the current OS/architecture.

### Build it locally

```bash
./mvnw clean package -DskipTests -DskipGitHooks=true
npx golemcore-bot-local-build-native-dist
```

This produces an archive in:

```text
target/native-dist/golemcore-bot-<version>-<platform>-<arch>.tar.gz
```

### What is inside

The app-image contains:

- a small launcher application produced by `jpackage`
- the regular self-updatable runtime jar under `lib/runtime/`
- a picocli-powered native launcher entrypoint with built-in help and launcher-only options
- launcher wiring that points to that bundled runtime jar first

So the startup order becomes:

1. staged update from `updates/current.txt`
2. bundled runtime jar from the app-image
3. legacy Jib/classpath fallback

### Native launcher options

The native launcher uses picocli, so it has first-class help and a small set of launcher-specific flags.

Show help:

```bash
./golemcore-bot/bin/golemcore-bot --help
```

Common options:

- `--server-port=<port>` — forwards `-Dserver.port=<port>` to the spawned runtime
- `--storage-path=<path>` — forwards `-Dbot.storage.local.base-path=<path>`
- `--updates-path=<path>` — forwards `-Dbot.update.updates-path=<path>`
- `--bundled-jar=<path>` — overrides the bundled runtime jar path
- `-J=<jvm-option>` / `--java-option=<jvm-option>` — forwards extra JVM options to the spawned runtime

Examples:

```bash
./golemcore-bot/bin/golemcore-bot --server-port=9090
./golemcore-bot/bin/golemcore-bot -J=-Xmx1g --server-port=9090
./golemcore-bot/bin/golemcore-bot --storage-path=/srv/golemcore/workspace --updates-path=/srv/golemcore/updates
```

### Spring runtime arguments still work

Unknown arguments are forwarded to Spring Boot unchanged, so existing application arguments continue to work:

```bash
./golemcore-bot/bin/golemcore-bot --spring.profiles.active=prod
./golemcore-bot/bin/golemcore-bot --server.port=9090
./golemcore-bot/bin/golemcore-bot -Dspring.profiles.active=prod
```

If you want to make the split explicit, you can also use `--` before Spring arguments:

```bash
./golemcore-bot/bin/golemcore-bot --server-port=9090 -- --spring.profiles.active=prod
```

### Why this matters

This keeps the existing self-update model based on:

- `updates/current.txt`
- `updates/jars/`

while also letting the bot start cleanly from a native local bundle with documented launcher parameters.

---

## First-run setup

1. Open the dashboard.
Expand Down
29 changes: 29 additions & 0 deletions bin/golemcore-bot-local-build-native-dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail

# Convenience wrapper that finds a golemcore-bot checkout from the current
# directory and delegates to the canonical native distribution build script.
find_project_root() {
local current_dir="${PWD}"
while [[ "${current_dir}" != "/" ]]; do
if [[ -f "${current_dir}/pom.xml" && -f "${current_dir}/.github/scripts/build-native-distribution.sh" ]]; then
printf '%s\n' "${current_dir}"
return 0
fi
current_dir="$(dirname "${current_dir}")"
done
return 1
}

PROJECT_ROOT="${GOLEMCORE_PROJECT_DIR:-}"
if [[ -z "${PROJECT_ROOT}" ]]; then
PROJECT_ROOT="$(find_project_root || true)"
fi

if [[ -z "${PROJECT_ROOT}" ]]; then
echo "Unable to locate a golemcore-bot checkout from the current directory." >&2
echo "Run this command inside the repository or set GOLEMCORE_PROJECT_DIR explicitly." >&2
exit 1
fi

exec "${PROJECT_ROOT}/.github/scripts/build-native-distribution.sh" "$@"
37 changes: 36 additions & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ Some settings are still controlled via Spring properties (application config), t
- Plugin marketplace HTTP fallback: `BOT_PLUGINS_MARKETPLACE_API_BASE_URL`, `BOT_PLUGINS_MARKETPLACE_RAW_BASE_URL`, `BOT_PLUGINS_MARKETPLACE_REMOTE_CACHE_TTL`
- SelfEvolving bootstrap overrides: `bot.self-evolving.bootstrap.*`
- Self-update controls: `BOT_UPDATE_ENABLED`, `UPDATE_PATH`, `BOT_UPDATE_MAX_KEPT_VERSIONS`, `BOT_UPDATE_CHECK_INTERVAL`
- Local bundle runtime override: `GOLEMCORE_BUNDLED_JAR`, `golemcore.launcher.bundled-jar`
- Allowed providers in model picker: `BOT_MODEL_SELECTION_ALLOWED_PROVIDERS`
- Tool result truncation: `bot.auto-compact.max-tool-result-chars`
- Plan mode feature flag: `bot.plan.enabled`
Expand Down Expand Up @@ -818,11 +819,45 @@ Configurable properties:
- `bot.update.max-kept-versions` (`BOT_UPDATE_MAX_KEPT_VERSIONS`, default `3`)
- `bot.update.check-interval` (`BOT_UPDATE_CHECK_INTERVAL`, default `PT1H`)

### Local native bundle runtime resolution

When the app starts through the native app-image launcher, `RuntimeLauncher` can also use:

- `GOLEMCORE_BUNDLED_JAR`
- `golemcore.launcher.bundled-jar`

These point to the bundled runtime jar inside the local app-image.

The native launcher itself is picocli-based and documents its launcher-only options via `--help`.

Launcher-specific options:

- `--server-port=<port>` forwards `-Dserver.port=<port>` to the spawned runtime
- `--storage-path=<path>` forwards `-Dbot.storage.local.base-path=<path>`
- `--updates-path=<path>` forwards `-Dbot.update.updates-path=<path>`
- `--bundled-jar=<path>` overrides the bundled runtime jar path
- `-J=<jvm-option>` / `--java-option=<jvm-option>` forwards extra JVM options

Unknown arguments are passed through to Spring Boot, so both of these remain valid:

```bash
golemcore-bot --server-port=9090
golemcore-bot --spring.profiles.active=prod
```

The launcher priority is:

1. staged update selected by `updates/current.txt`
2. bundled runtime jar
3. legacy Jib classpath fallback

This preserves the existing self-update model for local distributions while adding a documented native launcher CLI.

## Storage Layout

Default (macOS/Linux): `~/.golemcore/workspace`

```
```text
workspace/
├── auto/ # auto mode + plan mode state
├── memory/ # structured memory items (JSONL)
Expand Down
Loading
Loading