feat: add fleet deploy, and let start/stop take --all - #147
Merged
Conversation
Adds a spinloop field to kind: remote fleet nodes and a spinloop fleet deploy command that batch-deploys their AWS environments, instead of running spinloop remote deploy once per environment by hand.
Renames the per-node deploy-source field from spinloop to file and makes it optional: a kind: remote node's own name is checked against the spinloop alias registry first, then against a same-named <name>/Spinloop subdirectory beside the fleet file, before failing.
A kind: daemon node does get told what to serve via a DeployConfig derived from a Spinloop file -- routing already does this on every wake, through Config.Wake/StartWith. The new file field is irrelevant to daemon nodes only because it feeds fleet deploy specifically, which persistently creates a cloud environment; a daemon node's machine has no equivalent provisioning step. Reworded to say that precisely instead of implying daemon nodes have no such mechanism at all.
Generalises the file/alias/subdirectory resolution mechanism from a kind: remote-only concept to any fleet node: spinloop fleet start on a kind: daemon node now resolves the same way and pushes the derived config via StartWith (the same mechanism a routed wake already uses), falling back to today's plain start when nothing resolves. Also requires fleet deploy to name its target nodes explicitly, or pass --all, rather than defaulting to the whole fleet.
No shipped release depends on today's fleet start behaviour for a kind: daemon node, so there's no one to protect with a silent fallback to a plain, config-less start. Make Spinloop-source resolution mandatory for fleet start on daemon nodes, exactly as fleet deploy already requires it for remote nodes: a node with no file field, alias, or subdirectory now fails fleet start rather than falling back, and every example fleet needs updating accordingly. Marked BREAKING in the proposal.
fleet deploy already accepted multiple node names; fleet start gains the same, plus --all for every node in the file (either kind, since starting is meaningful for both, unlike deploy). Reuses Config.FanOut via a new OnlyNames helper rather than a bespoke loop, since every node fleet start targets already exists as a live Node -- unlike deploy, which creates what a Node would later address. fleet stop is deliberately left untouched: exactly one node, no --all.
For symmetry with fleet start's new target selection. Both now share a runFleetDrive helper (OnlyNames + FanOut) replacing driveOneNode, which is deleted -- stop's own per-node call is unchanged (a plain Stop, no config to resolve), only how its target is selected changes.
Implements the fleet-deploy change: a node's `file` field (or a resolved spinloop alias, or a same-named subdirectory beside the fleet file) names the Spinloop that describes what it runs. - spinloop fleet deploy <node...>|--all creates the AWS environment for one or more kind: remote nodes, reusing remote deploy's own derivation/consent/registration (deriveDeployTarget + runDeploy, factored out of runRemoteDeploy). - spinloop fleet start/stop now take multiple node names or --all, replacing driveOneNode with a shared runFleetDrive built on Config.FanOut and a new Config.OnlyNames. - start on a kind: daemon node now requires its Spinloop source to resolve, pushing the derived config via StartWith -- no fallback to a plain start. BREAKING: every daemon node needs a file/alias/ subdirectory now, or start fails for it. Every example fleet updated accordingly; fleet-docker's real Docker Compose E2E suite re-run in full and passing, including new --all coverage.
resolveNodeSpinloop succeeding isn't the same as the config it points at being usable -- add coverage for the gap in between: fleetStartCall never calls Start/StartWith when the resolved source is unparseable, names an unsupported provider (the same llamacpp/vllm-only limit a routed wake already has -- MLX included), has no model, or has an engine token that resolves to nothing; and deployOneNode fails only the one node whose resolved Spinloop is itself undeployable (no REMOTE), leaving its siblings unaffected.
# Conflicts: # cmd/spinloop/remote.go # docs/commands/remote.md
Real usage of fleet deploy --all surfaced two problems: several nodes' output ran together with nothing marking where one ended and the next began, and a slow --all run gave no feedback while AWS calls were still in flight. Adds a live per-node Braille spinner while a node's deploy is still running (skipped for a non-TTY run -- gated the same way fleet dashboard already checks for one), a coloured tick/cross/warning header on each node's final report, and a closing summary line.
Adds the requirement the previous commit's fix implements: in-progress feedback while nodes are still deploying, a clear per-node boundary in the report, a closing summary, and no terminal-control output when piped or redirected. Technology- neutral -- doesn't mandate a spinner or specific escape codes, just the observable behaviour.
Syncs its delta specs into fleet-config and fleet-client, then moves the change to archive/2026-09-03-fleet-deploy/ now that implementation, tests and docs are all complete.
Every existing example in this repo names its Spinloop file literally "Spinloop", one per directory -- the fleet-deploy work broke that convention by inventing a "<name>.Spinloop" suffix for files that needed to sit beside a fleet.yaml. Moves each into its own <node-name>/ subdirectory instead, which also means the explicit file field naming them was redundant with the subdirectory resolution tier and gets dropped -- examples/fleet-docker's client/Spinloop remains the one place demonstrating file pointing somewhere that doesn't match the node's own name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A node's
filefield (or a resolvedspinloop alias, or a same-named subdirectory beside the fleet file) names the Spinloop that describes what it runs — the linkspinloop fleet deployandspinloop fleet startboth need and previously had no way to express.Summary
filefield to a fleet-file node entry (either kind). When absent, resolves in order: the node's ownnameas a registeredspinloop alias, then a<name>/Spinloopsubdirectory beside the fleet file.spinloop fleet deploy <node...>(or--all): batch-creates the AWS environment forkind: remotenodes, reusingremote deploy's derivation, consent, and registration behavior (--api-key-envincluded) — the two can never disagree about what a Spinloop file deploys.spinloop fleet start/spinloop fleet stopnow both take multiple node names or--all, replacing the old "exactly one node" restriction, via a sharedrunFleetDrivebuilt onConfig.FanOutand a newConfig.OnlyNames.kind: daemonnode,fleet startnow requires that node's Spinloop source to resolve, and pushes it viaStartWith— the same mechanism a routed wake already uses — rather than a plain, config-less start. No fallback: this is an unreleased feature with no users to protect. Every example fleet with a daemon node has afile/alias/subdirectory added.kind: remotenode behavior is unaffected either way — what it serves is fixed at deploy time.Implementation
cmd/spinloop/remote.go:runRemoteDeploysplit intoderiveDeployTarget+runDeploy, shared byremote deployand the newfleet deploy.cmd/spinloop/fleet.go:fleetDeployCmd,resolveNodeSpinloop, and a rewrittenfleetStartCmd/fleetStopCmdonrunFleetDrive(driveOneNodedeleted).internal/fleet/config.go:NodeConfig.File,Config.OnlyNames.examples/fleet-docker's real Docker Compose E2E suite re-run in full and passing, including new--allcoverage.--api-key-envdeploy feature (landed onmainmid-branch) and extended it tofleet deployfor parity withremote deploy.Test plan
go build ./...,go vet ./...,gofmt -l .all clean.go test ./... -cover: all packages passing,cmd/spinloopandinternal/fleetboth >90%.examples/fleet-docker/run-tests.sh(real Docker stack): all assertions pass.concord ci: no drift, no overlap.