diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9aebe2b..238240b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,12 +110,48 @@ jobs: path: dist/installer/*.msi if-no-files-found: error + # ────────────────────────────────────────────────────────────────── + # Linux — produces AgentMark--linux-x86_64.AppImage + # ────────────────────────────────────────────────────────────────── + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node (build-time) + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' + + - name: Install FUSE (needed for appimagetool on Ubuntu 22+) + run: | + sudo apt-get update + sudo apt-get install -y libfuse2 + + - name: Build .AppImage + run: | + chmod +x packaging/scripts/*.sh + packaging/scripts/build-linux.sh + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: agentmark-linux + path: dist/installer/*.AppImage + if-no-files-found: error + # ────────────────────────────────────────────────────────────────── # Attach artifacts to the GitHub Release (only on tag pushes). # ────────────────────────────────────────────────────────────────── publish: if: startsWith(github.ref, 'refs/tags/v') - needs: [build-macos, build-windows] + needs: [build-macos, build-windows, build-linux] runs-on: ubuntu-latest permissions: contents: write @@ -130,5 +166,6 @@ jobs: files: | artifacts/agentmark-macos/*.pkg artifacts/agentmark-windows/*.msi + artifacts/agentmark-linux/*.AppImage draft: true fail_on_unmatched_files: true diff --git a/packaging/README.md b/packaging/README.md index 41424a2..943faa9 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -12,6 +12,7 @@ Each installer drops these files onto the target machine: |---|---|---| | macOS | `/opt/thinkfleet/agentmark/` | `node` (arm64 + x64 universal), `agentmark/` (npm package), `bridges/agentmark-bridge-macos` (Swift AXAPI binary), `bin/agentmark-mcp` launcher script | | Windows | `C:\Program Files\ThinkFleet\AgentMark\` | `node.exe`, `agentmark\` (npm package), `bridges\agentmark-bridge-windows.exe` (.NET UIA), `agentmark-mcp.cmd` launcher (added to PATH) | +| Linux | self-contained `.AppImage` | `node` + `agentmark/` (npm package) + `agentmark-mcp` launcher inside the AppDir. **No desktop bridge** — AT-SPI Linux bridge is future work; every other plugin runs fine. | Both installers add an `agentmark-mcp` command to the user's PATH that runs the bundled Node against the bundled agentmark package. @@ -45,9 +46,19 @@ WiX Toolset 4. # Produces: dist\installer\AgentMark--windows.msi ``` -### Linux (future) +### Linux -AppImage scaffolding lives in a follow-up PR. +Requires: Linux host (or GitHub Actions ubuntu-latest), `libfuse2` +installed (`apt install libfuse2` on Ubuntu 22+ for appimagetool). + +```sh +./packaging/scripts/build-linux.sh +# Produces: dist/installer/AgentMark--linux-x86_64.AppImage +``` + +Note: Linux ships without the desktop a11y bridge for now. An AT-SPI +bridge would slot into the same AppDir layout (`usr/lib/agentmark/ +bridges/agentmark-bridge-linux`); the launcher already detects it. ## Code signing diff --git a/packaging/scripts/build-linux.sh b/packaging/scripts/build-linux.sh new file mode 100755 index 0000000..8619b66 --- /dev/null +++ b/packaging/scripts/build-linux.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Build the Linux AppImage. +# +# Pipeline: +# 1. pnpm build → dist/ +# 2. Download pinned Node binary (linux-x64). +# 3. Assemble an AppDir. +# 4. Download appimagetool, run it. +# +# Output: dist/installer/AgentMark--linux-x86_64.AppImage +# +# Note: no a11y bridge bundled (Linux AT-SPI bridge isn't in this +# codebase yet). The agentmark MCP server runs fine — every plugin +# except `desktop` works. Add the Linux bridge later, ship it via +# the same AppDir layout when it lands. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +# shellcheck source=./common.sh +source "$SCRIPT_DIR/common.sh" + +VERSION="$(read_pkg_version "$REPO_ROOT/package.json")" +BUILD_DIR="$REPO_ROOT/dist/installer-build/linux" +OUT_DIR="$REPO_ROOT/dist/installer" +APPDIR="$BUILD_DIR/AgentMark.AppDir" +APPIMAGE_OUT="$OUT_DIR/AgentMark-${VERSION}-linux-x86_64.AppImage" + +mkdir -p "$BUILD_DIR" "$OUT_DIR" +rm -rf "$APPDIR" +mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/lib/agentmark" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps" + +# ────────────────────────────────────────────────────────────────────── +# 1. Build the npm package +# ────────────────────────────────────────────────────────────────────── +section "Build npm package" +cd "$REPO_ROOT" +pnpm install --frozen-lockfile +pnpm build + +# ────────────────────────────────────────────────────────────────────── +# 2. Download pinned Node +# ────────────────────────────────────────────────────────────────────── +section "Download Node v${NODE_VERSION} (linux-x64)" +NODE_ARCHIVE="$(download_node "linux-x64" "$BUILD_DIR/node-download")" +NODE_EXTRACT_DIR="$BUILD_DIR/node-extract" +mkdir -p "$NODE_EXTRACT_DIR" +tar -xJf "$NODE_ARCHIVE" -C "$NODE_EXTRACT_DIR" +NODE_BIN="$NODE_EXTRACT_DIR/node-v${NODE_VERSION}-linux-x64/bin/node" +[[ -x "$NODE_BIN" ]] || die "Extracted Node binary not found at $NODE_BIN" + +# ────────────────────────────────────────────────────────────────────── +# 3. Assemble AppDir +# ────────────────────────────────────────────────────────────────────── +section "Assemble AppDir" + +# Node runtime + npm package +cp "$NODE_BIN" "$APPDIR/usr/bin/node" +chmod +x "$APPDIR/usr/bin/node" + +cp -R "$REPO_ROOT/dist" "$APPDIR/usr/lib/agentmark/dist" +cp -R "$REPO_ROOT/schema" "$APPDIR/usr/lib/agentmark/schema" +cp "$REPO_ROOT/package.json" "$APPDIR/usr/lib/agentmark/package.json" + +( cd "$APPDIR/usr/lib/agentmark" && \ + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ + pnpm install --prod --ignore-scripts ) + +# Launcher + AppRun +cp "$SCRIPT_DIR/../templates/launcher.sh" "$APPDIR/usr/bin/agentmark-mcp" +chmod +x "$APPDIR/usr/bin/agentmark-mcp" + +# AppRun is the AppImage entrypoint — it runs when the user +# double-clicks (or executes) the .AppImage. Delegates to our +# launcher, which already knows how to find Node + the agentmark dist +# relative to its own location. +cp "$SCRIPT_DIR/../templates/AppRun" "$APPDIR/AppRun" +chmod +x "$APPDIR/AppRun" + +# Required by the AppImage spec: a .desktop file + an icon. We don't +# ship a GUI, but appimagetool refuses to build without these so we +# satisfy the spec with minimal valid content. +sed "s/__VERSION__/$VERSION/g" "$SCRIPT_DIR/../templates/agentmark.desktop" > "$APPDIR/agentmark.desktop" +cp "$APPDIR/agentmark.desktop" "$APPDIR/usr/share/applications/agentmark.desktop" + +# Placeholder icon. AppImage needs *something* at the AppDir root and +# under usr/share/icons. A 1x1 transparent PNG is enough. +PLACEHOLDER_PNG="$BUILD_DIR/agentmark.png" +if [[ ! -f "$PLACEHOLDER_PNG" ]]; then + # 1x1 transparent PNG, base64-encoded. + base64 --decode > "$PLACEHOLDER_PNG" </bin/agentmark-mcp, +# node at /node, agentmark at /agentmark. +INSTALL_ROOT="$(cd "$BIN_DIR/.." && pwd)" NODE="$INSTALL_ROOT/node" ENTRY="$INSTALL_ROOT/agentmark/dist/src/mcp/cli.js" +BRIDGES_DIR="$INSTALL_ROOT/bridges" + +# Fall back to the AppImage layout: launcher at /usr/bin/ +# agentmark-mcp, node at /usr/bin/node, agentmark at +# /usr/lib/agentmark. +if [[ ! -x "$NODE" || ! -f "$ENTRY" ]]; then + INSTALL_ROOT="$(cd "$BIN_DIR/../.." && pwd)" + NODE="$INSTALL_ROOT/usr/bin/node" + ENTRY="$INSTALL_ROOT/usr/lib/agentmark/dist/src/mcp/cli.js" + BRIDGES_DIR="$INSTALL_ROOT/usr/lib/agentmark/bridges" +fi + +[[ -x "$NODE" ]] || { echo "agentmark-mcp: bundled Node not found at $NODE" >&2; exit 1; } +[[ -f "$ENTRY" ]] || { echo "agentmark-mcp: agentmark entry not found at $ENTRY" >&2; exit 1; } -# Tell the agentmark Desktop plugin where the bridge lives unless the -# operator overrode the path explicitly. +# Point the desktop plugin at the bundled bridge if one exists on this +# platform. AppImage builds ship without a bridge today (no Linux +# AT-SPI bridge yet); leaving the env var unset is the right behavior +# — agentmark_desktop_open will return a clean OS-mismatch error. if [[ -z "${AGENTMARK_BRIDGE_PATH:-}" ]]; then - export AGENTMARK_BRIDGE_PATH="$INSTALL_ROOT/bridges/agentmark-bridge-macos" + if [[ -x "$BRIDGES_DIR/agentmark-bridge-macos" ]]; then + export AGENTMARK_BRIDGE_PATH="$BRIDGES_DIR/agentmark-bridge-macos" + elif [[ -x "$BRIDGES_DIR/agentmark-bridge-linux" ]]; then + export AGENTMARK_BRIDGE_PATH="$BRIDGES_DIR/agentmark-bridge-linux" + fi fi exec "$NODE" "$ENTRY" "$@"