Skip to content
Merged
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
39 changes: 38 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,48 @@ jobs:
path: dist/installer/*.msi
if-no-files-found: error

# ──────────────────────────────────────────────────────────────────
# Linux — produces AgentMark-<version>-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
Expand All @@ -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
15 changes: 13 additions & 2 deletions packaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -45,9 +46,19 @@ WiX Toolset 4.
# Produces: dist\installer\AgentMark-<version>-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-<version>-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

Expand Down
120 changes: 120 additions & 0 deletions packaging/scripts/build-linux.sh
Original file line number Diff line number Diff line change
@@ -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-<version>-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" <<EOF
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNgAAIAAAUAAen63NgAAAAA
SUVORK5CYII=
EOF
fi
cp "$PLACEHOLDER_PNG" "$APPDIR/agentmark.png"
cp "$PLACEHOLDER_PNG" "$APPDIR/usr/share/icons/hicolor/256x256/apps/agentmark.png"

# ──────────────────────────────────────────────────────────────────────
# 4. Build the AppImage
# ──────────────────────────────────────────────────────────────────────
section "Build .AppImage"
APPIMAGETOOL="$BUILD_DIR/appimagetool"
if [[ ! -x "$APPIMAGETOOL" ]]; then
echo "Downloading appimagetool…"
curl --fail --location --silent --show-error \
--output "$APPIMAGETOOL" \
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x "$APPIMAGETOOL"
fi

# `--no-appstream` skips the appstreamcli check (we're a CLI, no
# AppStream metadata). `ARCH=x86_64` ensures the output is named
# consistently regardless of the host arch.
ARCH=x86_64 "$APPIMAGETOOL" --no-appstream "$APPDIR" "$APPIMAGE_OUT"

section "Done"
echo "Installer: $APPIMAGE_OUT"
ls -lh "$APPIMAGE_OUT"
11 changes: 11 additions & 0 deletions packaging/templates/AppRun
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# AppImage entrypoint.
#
# Mounted at /tmp/.mount_xxx/ when the .AppImage runs. We hand off to
# the bundled launcher which knows how to resolve Node + agentmark dist
# relative to itself.

set -e

HERE="$(dirname "$(readlink -f "${0}")")"
exec "$HERE/usr/bin/agentmark-mcp" "$@"
10 changes: 10 additions & 0 deletions packaging/templates/agentmark.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=ThinkFleet AgentMark
GenericName=MCP Server
Comment=Model Context Protocol server for AgentMark
Exec=agentmark-mcp
Icon=agentmark
Categories=Development;
Terminal=true
Version=__VERSION__
53 changes: 44 additions & 9 deletions packaging/templates/launcher.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
#!/usr/bin/env bash
# agentmark-mcp launcher (POSIX).
#
# Installed at /opt/thinkfleet/agentmark/bin/agentmark-mcp; symlinked
# into /usr/local/bin by the .pkg post-install step. Resolves the
# bundle's Node + agentmark dist relative to its own path so the
# launcher is relocatable.
# Used in two install layouts:
#
# macOS .pkg (installed at /opt/thinkfleet/agentmark/):
# bin/agentmark-mcp ← this script
# node
# agentmark/dist/src/mcp/cli.js
# bridges/agentmark-bridge-macos
#
# Linux AppImage (mounted at /tmp/.mount_xxx/):
# usr/bin/agentmark-mcp ← this script
# usr/bin/node
# usr/lib/agentmark/dist/src/mcp/cli.js
# (no bridge — AT-SPI bridge not yet shipped)
#
# Resolves Node + the agentmark dist relative to its own path by
# probing both layouts so a single launcher script works for both.

set -e

# Resolve the directory this script lives in, following symlinks.
# Resolve the directory this script lives in, following symlinks (the
# macOS .pkg symlinks this from /usr/local/bin).
SCRIPT_PATH="$0"
while [[ -L "$SCRIPT_PATH" ]]; do
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
[[ "$SCRIPT_PATH" != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
done
BIN_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
INSTALL_ROOT="$(cd "$BIN_DIR/.." && pwd)"

# Try the macOS pkg layout first: launcher at <root>/bin/agentmark-mcp,
# node at <root>/node, agentmark at <root>/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 <root>/usr/bin/
# agentmark-mcp, node at <root>/usr/bin/node, agentmark at
# <root>/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" "$@"
Loading