Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
486f02e
feat: add native app-image launcher distribution
Mar 15, 2026
47ad447
Merge remote-tracking branch 'origin/main' into pr-172
golemcore1 Apr 12, 2026
2a076dc
fix(launcher): satisfy code quality after merge
golemcore1 Apr 12, 2026
9bdb88f
fix(launcher): resolve sonar review issues
golemcore1 Apr 13, 2026
775ad48
Merge branch 'main' into feat/native-app-image-launcher
alexk-dev Apr 14, 2026
725d80a
Merge remote-tracking branch 'origin/main' into feat/native-app-image…
Apr 16, 2026
9e1a312
docs(launcher): add runtime launcher documentation
Apr 16, 2026
c502d76
feat(launcher): add picocli native launcher args
Apr 16, 2026
5f43e4e
style(launcher): apply formatter to picocli launcher
Apr 17, 2026
c0f8f56
fix(launcher): expose parsed launcher args for tests
Apr 17, 2026
50ac405
fix(launcher): align tests with picocli passthrough
Apr 17, 2026
e0bb61d
fix(launcher): remove redundant version fallback
Apr 17, 2026
eee8dd0
Merge remote-tracking branch 'origin/main' into codex/pr-172-merge-ma…
alexk-dev Apr 18, 2026
7510215
test: allow native launcher size debt
alexk-dev Apr 18, 2026
a1bad0e
refactor: split runtime launcher responsibilities
alexk-dev Apr 18, 2026
c0d6572
test: cover launcher adapters
alexk-dev Apr 18, 2026
fd28407
ci: upload native linux distribution
alexk-dev Apr 18, 2026
edde2c2
fix: include launcher cli dependency in native bundle
alexk-dev Apr 18, 2026
755c9a4
fix: make native launcher bundle self-contained
alexk-dev Apr 18, 2026
65e52d0
fix: use bundled java in native launcher
alexk-dev Apr 18, 2026
bb00906
feat: require web launcher command
alexk-dev Apr 18, 2026
62d5af2
fix: satisfy launcher pmd rule
alexk-dev Apr 18, 2026
2b67437
fix: default native launcher to prod profile
alexk-dev Apr 19, 2026
caa2150
fix: ignore plugin engine upper bounds
alexk-dev Apr 19, 2026
97c4972
fix: satisfy spotbugs for plugin constraints
alexk-dev Apr 19, 2026
21b6345
Merge remote-tracking branch 'origin/main' into codex/pr-172-merge-ma…
alexk-dev Apr 23, 2026
2f3dbe7
fix: split legacy and cli runtime launchers
alexk-dev Apr 23, 2026
d0ecd85
fix: smoke docker image on public endpoint
alexk-dev Apr 23, 2026
f79706c
test: verify launcher entrypoints in ci
alexk-dev Apr 23, 2026
4d7225e
fix: compare bundled runtime jar version
alexk-dev Apr 23, 2026
0220881
test: expand runtime launch smoke matrix
alexk-dev Apr 23, 2026
a7096c7
fix: accept bare jvm system properties
alexk-dev Apr 23, 2026
49d9fc1
fix: publish executable runtime jars explicitly
alexk-dev Apr 23, 2026
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
151 changes: 151 additions & 0 deletions .github/scripts/build-native-distribution.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
set -euo pipefail

# Builds a local native distribution bundle around RuntimeCliLauncher 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.RuntimeCliLauncher"

# 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 RuntimeCliLauncher 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 "Launcher classes not found in target/classes. Run ./mvnw clean package first." >&2
exit 1
fi
if [[ ! -d "${TARGET_DIR}/classes/me/golemcore/bot/runtime" ]]; then
echo "Runtime support 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}"
BUNDLED_RUNTIME_JAR_OPTION="\$APPDIR/../runtime/${RUNTIME_JAR_NAME}"
BUNDLED_JAVA_COMMAND_OPTION="\$APPDIR/../runtime/bin/java"
APP_IMAGE_JAVA_COMMAND_PATH="${APP_IMAGE_OUTPUT_DIR}/${APP_IMAGE_ROOT_NAME}/lib/runtime/bin/java"
;;
Darwin)
PLATFORM="macos"
APP_IMAGE_ROOT_NAME="${APP_NAME}.app"
APP_IMAGE_APP_DIR="${APP_NAME}.app/Contents/app"
BUNDLED_RUNTIME_JAR_OPTION="\$APPDIR/lib/runtime/${RUNTIME_JAR_NAME}"
BUNDLED_JAVA_COMMAND_OPTION="\$APPDIR/../runtime/Contents/Home/bin/java"
APP_IMAGE_JAVA_COMMAND_PATH="${APP_IMAGE_OUTPUT_DIR}/${APP_IMAGE_ROOT_NAME}/Contents/runtime/Contents/Home/bin/java"
;;
*)
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"
PICOCLI_VERSION="$(./mvnw -q -DforceStdout help:evaluate -Dexpression=picocli.version)"
PICOCLI_JAR_PATH="${HOME}/.m2/repository/info/picocli/picocli/${PICOCLI_VERSION}/picocli-${PICOCLI_VERSION}.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 and runtime-version support 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 \
-C "${TARGET_DIR}/classes" me/golemcore/bot/runtime

