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
134 changes: 134 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Release

# Triggers on version tags (e.g. v0.12.0). Builds installers on every
# target platform in parallel and attaches them to the GitHub Release.
# Signing is conditional on the matching secret being present — missing
# secrets skip signing rather than failing the build, so this workflow
# works for internal-test releases before code-signing certs are in place.

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag name (e.g. v0.12.0) — used to name artifacts when running manually'
required: false

jobs:
# ──────────────────────────────────────────────────────────────────
# macOS — produces AgentMark-<version>-macos.pkg
# ──────────────────────────────────────────────────────────────────
build-macos:
runs-on: macos-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: Build .pkg
env:
# Optional signing — workflow runs without these too.
APPLE_DEVELOPER_ID: ${{ secrets.APPLE_DEVELOPER_ID }}
APPLE_APP_NOTARIZATION_USER: ${{ secrets.APPLE_APP_NOTARIZATION_USER }}
APPLE_APP_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_APP_NOTARIZATION_TEAM_ID }}
APPLE_APP_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_APP_NOTARIZATION_PASSWORD }}
run: |
# Import code-signing cert into a temporary keychain if provided.
# The .p12 is base64-encoded in the secret.
if [[ -n "${{ secrets.APPLE_CERT_P12_BASE64 }}" ]]; then
KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
KEYCHAIN_PASSWORD=$(uuidgen)
CERT_PATH=$RUNNER_TEMP/codesign.p12
echo -n "${{ secrets.APPLE_CERT_P12_BASE64 }}" | base64 --decode > "$CERT_PATH"

security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "${{ secrets.APPLE_CERT_P12_PASSWORD }}" \
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychain -d user | xargs)
rm "$CERT_PATH"
fi

