This is a personal project that was made public because the write-up was worth sharing. Contributions are welcome and there is no obligation for anyone to send any.
The most useful thing you can send is a device report: what your hardware measured, what it refused, and what broke. The whole point of the project is comparable numbers across hardware nobody has a lab full of, and every phone that is not on this shelf is data this repository does not have.
Five components, five suites. Only run the one you touched.
=== "collector"
```bash
cd collector
npm install
npm run dash:install # once, for the dashboard build step
npm test
```
`npm test` typechecks, builds the dashboard, checks metric names against
the schema, validates the example job specs, then **starts its own
collector on a kernel-assigned port with a temporary database** and runs
the smoke suite against it. It never touches a real fleet's history, which
is why it can be wired to `npm test` at all.
It takes about five minutes. If you changed a route in
`src/api/v1/routes.ts`, run `npm run gen:api` first: the suite fails when
`openapi.json` or the generated client no longer match the route table.
=== "fleet"
```bash
cd fleet
npm install
npm test
```
The CLI, the MCP server and the remote gateway, each tested against a real
collector it starts itself. `npm run skill:gen` after changing a tool or
an exit code; the suite fails when `skills/fleet-runner/SKILL.md` is stale.
=== "runner-android"
```bash
cd runner-android
./gradlew :app:testDebugUnitTest -Pfleet.skipNative
```
**`-Pfleet.skipNative` is what lets you run this with no NDK.** The arm64
llama.cpp build takes about fifteen minutes and needs a pinned NDK, and the
JVM tests never call into it. CI uses the same flag.
For the native build you need the submodule and NDK 27.2:
```bash
git submodule update --init --recursive
./gradlew :app:assembleDebug
```
=== "runner-ios"
```bash
cd runner-ios
brew install xcodegen
./generate.sh
xcodebuild build -project FleetRunner.xcodeproj -scheme FleetRunner \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build CODE_SIGNING_ALLOWED=NO
```
=== "runner-machine"
```bash
cd runner-machine
npm install && npm test
```
About a second. This is the easiest component to contribute to
and the reference implementation of the protocol.
Each of these cost somebody an evening.
Run one xcodebuild per simulator. Two concurrent test sessions against
the same simulator send each other SIGTERM and wedge it. A "crash" with no
.ips file is almost always a collision, not a bug in the app.
Call xcodegen by its full path in anything non-interactive. Homebrew's
bin is not on a non-interactive PATH, so a bare xcodegen over SSH or under
launchd fails in a way that looks like the project generated fine and your new
files simply did not compile. generate.sh already handles this.
Regenerate the Xcode project after syncing files to another machine. An
rsync that carries *.xcodeproj across overwrites a project generated for
that machine. Exclude it and run ./generate.sh on the far end.
The committed FleetRunner.xcodeproj is deliberately the one without the
llama.cpp framework. Xcode treats a declared-but-missing XCFramework as a hard
error rather than something to skip, so a spec naming it unconditionally cannot
be built from a fresh clone. If you regenerate with the framework present, the
project will show as modified — do not commit that.
An iOS build succeeding does not mean the app starts. import WebKit once
hard-linked a Swift overlay that newer runtimes no longer ship, and the app died
in dyld before main() on every push that CI called green. check-backdeploy.sh
guards that statically and CI runs it. If you touch anything that imports a
system framework, also run ./launch-smoke.sh by hand — CI cannot, because a
GitHub runner has only the newest iOS runtime and back-deployment breaks on the
older ones.
A metric name that is not in result.schema.json is stored and unqueryable.
Add it to the schema in the same change that emits it. This has already cost
one write-up its reproducibility: an eval's accuracy rode in decode_tok_s
because vision had no field of its own, and no query can reproduce those numbers
today. npm test fails if the schema and its mirror disagree.
Declare a capability only when the machine can honour it. A capability claimed but not implemented is a job the queue hands over and the agent bounces, which from the dashboard looks exactly like a broken workload.
You do not need a collector release. The collector accepts any workload a registered agent declares in its capabilities. See Add a workload and Writing a runner.
Say what is not true yet.
Every workload page ends with what that workload refuses to do. Every README has a "what works, and what does not". The integration guide says on its first screen that the GitHub Actions path has never run on a real runner.
When you cannot measure the thing asked for, fail with a message naming what is missing. A wrong number recorded as a result is worse than a failure, because a failure gets investigated and a number gets believed. The iOS Simulator's GPU returning an all-zero logits tensor — silently, with no error — is the reason this is the first rule rather than a nice sentiment.
- Branch from
develop.mainis the released trunk. - Run the suite for what you touched, and say in the description what you ran.
- Update the docs in the same change. The site builds with
--strict, so a broken link fails CI. - Add a
CHANGELOG.mdentry under## [Unreleased]for anything user-visible, ending with a link to the pull request (([#86](https://github.com/addisdev/fleet-runner/pull/86))) once it has a number. A pull request nobody using Fleet Runner would notice — CI plumbing, a typo — gets theno-changeloglabel instead. The release check below holds both to it. - A number the docs state by hand (how many workloads there are) is marked
<!-- count:workloads -->34.repo scriptsinci.ymlfails when a marked number disagrees withcollector/src/workloads/catalog.ts; fix the number, keep the marker.
ci-ok is the only required check. It always reports, and it passes when every
component suite is green or skipped — a change to one runner does not build
the others.
One version covers every component, so a release is one number everywhere.
-
On a branch named
release/X.Y.Zfromdevelop, runnode scripts/version.mjs set X.Y.Z. It writesVERSIONand every component's own version, the desktop app'sCargo.tomlandtauri.conf.jsonincluded. Then runnpm --prefix collector run gen:api, becauseopenapi.jsoncarries the version and CI fails when it is stale. Android'sversionCodeis not derived from it; bump that by hand if the Play Store build needs it. -
In
CHANGELOG.md, rename## [Unreleased]to## [X.Y.Z] — YYYY-MM-DD, add a fresh empty## [Unreleased]above it, and update the compare links at the bottom. -
Merge that into
develop, then open adevelop→mainpull request namedRelease X.Y.Zand merge it onceci-okpasses.On both pull requests
ci.ymlrunschangelog coverage(scripts/changelog-coverage.mjs). It lists every pull request merged intodevelopsince the lastv*tag and fails, naming each, if one is not mentioned as#Nor a/pull/Nlink between## [Unreleased]and the last release's heading. Label a pull requestno-changelogto exempt it; Dependabot's are exempt already, and so are the release's own pull requests (release/*intodevelop,developintomain). The check only runs when the branch isrelease/*, ordevelopintomain— so name the branch that way, or it will not look. Run it yourself first withnode scripts/changelog-coverage.mjs --head origin/develop(it needsghfor the labels). -
Tag the merge commit on
mainand push the tag:git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z.
The tag runs release.yml. Its first job refuses to publish anything unless
the tag, VERSION and every component agree. Then it builds the six
platform archives and SHASUMS256.txt, pushes the container image, and creates
the GitHub Release, which install.sh and install.ps1 read. A version with a
suffix (0.9.0-rc.1) is published as a pre-release and leaves latest alone.
Pushing to main also publishes the docs site.
Dependabot is deliberately limited to security alerts and security fixes.
There is no dependabot.yml, which is what keeps version-bump pull requests
from burying the ones that matter. If you want to upgrade something, do it in a
change that says why.