if [[ ! -f "${PICOCLI_JAR_PATH}" ]]; then
echo "picocli jar not found: ${PICOCLI_JAR_PATH}" >&2
exit 1
fi
cp "${PICOCLI_JAR_PATH}" "${JPACKAGE_INPUT_DIR}/$(basename "${PICOCLI_JAR_PATH}")"

# 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" \
--jlink-options "--strip-debug --no-man-pages --no-header-files" \
--java-options '-Dfile.encoding=UTF-8' \
--java-options '-Dspring.profiles.active=prod' \
--java-options "-Dgolemcore.launcher.bundled-jar=${BUNDLED_RUNTIME_JAR_OPTION}" \
--java-options "-Dgolemcore.launcher.java-command=${BUNDLED_JAVA_COMMAND_OPTION}"

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
if [[ ! -x "${APP_IMAGE_JAVA_COMMAND_PATH}" ]]; then
echo "Expected bundled Java command not found: ${APP_IMAGE_JAVA_COMMAND_PATH}" >&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}"
59 changes: 59 additions & 0 deletions .github/scripts/verify-launcher-entrypoints.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TARGET_DIR="${ROOT_DIR}/target"
EXEC_JAR_PATH="$(find "${TARGET_DIR}" -maxdepth 1 -type f -name 'bot-*-exec.jar' | sort | head -n 1)"
NATIVE_LAUNCHER_JAR_PATH="${TARGET_DIR}/native-dist/build/jpackage-input/golemcore-bot-launcher.jar"

expect_manifest_value() {
local jar_path="$1"
local key="$2"
local expected="$3"
local actual

if ! actual="$(unzip -p "${jar_path}" META-INF/MANIFEST.MF \
| tr -d '\r' \
| awk -F': ' -v key="${key}" '$1 == key { print $2; found = 1; exit } END { if (!found) exit 1 }')"; then
echo "Manifest key ${key} not found in ${jar_path}." >&2
exit 1
fi

if [[ "${actual}" != "${expected}" ]]; then
echo "Unexpected ${key} in ${jar_path}: expected '${expected}', got '${actual}'." >&2
exit 1
fi
}

expect_jar_entry() {
local jar_path="$1"
local entry="$2"

if ! jar tf "${jar_path}" | grep -Fxq "${entry}"; then
echo "Expected jar entry not found in ${jar_path}: ${entry}" >&2
exit 1
fi
}

if [[ -z "${EXEC_JAR_PATH}" ]]; then
echo "Executable runtime jar not found in ${TARGET_DIR}." >&2
exit 1
fi

expect_manifest_value "${EXEC_JAR_PATH}" "Main-Class" "org.springframework.boot.loader.launch.JarLauncher"
expect_manifest_value "${EXEC_JAR_PATH}" "Start-Class" "me.golemcore.bot.BotApplication"
expect_jar_entry "${EXEC_JAR_PATH}" "BOOT-INF/classes/me/golemcore/bot/launcher/RuntimeLauncher.class"
expect_jar_entry "${EXEC_JAR_PATH}" "BOOT-INF/classes/me/golemcore/bot/launcher/RuntimeCliLauncher.class"
expect_jar_entry "${EXEC_JAR_PATH}" "BOOT-INF/classes/me/golemcore/bot/launcher/RuntimeJarVersionReader.class"

if [[ ! -f "${NATIVE_LAUNCHER_JAR_PATH}" ]]; then
echo "Native launcher jar not found: ${NATIVE_LAUNCHER_JAR_PATH}" >&2
exit 1
fi

expect_manifest_value "${NATIVE_LAUNCHER_JAR_PATH}" "Main-Class" "me.golemcore.bot.launcher.RuntimeCliLauncher"
expect_jar_entry "${NATIVE_LAUNCHER_JAR_PATH}" "me/golemcore/bot/launcher/RuntimeLauncher.class"
expect_jar_entry "${NATIVE_LAUNCHER_JAR_PATH}" "me/golemcore/bot/launcher/RuntimeCliLauncher.class"
expect_jar_entry "${NATIVE_LAUNCHER_JAR_PATH}" "me/golemcore/bot/launcher/RuntimeJarVersionReader.class"

echo "Launcher entrypoint packaging contract verified."
Loading
Loading