Skip to content
Closed
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
38 changes: 37 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,42 @@ jobs:
- uses: actions/checkout@v4
- run: node scripts/version.mjs check

# Also unconditional, and for the same reason as `version`: the numbers it
# checks are written in README.md and docs/, so a docs-only pull request is
# exactly where a stale one arrives, and no component suite runs for that.
repo-scripts:
name: repo scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Unit tests for scripts/
run: node --test scripts/*.test.mjs
- name: Documented counts match the code
run: node scripts/check-counts.mjs

# A release pull request -- a `release/*` branch, or `develop` into `main` --
# must mention every pull request merged into develop since the last tag.
# Every other pull request skips this, and skipped counts as green below.
changelog:
name: changelog coverage
if: >-
github.event_name == 'pull_request' && (
startsWith(github.head_ref, 'release/') ||
(github.head_ref == 'develop' && github.base_ref == 'main'))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v4
with:
# The tags and the whole of develop's history since the last one.
fetch-depth: 0
- name: Every merged pull request is in the changelog
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/changelog-coverage.mjs --head "${{ github.event.pull_request.head.sha }}"

collector:
needs: changes
if: needs.changes.outputs.collector == 'true'
Expand Down Expand Up @@ -142,7 +178,7 @@ jobs:

ci-ok:
name: ci-ok
needs: [changes, version, collector, runner-android, runner-ios, runner-machine, fleet, runner-roku, desktop]
needs: [changes, version, repo-scripts, changelog, collector, runner-android, runner-ios, runner-machine, fleet, runner-roku, desktop]
# Without this the gate inherits the default "only if every dependency
# succeeded", which makes a skipped suite skip the gate -- and a skipped
# required check never reports, which is the exact bug being fixed.
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,66 @@ jobs:
./desktop/stage-sidecar.sh
- working-directory: desktop/src-tauri
run: cargo check --all-targets

launch:
# The first time the app ever ran was this job. `cargo check` proves the
# Tauri calls exist; this proves the binary starts, spawns `fleet up` as its
# sidecar, stays up, and that its settings window loads and answers over
# IPC. Linux rather than macOS because that is where Tauri's WebDriver
# support exists (tauri-driver wraps WebKitWebDriver; there is no macOS
# driver), and because Xvfb gives it a display without a logged-in desktop.
# What it cannot prove is anything macOS-specific: the tray in the menu bar,
# the Local Network grant, signing. See desktop/README.md.
name: launch smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22.13"
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: desktop/src-tauri
key: linux-launch
# Tauri 2's Linux prerequisites (webkit2gtk 4.1, appindicator for the
# tray), plus what the smoke itself uses: Xvfb for a display, dbus for a
# session bus, WebKitWebDriver for tauri-driver, ImageMagick to capture
# the screen.
- name: Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev \
libssl-dev libayatana-appindicator3-dev librsvg2-dev \
xvfb x11-utils dbus-x11 webkit2gtk-driver imagemagick jq
- name: Stage the fleet sidecar
run: |
npm --prefix collector ci
npm --prefix fleet ci
./desktop/stage-sidecar.sh
- name: Build the app
working-directory: desktop/src-tauri
# custom-protocol is what `cargo tauri build` turns on: the frontend is
# embedded and served from the binary, as it is in a release, rather
# than expected from a dev server.
run: cargo build --features custom-protocol
- name: tauri-driver
run: cargo install tauri-driver --locked
- name: Launch it
run: ./desktop/ci/launch-smoke.sh
env:
OUT: ${{ runner.temp }}/desktop-smoke
- if: always()
name: What it printed
run: |
for f in "$RUNNER_TEMP"/desktop-smoke/*.log "$RUNNER_TEMP"/desktop-smoke/fleet-home/logs/*.log; do
[ -f "$f" ] || continue
echo "== $f"; tail -80 "$f"
done
- if: always()
uses: actions/upload-artifact@v4
with:
name: desktop-launch-smoke
path: ${{ runner.temp }}/desktop-smoke
if-no-files-found: warn
162 changes: 159 additions & 3 deletions .github/workflows/fleet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,21 @@ jobs:
# run it the way the docs say, and drive it through the CLI from outside:
# a container whose port is published but whose collector never answers is
# the failure a `docker build` alone would call green.
name: container smoke
runs-on: ubuntu-latest
#
# The arm64 row builds natively on GitHub's arm64 runner rather than under
# QEMU on the amd64 one. The release publishes linux/arm64 from a QEMU
# build, and an image that only ever ran emulated is the one a Raspberry Pi
# owner would be first to start for real.
name: container smoke${{ matrix.suffix }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
suffix: ""
- runner: ubuntu-24.04-arm
suffix: " (arm64)"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand All @@ -99,13 +112,21 @@ jobs:
cache: npm
cache-dependency-path: fleet/package-lock.json
- working-directory: .
run: docker build -f fleet/Dockerfile -t fleet:ci .
run: |
docker build -f fleet/Dockerfile -t fleet:ci .
docker image inspect fleet:ci --format 'built for {{.Os}}/{{.Architecture}}'

- working-directory: .
run: docker run -d --name fleet -p 8788:8788 fleet:ci up --role brain,agent
- name: The collector answers, and the machine agent registers
shell: bash
env:
EXPECT_ARCH: ${{ matrix.runner == 'ubuntu-24.04-arm' && 'aarch64' || 'x86_64' }}
run: |
set -euo pipefail
arch=$(docker exec fleet uname -m)
echo "container is running on $arch"
[ "$arch" = "$EXPECT_ARCH" ] || { echo "::error::expected $EXPECT_ARCH, the container says $arch"; exit 1; }
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:8788/api/v1/health && break
sleep 2
Expand Down Expand Up @@ -165,3 +186,138 @@ jobs:
run: |
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
journalctl --user -u fleet.service --no-pager | tail -100 || true

launchd:
# The twin of the systemd smoke for macOS. `fleet service install` writes a
# user LaunchAgent and bootstraps it into the gui/<uid> domain; GitHub's
# macOS runners run their jobs inside a logged-in GUI session, so that
# domain exists here the way it does on a desk. The first step says so
# rather than assuming it, because a runner without one would make every
# later failure look like a fleet bug.
name: launchd smoke
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22.13"
cache: npm
cache-dependency-path: fleet/package-lock.json
- run: npm ci && npm --prefix ../collector ci && npm run build
- name: The runner has a GUI launchd domain
shell: bash
run: |
set -euo pipefail
echo "uid $(id -u), user $(id -un)"
launchctl print "gui/$(id -u)" | head -15
- name: Install as a LaunchAgent, it serves, and its agent registers
shell: bash
run: |
set -euo pipefail
mkdir -p "$HOME/.fleet"
cp -R dist/. "$HOME/.fleet/"
fleet() { node "$HOME/.fleet/bin/fleet.mjs" "$@"; }
plist="$HOME/Library/LaunchAgents/com.addisdev.fleet.plist"
fleet service install
plutil -lint "$plist"
launchctl print "gui/$(id -u)/com.addisdev.fleet" | head -20
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:8788/api/v1/health && break
sleep 2
done
curl -fsS http://127.0.0.1:8788/api/v1/health >/dev/null
n=0
for i in $(seq 1 30); do
n=$(fleet devices --url http://127.0.0.1:8788 --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).items.length))')
[ "$n" -gt 0 ] && break
sleep 2
done
[ "$n" -gt 0 ] || { echo "::error::the agent under launchd never registered"; exit 1; }
fleet devices --url http://127.0.0.1:8788
fleet service status
# Reinstalling is bootout-then-bootstrap, the race service.ts retries.
# Run it once here so the retry is exercised rather than trusted.
fleet service install
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:8788/api/v1/health >/dev/null 2>&1 && break
sleep 2
done
curl -fsS http://127.0.0.1:8788/api/v1/health >/dev/null
fleet service uninstall
sleep 5
if curl -fsS http://127.0.0.1:8788/api/v1/health 2>/dev/null; then
echo "::error::the collector is still answering after uninstall"
exit 1
fi
[ ! -e "$plist" ] || { echo "::error::$plist is still there after uninstall"; exit 1; }
if launchctl print "gui/$(id -u)/com.addisdev.fleet" >/dev/null 2>&1; then
echo "::error::launchd still has com.addisdev.fleet loaded after uninstall"
exit 1
fi
if fleet service status; then
echo "::error::fleet service status exited 0 after uninstall"
exit 1
fi
- if: failure()
shell: bash
run: |
tail -100 "$HOME/.fleet/logs/fleet.log" || true
for f in "$HOME"/.fleet/logs/*.log; do echo "== $f"; tail -40 "$f"; done || true

windows-task:
# `fleet service install` on Windows creates a scheduled task that runs
# `fleet up` at logon and starts it at once. Same lifecycle as the systemd
# and launchd smokes: install, it serves, its agent registers, status says
# so, uninstall, and nothing is left answering.
name: windows task smoke
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22.13"
cache: npm
cache-dependency-path: fleet/package-lock.json
- shell: bash
run: npm ci && npm --prefix ../collector ci && npm run build
- name: Install as a scheduled task, it serves, and its agent registers
shell: bash
run: |
set -euo pipefail
home="$(cygpath -u "$USERPROFILE")/.fleet"
mkdir -p "$home"
cp -R dist/. "$home/"
fleet() { node "$home/bin/fleet.mjs" "$@"; }
fleet service install
schtasks //Query //TN FleetRunner //FO LIST //V
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:8788/api/v1/health && break
sleep 2
done
curl -fsS http://127.0.0.1:8788/api/v1/health >/dev/null
n=0
for i in $(seq 1 30); do
n=$(fleet devices --url http://127.0.0.1:8788 --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).items.length))')
[ "$n" -gt 0 ] && break
sleep 2
done
[ "$n" -gt 0 ] || { echo "::error::the agent under the scheduled task never registered"; exit 1; }
fleet devices --url http://127.0.0.1:8788
fleet service status
fleet service uninstall
sleep 5
if curl -fsS http://127.0.0.1:8788/api/v1/health 2>/dev/null; then
echo "::error::the collector is still answering after uninstall"
exit 1
fi
if schtasks //Query //TN FleetRunner >/dev/null 2>&1; then
echo "::error::the FleetRunner task is still there after uninstall"
exit 1
fi
- if: failure()
shell: bash
run: |
home="$(cygpath -u "$USERPROFILE")/.fleet"
schtasks //Query //TN FleetRunner //FO LIST //V || true
for f in "$home"/logs/*.log; do echo "== $f"; tail -60 "$f"; done || true
tasklist //FI "IMAGENAME eq node.exe" || true
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ from an agent, see [`skills/fleet-runner/SKILL.md`](skills/fleet-runner/SKILL.md
| `collector/src/api/v1/` | The control API. `routes.ts` is the single route table: it registers the routes, generates `openapi.json` and the typed client |
| `collector/src/client/` | The generated client (`api.gen.ts`) and its transport. The CLI and the MCP server use nothing else |
| `fleet/` | The `fleet` / `fleetr` command: runs a brain or a runner, plus the work commands, `fleet mcp` and the remote gateway |
| `desktop/` | A Tauri menu-bar app around `fleet`. A front door, not a runner; compiled in CI and never launched |
| `desktop/` | A Tauri menu-bar app around `fleet`. A front door, not a runner; compiled in CI on macOS and launched in CI on Linux (`desktop/ci/launch-smoke.sh`), never run on a Mac |
| `runner-machine/`, `runner-android/`, `runner-ios/`, `runner-roku/`, `collector/runner-web/` | Runners. They speak the runner protocol (`/devices/register`, `/devices/:id/next-job`, `/results`), not `/api/v1` |
| `skills/fleet-runner/` | The agent skill. Its tables are generated |
| `bug-garden/` | An Android fixture app with twenty planted defects, for scoring exploratory QA |
Expand Down
Loading
Loading