chmod +x packaging/scripts/*.sh
packaging/scripts/build-macos.sh

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agentmark-macos
path: dist/installer/*.pkg
if-no-files-found: error

# ──────────────────────────────────────────────────────────────────
# Windows — produces AgentMark-<version>-windows.msi
# ──────────────────────────────────────────────────────────────────
build-windows:
runs-on: windows-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: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Build .msi
env:
WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
WINDOWS_CERT_PFX_PASSWORD: ${{ secrets.WINDOWS_CERT_PFX_PASSWORD }}
shell: pwsh
run: |
.\packaging\scripts\build-windows.ps1

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agentmark-windows
path: dist/installer/*.msi
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]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts

- name: Publish to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/agentmark-macos/*.pkg
artifacts/agentmark-windows/*.msi
draft: true
fail_on_unmatched_files: true
96 changes: 96 additions & 0 deletions packaging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# AgentMark Installers

This directory builds **production installers** for the agentmark MCP
server + bridges, bundled with a pinned Node runtime so end users
don't need to install Node themselves.

## What ships

Each installer drops these files onto the target machine:

| Platform | Install path | Contents |
|---|---|---|
| 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) |

Both installers add an `agentmark-mcp` command to the user's PATH that
runs the bundled Node against the bundled agentmark package.

## What's NOT bundled

- **Playwright Chromium.** Adds ~150MB and most users won't need the
browser-driving plugin. The installer wires up a post-install step
that runs `playwright install chromium` only if the user opted in.
- **Activepieces backend.** That's a separate service the user
configures via env vars when they want the cloud-backed Memory.

## Building

### macOS

Requires: macOS host, Xcode CLI tools, Swift 5.9+.

```sh
./packaging/scripts/build-macos.sh
# Produces: dist/installer/AgentMark-<version>-macos.pkg
```

### Windows

Requires: Windows host (or GitHub Actions windows-latest), .NET 8 SDK,
WiX Toolset 4.

```ps1
.\packaging\scripts\build-windows.ps1
# Produces: dist\installer\AgentMark-<version>-windows.msi
```

### Linux (future)

AppImage scaffolding lives in a follow-up PR.

## Code signing

Signing is **optional** in the build scripts — they detect required
secrets and run signing steps only when present. The full release
flow is meant to run in GitHub Actions, which injects certs from
encrypted secrets.

**macOS:** requires an Apple Developer ID Application certificate
imported into the runner's keychain. Set these GitHub secrets:

- `APPLE_DEVELOPER_ID` — Common Name of the cert (e.g.
`Developer ID Application: ThinkFleet AI, Inc. (TEAMID)`)
- `APPLE_APP_NOTARIZATION_USER` — your App Store Connect Apple ID
- `APPLE_APP_NOTARIZATION_TEAM_ID` — 10-char team ID
- `APPLE_APP_NOTARIZATION_PASSWORD` — app-specific password
- `APPLE_CERT_P12_BASE64` — base64 of the .p12 file (export from
Keychain Access)
- `APPLE_CERT_P12_PASSWORD` — password protecting the .p12

**Windows:** requires an Authenticode code-signing certificate.

- `WINDOWS_CERT_PFX_BASE64` — base64 of the .pfx
- `WINDOWS_CERT_PFX_PASSWORD` — password protecting the .pfx

Without these secrets, the build scripts skip signing and produce
unsigned artifacts (suitable for internal testing; macOS Gatekeeper +
Windows SmartScreen will warn end users).

## Release flow

Tag a release (e.g. `v0.12.0`). GitHub Actions builds all platforms
in parallel and uploads artifacts to the GitHub Release. See
`.github/workflows/release.yml`.

## Pinned versions

| Component | Version | Where |
|---|---|---|
| Node | 22.11.0 (LTS) | `packaging/scripts/common.sh:NODE_VERSION` |
| .NET | 8.0 | bridge `csproj` |
| Swift | 5.9+ | bridge `Package.swift` |
| WiX | 4.x | installed via `dotnet tool install --global wix` in the workflow |

Bumping Node: update `NODE_VERSION` in `common.sh`, then rebuild on
each platform.
157 changes: 157 additions & 0 deletions packaging/scripts/build-macos.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
# Build the macOS .pkg installer.
#
# Pipeline:
# 1. Build the npm package (pnpm build).
# 2. Build the Swift AXAPI bridge in release mode (universal arm64+x64).
# 3. Download the pinned Node binary (arm64 + x64 if needed).
# 4. Assemble the install staging directory.
# 5. Run pkgbuild + productbuild to produce the .pkg.
# 6. (Optional) Sign + notarise if Apple secrets are present.
#
# Output: dist/installer/AgentMark-<version>-macos.pkg
#
# Designed to run BOTH on a developer's Mac and in GitHub Actions
# macos-latest. Signing is conditional on env vars; missing certs are
# logged but don't fail the build.

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/macos"
OUT_DIR="$REPO_ROOT/dist/installer"
STAGE_DIR="$BUILD_DIR/stage/opt/thinkfleet/agentmark"
PKG_OUT="$OUT_DIR/AgentMark-${VERSION}-macos.pkg"

mkdir -p "$BUILD_DIR" "$OUT_DIR"
rm -rf "$BUILD_DIR/stage"
mkdir -p "$STAGE_DIR"

# ──────────────────────────────────────────────────────────────────────
# 1. Build the npm package
# ──────────────────────────────────────────────────────────────────────
section "Build npm package"
cd "$REPO_ROOT"
pnpm install --frozen-lockfile
pnpm build

# ──────────────────────────────────────────────────────────────────────
# 2. Build the Swift bridge (release, universal)
# ──────────────────────────────────────────────────────────────────────
section "Build macOS AXAPI bridge"
cd "$REPO_ROOT/apps/agent-runner/bridges/macos"
swift build -c release --arch arm64 --arch x86_64
BRIDGE_BIN="$REPO_ROOT/apps/agent-runner/bridges/macos/.build/apple/Products/Release/agentmark-bridge-macos"
[[ -f "$BRIDGE_BIN" ]] || die "Swift build did not produce expected binary at $BRIDGE_BIN"

# ──────────────────────────────────────────────────────────────────────
# 3. Download pinned Node
# ──────────────────────────────────────────────────────────────────────
section "Download Node v${NODE_VERSION}"
HOST_ARCH="$(uname -m)"
NODE_TARGET="darwin-arm64"
if [[ "$HOST_ARCH" != "arm64" ]]; then
NODE_TARGET="darwin-x64"
fi
NODE_ARCHIVE="$(download_node "$NODE_TARGET" "$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}-${NODE_TARGET}/bin/node"
[[ -x "$NODE_BIN" ]] || die "Extracted Node binary not found at $NODE_BIN"

# ──────────────────────────────────────────────────────────────────────
# 4. Assemble staging tree
# ──────────────────────────────────────────────────────────────────────
section "Assemble installer staging"
mkdir -p "$STAGE_DIR/bin" "$STAGE_DIR/bridges"

# Node runtime
cp "$NODE_BIN" "$STAGE_DIR/node"
chmod +x "$STAGE_DIR/node"

# npm package: copy dist/ + schema/ + package.json + production deps
mkdir -p "$STAGE_DIR/agentmark"
cp -R "$REPO_ROOT/dist" "$STAGE_DIR/agentmark/dist"
cp -R "$REPO_ROOT/schema" "$STAGE_DIR/agentmark/schema"
cp "$REPO_ROOT/package.json" "$STAGE_DIR/agentmark/package.json"

# Install production deps into the staging dir. --prod skips devDeps
# (vitest, eslint, etc.). Playwright Chromium is *not* downloaded here;
# the agent-side `agentmark-mcp install-browsers` step handles that
# when the web plugin is used.
( cd "$STAGE_DIR/agentmark" && \
PNPM_DEPLOY_NO_FROZEN_LOCKFILE=true \
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
pnpm install --prod --ignore-scripts )

# Bridge
cp "$BRIDGE_BIN" "$STAGE_DIR/bridges/agentmark-bridge-macos"
chmod +x "$STAGE_DIR/bridges/agentmark-bridge-macos"

# Launcher script
cp "$SCRIPT_DIR/../templates/launcher.sh" "$STAGE_DIR/bin/agentmark-mcp"
chmod +x "$STAGE_DIR/bin/agentmark-mcp"

# ──────────────────────────────────────────────────────────────────────
# 5. Build the .pkg
# ──────────────────────────────────────────────────────────────────────
section "Build .pkg"
COMPONENT_PKG="$BUILD_DIR/agentmark-component.pkg"

# pkgbuild creates a single-component package. We pair it with
# productbuild so we can attach a Distribution.xml (which controls the
# installer UX — license screen, post-install symlink to /usr/local/bin).
pkgbuild \
--root "$BUILD_DIR/stage" \
--identifier "$BUNDLE_ID" \
--version "$VERSION" \
--install-location "/" \
--scripts "$SCRIPT_DIR/../templates/macos-pkg-scripts" \
"$COMPONENT_PKG"

DISTRIBUTION_XML="$BUILD_DIR/distribution.xml"
sed "s/__VERSION__/$VERSION/g; s/__BUNDLE_ID__/$BUNDLE_ID/g; s/__DISPLAY_NAME__/$DISPLAY_NAME/g" \
"$SCRIPT_DIR/../templates/distribution.xml" > "$DISTRIBUTION_XML"

productbuild \
--distribution "$DISTRIBUTION_XML" \
--package-path "$BUILD_DIR" \
--version "$VERSION" \
"$PKG_OUT"

# ──────────────────────────────────────────────────────────────────────
# 6. Sign + notarise (optional — only when Apple secrets are present)
# ──────────────────────────────────────────────────────────────────────
if [[ -n "${APPLE_DEVELOPER_ID:-}" ]]; then
section "Sign .pkg"
SIGNED_PKG="$BUILD_DIR/AgentMark-${VERSION}-macos-signed.pkg"
productsign \
--sign "$APPLE_DEVELOPER_ID" \
"$PKG_OUT" \
"$SIGNED_PKG"
mv "$SIGNED_PKG" "$PKG_OUT"

if [[ -n "${APPLE_APP_NOTARIZATION_USER:-}" && -n "${APPLE_APP_NOTARIZATION_TEAM_ID:-}" && -n "${APPLE_APP_NOTARIZATION_PASSWORD:-}" ]]; then
section "Notarise .pkg"
xcrun notarytool submit "$PKG_OUT" \
--apple-id "$APPLE_APP_NOTARIZATION_USER" \
--team-id "$APPLE_APP_NOTARIZATION_TEAM_ID" \
--password "$APPLE_APP_NOTARIZATION_PASSWORD" \
--wait
xcrun stapler staple "$PKG_OUT"
else
echo "Notarisation env vars not set; skipping. Apple Gatekeeper will warn end users."
fi
else
echo "APPLE_DEVELOPER_ID not set; skipping signing. The .pkg works but Gatekeeper will warn."
fi

section "Done"
echo "Installer: $PKG_OUT"
ls -lh "$PKG_OUT"
Loading
Loading