Command reference¶
This page documents every command-line entry point in the repository: what it
does, whether it changes anything, and each flag with its type, default, and
meaning. The flag tables come from --help output and from the argparse
definitions in the source, because many defaults appear only in the source.
Run all commands from the repository root with Python 3.11 or later. The scripts
under src/ and tools/ use only the Python standard library.
Read-only and mutating commands¶
Before you run a command that you have not used before, find it in this table. The last column names the exact flag or action that makes it change something.
| Command | Default effect | What makes it change something |
|---|---|---|
tools/spot_prices.py |
Reads prices and inventory | --csv writes a local file |
src/spot_finder.py |
Reads prices and inventory | Nothing in the cloud. A quota error writes a local cooldown marker, gcp-spot-finder-quota-<project>.timestamp, in the temporary directory, and later runs wait out that cooldown |
src/runner_fleet.py |
Reads runners and VMs | Nothing |
src/spot_runner_manager.py |
Prints a plan after read-only checks | --apply, which also deletes old unattached manager-labeled disks in the zone, or --patch-live (which needs no --apply) |
src/spot_runner_autoscaler.py |
Manages the fleet | --dry-run leaves VMs, disks, registrations, and controller state unchanged, but its gcloud compute ssh can add your SSH key; --install-launch-agent installs the macOS agent |
src/spot_runner_autoscaler_deploy.py |
Prints the deploy commands | --apply creates or updates the autoscaler's serverless job, scheduler, service accounts, custom role, and IAM bindings |
src/control_vm.py |
status and plan read only |
ensure --apply, rotate --apply; render-startup always writes its --output file |
src/runner_floor.py |
status and tick read only |
tick --apply and replace-unmanaged --apply create, start, and delete runner hosts and runner registrations |
src/web_dashboard.py |
Serves HTTP | --state-file persists state, including with --dump-overview; served POST routes can change state. With SPOT_CONFIG_GH_TOKEN or GH_TOKEN set, POST /api/patches/save opens a pull request on the config repository, and with DASHBOARD_HOT_PATCH_EXECUTOR=gcloud, /api/patches/hot-patch changes running hosts |
src/render_builder_startup.py |
Writes a local directory | Running the rendered launch-command.sh creates a billable VM |
src/lib/builder_runtime_update.py |
Captures and checks only | --apply, --finish, --recover-committed, --recover-finish |
src/remote_rust_build.py |
Uploads source and builds | Always changes the selected host |
src/cargo_remote.py |
Wraps remote_rust_build.py |
Always changes the selected host |
src/auto_remote_build.py |
Selects a host and builds | Always changes the selected host |
src/install_remote_commands.py |
Writes two shims | Always writes local files |
| Proof runners | Run the test suite in CI | Write proof files under /target |
just recipes |
just lists recipes |
just test starts remote work |
A live call to GCP or GitHub needs authenticated gcloud
and gh command-line tools. A read-only command still needs those credentials.
Several commands accept --project with a default that is a real GCP project
identifier (ID) of the repository owner. Always pass --project explicitly. The
tables below mark the default of these flags as the owner's project.
tools/spot_prices.py¶
Lists current GCP Spot compute prices for machine types that meet a minimum CPU count and RAM and cost no more than a daily cap. It reads the official GCP Billing Catalog and the machine-type inventory of all zones, and prints CSV to standard output with progress on standard error.
- Effect. Read-only. It creates no VM and reserves no capacity.
- Prices. Compute only. Disks, GPUs, networking, and taxes are excluded.
- Omissions. Shared-core shapes and shapes with missing catalog prices are left out and counted in a diagnostic message. Conflicting catalog prices get their own message. Accelerator and bare-metal shapes are skipped silently.
- Credentials. The active
gcloudaccount supplies an access token. The project comes from--projector fromgcloud config.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--min-cpu |
Positive integer | Required | Minimum vCPU count. |
--min-ram |
Positive number | Required | Minimum RAM in GiB. |
--price-per-day |
Positive number | Required | Maximum USD per 24 hours, compute only. |
--list |
cheapest, expensive |
cheapest |
Sort order of the result. |
--results |
all or a positive integer |
all |
Number of rows to print. |
--project |
String | Active gcloud project |
Project for the machine-type inventory. |
--csv |
Path | None | Also write all displayed rows to this CSV file. |
The CSV columns are region, machine_type, vCPU, RAM_GiB, USD_hour,
USD_day, and zones.
python3 tools/spot_prices.py --min-cpu 4 --min-ram 8 \
--price-per-day 5 --list cheapest --results 20
src/spot_finder.py¶
Scans all active regions for the cheapest Spot offers that meet a minimum vCPU count and RAM, then prints a table with hourly, daily, monthly, and per-runner prices. It optionally prints an alert for offers that are at least 95 percent below the on-demand price.
- Effect. Read-only in the cloud. Results are candidates, not reservations. A quota error writes the
local cooldown marker
gcp-spot-finder-quota-<project>.timestampin the temporary directory. - Prices. Compute only. Disk, network, operating system licensing, and capacity are excluded.
- Exit codes.
0on success,1on an authentication or API error,2when no offer matches. If some regional price requests fail but offers remain, the scan is partial: the command prints a warning and the first five errors on stderr, still lists the remaining offers, and exits0. If every request fails, no offer remains and the command exits2. - Alerts. The 95 percent alert needs either
--list-priceor a Billing Catalog API key from--billing-api-keyor theGOOGLE_CLOUD_API_KEYenvironment variable. Without either, the command prints a notice that the alerts are off. The command does not read.env, so export the variable in your shell.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--provider |
GCP (case-insensitive) |
GCP |
Provider. Only GCP exists. |
--project |
String | Active gcloud project |
GCP project ID. |
--min-vcpu |
Positive integer | 32 |
Minimum vCPU count. |
--min-ram |
Positive number, optional GB or GiB suffix |
128 |
Minimum RAM in GiB. |
--runners |
Positive integer | 1 for hosts below 4 vCPUs or with at most 8 GiB of RAM, otherwise vCPU // 4 |
Runner count per instance, used for per-runner prices. |
--top |
Positive integer | 10 |
Number of cheapest results. |
--concurrency |
Positive integer | 600 |
Concurrent API requests. |
--list-price |
Positive number | None | On-demand hourly price baseline in USD for the 95 percent alerts. The finder applies this one value to every offer, whatever its shape. |
--billing-api-key |
String | GOOGLE_CLOUD_API_KEY |
Billing Catalog API key for automatic 95 percent alerts. |
-v |
Repeatable flag | Off | More diagnostic detail. Repeat as -v, -vv, or -vvv. |
src/runner_fleet.py¶
Prints the state of the GitHub self-hosted runners and the Compute Engine VMs,
and the scale-out plan that would restore an online-runner floor. It calls the
gh and gcloud command-line tools and creates or deletes nothing.
- Effect. Read-only.
- Output. Online, registered, and busy runner counts; running, listed, Spot, and stopped VM counts; and the scale-out plan.
- Exit codes.
0on success,2on agh,gcloud, or parsing error. - Scope.
--labelfilters only the runner counts. The VM list covers every VM in the project unless you pass--vm-name-prefix. - Evidence. The counts do not show workload readiness, free memory, or an exclusive build reservation.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--org |
String | swack-tools |
GitHub organization. |
--target |
owner/repo or organization |
Value of --org |
Target repository or organization that the runners registered with. |
--project |
String | Owner's project | GCP project that holds the VMs. |
--label |
String | None | Count only runners that have this label. |
--vm-name-prefix |
String | None | List only VMs whose name starts with this prefix. |
--minimum-runners |
Integer | 1 |
Online-runner floor for the plan. Must be at least 1. |
--runners-per-vm |
Integer | 8 |
Runners per VM for the plan. Must be at least 1. |
--gh-binary |
Path or name | gh |
GitHub command-line tool to run. |
--gcloud-binary |
Path or name | gcloud |
gcloud command-line tool to run. |
python3 src/runner_fleet.py --org swack-tools --project YOUR_PROJECT \
--label spot --minimum-runners 1 --runners-per-vm 8
src/spot_runner_manager.py¶
Provisions one Spot VM that hosts persistent GitHub Actions runner containers,
or previews that deployment. The command verifies the machine shape, the live
Spot price, compatible storage, and quota before it changes anything. It loads
.env from the repository root for credentials such as GH_TOKEN.
- Effect. Without
--apply, the command exits after read-only checks and prints a plan. With--apply, it creates a runner registration token, provisions the VM, passes a startup script through instance metadata, registers the runners, waits for them to come online, and then removes the wholestartup-scriptmetadata key, which holds the one-use token. It also connects withgcloud compute sshto wait for bootstrap and check storage, which can add your SSH key to project or instance metadata, or to your OS Login profile. It rewrites the GitHub labels of the host's runners, records the applied patch in instance metadata and labels, writes the--deployment-receiptfile when you pass one, and, with a billing export table, queries BigQuery for project costs. - Disk cleanup. Every
--applyrun that passes the GitHub checks and obtains a registration token deletes orphaned runner data disks before it creates or adopts the VM. It deletes every disk in the selected zone and project that is labeledmanaged-by=spot-runner-manager, isREADY, is attached to no VM, and is older than six hours. The deletion happens even when the deployment then fails, and a deleted disk can't be recovered. Controller launches run the same cleanup. - Live patch.
--patch-livechanges a running VM and its runner containers over SSH, rewrites the runners' GitHub labels, and records the patch in instance metadata, even when the host already runs that patch. It does this without--applyand before the price and quota checks, so treat it as a mutating flag. - Sizing.
--runner-count,--runner-cpu, and--runner-memory-gbtake effect with--fixed-runner-sizing. A--runner-countof 1 is also honored without it. Without it, the command derives one runner per 4 vCPU from--cpuand--ram. A host below 4 vCPUs or with at most 8 GiB of RAM gets one runner, and so does a host where that count would leave a runner less than 2 GiB of memory. A tier or strict pool overrides these values. - Name limit. The runner name
<prefix>-<instance>-<count>can have at most 64 characters.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--project |
String | Owner's project | GCP project. |
--org |
String | swack-tools |
GitHub organization. |
--target |
owner/repo or organization |
Value of --org |
Target repository or organization. Overrides --org. |
--max-cost |
Decimal | Required | Compute price cap in USD per hour for this VM. The live price must be strictly below it. |
--max-total-daily-usd |
Decimal | None | Compute plus boot and data storage cost cap in USD per day. The total must be strictly below it. A tier caps it at the tier maximum. |
--cheapest-storage |
Flag | Off | Choose compatible storage from current regional prices. |
--billing-export-table |
project.dataset.table |
GCP_BILLING_EXPORT_TABLE |
Billing export table for project-wide cost logs during --apply. |
--instance-type |
String | n2-standard-32 |
Machine type. |
--min-cpu-platform |
String | Preferred platform for the machine family | Explicit minimum CPU platform. |
--region |
String | asia-south2 |
GCP region. |
--zone |
String | <region>-a |
GCP zone. |
--cpu |
Integer | 32 |
Expected vCPU count. Must match what Compute Engine reports for the machine type. |
--ram |
Integer | 128 |
Expected RAM in GiB. Must match what Compute Engine reports. |
--instance-name |
String | oxidex-spot-runners-asia |
VM name. |
--disk-size-gb |
Integer | 100 |
Data disk size in GiB. |
--disk-type |
String | Compatible type for the machine family | Data disk type. |
--disk-device-name |
String | runner-data |
Name of the data disk resource in the zone, and its device name. An unattached disk with that name and a matching type, size, and performance is reused. |
--heavy-runner-pool |
2, 3 |
None | Strict CI-only pool of that many runners, each with 8 vCPU and 32 GiB and no extra swap. |
--runner-count |
Integer | 4 |
Runner count. Takes effect with --fixed-runner-sizing. |
--runner-cpu |
Integer | 4 |
vCPU cap per runner. Takes effect with --fixed-runner-sizing. |
--runner-memory-gb |
Integer | 15 |
Memory cap per runner in GiB. Takes effect with --fixed-runner-sizing. |
--fixed-runner-sizing |
Flag | Off | Use the explicit runner sizing values. |
--runner-label |
String | oxidex-spot-7x27 |
Primary custom runner label. Runners also get the machine type, the region, and any patch labels as labels. A strict pool forces spot-heavy32. |
--runner-group-id |
Integer | The runner group ID that controller launches use | Organization runner group ID. Applies to organization targets. |
--runner-prefix |
String | oxidex-asia |
Runner name prefix. |
--existing-instance |
Flag | Off | Adopt and bootstrap an existing Spot VM instead of creating one. With --apply, it labels the VM, attaches the data disk or turns on its auto-delete, writes startup metadata, and resets a running VM or starts a stopped one. |
--deployment-receipt |
Path | None | Save the created VM identity before waiting for runner registration. |
--host-service-account |
Service account email | None | Create the VM as this account with only the logging-write and monitoring-write scopes, instead of the default Compute Engine account. |
--registration-timeout-seconds |
Integer | 1200 |
Fail when bootstrap has not finished this long after deployment starts. |
--apply |
Flag | Off | Run provisioning and startup-script deployment. |
--remote-builder |
Flag | Off | Install the legacy shared-host Rust builder on a tier host. See the note after this table. |
--resource-monitoring |
Flag | Off | Install metrics-only Ops Agent memory monitoring. |
--tier |
small, starting, expanded, large, bulky |
None | Tier host with shared memory admission. The --cpu and --ram values must match the tier shape. |
--tier-slots |
Integer | Tier's initial slot count | Number of runner listeners on a tier host. |
--tier-job-memory-gib |
Number | 15 |
Per-job memory cap and reservation. 15 is a verified compatible workload cap, not a measured peak. |
--host-overhead-gib |
Number | 2 |
Host memory for the operating system and services. A tier host reserves the larger of this value and 2 GiB, plus 0.25 GiB for each listener slot. Without a tier, it applies only to hosts with 8 GiB of RAM or less. |
--config-repo |
owner/repo |
<org>/spot-runners-config |
Central runner configuration repository. |
--patch-repo |
owner/repo |
None | Repository whose patch manifest the command loads from the configuration repository. |
--patch-file |
Path | None | Local spot-runner-patch-v1 YAML or JSON manifest. Choose at most one of --patch-file and --patch-repo. |
--patch-live |
Flag | Off | Hot-patch a running VM and its active runner containers in place. Needs --patch-repo or --patch-file. |
The --remote-builder help text says that the flag installs a dedicated Rust
1.97.1 remote builder. The flag does build the oxidex-remote-builder:1.97.1 image from
builder/Dockerfile, but that image is the legacy shared-host builder, not a
dedicated builder. Dedicated builder-* VMs use Rust 1.99.0 and come from
src/render_builder_startup.py. The flag requires
--tier, and a strict pool rejects it. Remote builds on a legacy host go through
src/remote_rust_build.py.
A strict pool needs an organization target, --runner-group-id, and host
headroom of 2 vCPU and 16 GiB beyond the pool. A pool is incompatible with
--tier and --remote-builder.
This preview prints the plan and creates nothing:
python3 src/spot_runner_manager.py \
--project YOUR_PROJECT --org swack-tools \
--max-cost 0.125 --max-total-daily-usd 3 \
--instance-type n4-standard-16 --region asia-east2 --zone asia-east2-b \
--cpu 16 --ram 64 --tier large --tier-slots 20 \
--tier-job-memory-gib 32 --instance-name oxidex-runners-example
For the patch flags, see Runner patches.
src/spot_runner_autoscaler.py¶
Runs a controller that keeps an online floor of cost-capped runners. Each check
reads GitHub and GCP, decides whether to grow, shrink, or rotate hosts, and
launches hosts through src/spot_runner_manager.py. It
persists operations in a state file so that a restart resumes an interrupted
operation. See Fleet and autoscaler for the policies.
- Effect. Without
--dry-run, the controller creates, drains, rotates, and deletes VMs and runner registrations, and reruns eligible crashed jobs. - Dry run.
--dry-runprints one JSON tier plan and changes no VM, disk, runner registration, or controller state. It reads host evidence withgcloud compute ssh, which can add your SSH key to project or instance metadata, or to your OS Login profile, on the first connection. It evaluates the tier policy whatever--scaling-modesays. It still creates the log file, the state directory, and the state lock file on your machine. It takes the same lock as a running controller, so it fails while a controller uses that state file: pass another--state-fileto preview next to a running controller. With--manage-control-vm, it also prints aControl VM plan (dry run):text line before the JSON and creates the project control VM lock file. - LaunchAgent.
--install-launch-agentwrites~/Library/LaunchAgents/com.swacktools.spot-runner-autoscaler.plist, runslaunchctl bootstrap, and exits. The agent hasRunAtLoadandKeepAlive, so a live controller starts at once, and its arguments never include--dry-run. It runs before the--dry-runbranch, so--dry-rundoes not make it safe. The command doesn't check thelaunchctlexit status. - Single controller. An exclusive lock on the sibling
<state file>.lockallows one controller for each state file. A state file binds to one target. Use a separate--state-filefor each--target. - Polling limits. The
resourceandtieredmodes need--interval-seconds 300. Thetieredmode needs--maximum-hostsof 5 or less. - Remote builder. The controller passes
--remote-builderto the manager only when it launches a tiered host. Inresourceandlegacymodes the flag has no effect.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--org |
String | swack-tools |
GitHub organization. |
--target |
owner/repo or organization |
Value of --org |
Target repository or organization. |
--make-before-break, --no-make-before-break |
Flag pair | Off | Run zero-downtime make-before-break rotation on price spikes. A controller with --target always rotates this way, whatever this flag says. The flag has no effect in tiered mode, which replaces an overpriced host through its own guarded tier price replacement. |
--github-user |
String | None | Use an existing gh account for this process without saving its credential. |
--project |
String | Owner's project | GCP project. |
--minimum-runners |
Integer | 1 |
Online runner floor for the resource and legacy modes. Must be at least 1. The tiered mode ignores it. |
--label |
String | spot |
Runner label that the controller manages. |
--max-daily-usd |
Decimal | 10 |
Maximum daily compute plus boot and data storage cost per VM. A tier uses its own cap. |
--interval-seconds |
Integer | 300 |
Seconds between checks. |
--scaling-mode |
tiered, resource, legacy |
resource |
Scaling policy. resource uses CPU and memory over two 5-minute checks at the 75 percent threshold. legacy uses runner occupancy. tiered uses tier evidence. |
--maximum-hosts |
Integer | 5 |
Maximum number of Spot hosts for resource scale-up and tier migrations, plus one host of overlap during a rotation or migration. The legacy scale-up doesn't check it. |
--tier-job-memory-gib |
Positive finite number | 15 |
Per-job memory cap and reservation. 15 is a verified compatible workload cap, not a measured peak. |
--dry-run |
Flag | Off | Tier decision only; changes no VM, disk, registration, or controller state. Its gcloud compute ssh can add your SSH key. |
--manage-control-vm |
Flag | Off | Also manage the control VM that hosts the dashboard. With --dry-run, print only its plan. |
--migrate-oversized |
Flag | Off | Gradually replace existing oversized owned hosts. |
--remote-builder, --no-remote-builder |
Flag pair | Enabled | Install the legacy shared-host Rust builder on new tiered hosts. |
--disk-size-gb |
Integer | 100 |
Data disk size for new hosts. |
--billing-export-table |
project.dataset.table |
GCP_BILLING_EXPORT_TABLE |
Billing export table for launch cost logs. |
--host-service-account |
Service account email | None | Account that new runner hosts run as, with only the logging-write and monitoring-write scopes. Without it, hosts run as the project's default Compute Engine account. The serverless autoscaler job sets the dedicated spot-runner-host account. |
--log-file |
Path | ~/spot-ghrunner.log |
Append-only controller log. |
--state-file |
Path | ~/Library/Application Support/Spot Runner Autoscaler/state.json |
Controller state file. |
--install-launch-agent |
Flag | Off | Install the macOS LaunchAgent and exit. |
--patch-repo |
owner/repo |
None | Repository whose patch manifest every launched host gets. Needed to rotate hosts that run a patch. |
--config-repo |
owner/repo |
<org>/spot-runners-config |
Central runner configuration repository. |
--job-repository |
owner/repo, repeatable |
The repository target itself, or <target>/oxidex for an organization target |
Repository whose crashed jobs the controller tracks and reruns. |
--once |
Flag | Off | Check and scale once, then exit. |
This preview reads providers and prints one plan:
python3 src/spot_runner_autoscaler.py \
--project YOUR_PROJECT --org swack-tools \
--github-user YOUR_GITHUB_ACCOUNT \
--scaling-mode tiered --tier-job-memory-gib 32 \
--no-remote-builder --dry-run --once
src/spot_runner_autoscaler_deploy.py¶
Creates or updates the autoscaler's Cloud Run job, its Cloud Scheduler trigger, its service accounts, the
spotRunnerHostAdmin custom role, and the IAM bindings. Without --apply it prints every command as a shell
script and runs nothing. See Serverless autoscaler.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--project |
String | Owner's project | GCP project. |
--region |
String | us-central1 |
Region of the job, the scheduler, and the image repository. |
--bucket |
String | <project>-spot-control-state |
State bucket that the watchdog deploy created. |
--image |
Image digest | None | <region>-docker.pkg.dev/<project>/spot-control/spot-runner-autoscaler@sha256:<digest>. Without it, only the infrastructure is deployed. |
--autoscaler-args-file |
Path | None | Autoscaler flags for the job: shell words on each line, # comments. Needed with --image. |
--github-secret |
String | spot-floor-github-swack-tools |
Secret Manager secret with the GitHub token. |
--ssh-secret |
String | spot-runner-autoscaler-ssh-key |
Secret Manager secret with the autoscaler's SSH private key. |
--cpu |
Integer | 1 |
vCPUs for each execution. |
--memory |
Quantity | 2Gi |
Memory for each execution. |
--task-timeout |
Integer | 3600 |
Seconds before Cloud Run stops an execution. |
--github-repo |
owner/repo |
swack-tools/spot-github-runners |
Repository of the image workflow. |
--project-number |
Integer | Looked up | Skip the project number lookup. |
--no-compute-condition |
Flag | Off | Grant the custom role without the oxidex-runners-* IAM condition. |
--skip-scheduler |
Flag | Off | Deploy the job, but not the scheduler. |
--apply |
Flag | Off | Run the commands. |
The job's entrypoint, src/spot_runner_autoscaler_job.py, runs one --once check for each execution. It
refuses --install-launch-agent, --github-user, --state-file, --log-file, --once, --dry-run, and
--manage-control-vm in its arguments, and refuses to start without SPOT_AUTOSCALER_STATE_URI and
SPOT_RUNNER_FLOOR_STATE_URI.
src/control_vm.py¶
Inspects and manages the control VM, either a Spot VM or the always-on free-tier
e2-micro, that hosts the web dashboard. The VM exists as two fixed slots,
spot-control-a and spot-control-b, for make-before-break replacement. See
Edge hosting and control VM for the design.
| Subcommand | Effect |
|---|---|
status |
Prints control VMs, the active origin, health, and transition state as JSON. Read-only. |
plan |
Prints the next lifecycle action. Read-only. |
render-startup |
Writes the static control VM startup script to --output. Always writes that file. |
ensure |
Runs one lifecycle step: bootstrap, start, recover, or price rotation. Prints a plan unless you pass --apply. |
rotate |
Runs a make-before-break rotation to the other slot now. Prints a plan unless you pass --apply. |
The top-level help says that nothing is written without --apply. That is
accurate for the control VM, the disks, and the state files, but
render-startup writes its --output file without --apply. Only ensure and
rotate accept --apply. With --apply, the command takes the project-wide
control VM lock, creates, starts, stops, or deletes VMs, writes Cloudflare
Workers KV entries, and saves the project control VM state. It also moves any
control VM state saved in the autoscaler --state-file into the project state
and rewrites that file. Without it, the command plans on an in-memory copy of the state.
Shared options¶
Every subcommand accepts these options. A flag overrides its environment variable.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--project |
String | Owner's project | GCP project. |
--state-file |
Path | ~/Library/Application Support/Spot Runner Autoscaler/state.json |
Autoscaler state file whose control VM state, saved before project scoping, moves to the project state file. |
--control-state-dir |
Path | Directory of the default state file; SPOT_CONTROL_STATE_DIR |
Directory of the project-scoped control VM state and lock. |
--provisioning |
spot, standard |
spot; SPOT_CONTROL_PROVISIONING |
spot picks the cheapest Spot shape under the daily cap. standard runs the free-tier e2-micro in us-central1, us-east1, or us-west1. |
--regions |
Comma-separated regions | us-central1, us-east1, us-west1 (us-central1 in standard); SPOT_CONTROL_REGIONS |
Allowed regions. |
--max-daily-usd |
Decimal | 0.50; SPOT_CONTROL_MAX_DAILY_USD |
Daily compute plus boot-disk cap per control VM. |
--no-external-ip |
Flag | External IP on; SPOT_CONTROL_EXTERNAL_IP set to 0, false, no, or off also turns it off |
Create the VM without an external IP. Needs a NAT gateway for egress. |
--service-account |
String (email address) | None; SPOT_CONTROL_SERVICE_ACCOUNT |
VM service account. |
--dashboard-image |
Digest-pinned image reference | None; SPOT_DASHBOARD_IMAGE |
Dashboard image. |
--dashboard-repo |
owner/repo |
swack-tools/spot-github-runners; SPOT_DASHBOARD_REPO |
Repository that a spot VM builds when no image is set. A standard VM never builds: it pulls the published image for --dashboard-source-ref, and only when this is the default repository. |
--dashboard-source-ref |
40-character commit hash | None; SPOT_DASHBOARD_SOURCE_REF |
Commit to build. |
--public-url |
URL | https://spot.swacktech.com; SPOT_DASHBOARD_PUBLIC_URL |
Public dashboard URL. |
--cloudflared-image |
Image reference with a version tag or digest (:latest is refused) |
A pinned cloudflared release; SPOT_CLOUDFLARED_IMAGE |
Tunnel client image. |
--origin-domain |
Domain | A domain of the repository owner; SPOT_CONTROL_ORIGIN_DOMAIN |
Domain of the per-slot tunnel hostnames. Always set it explicitly. |
--config-repo |
owner/repo |
None; SPOT_CONFIG_REPO |
Dashboard runner configuration repository. |
--controlled-targets |
Comma-separated targets | None; DASHBOARD_CONTROLLED_TARGETS |
Organizations and owner/repo targets on which the dashboard requires administrator rights. |
--global-admins |
login:numeric_id entries, or none |
Dashboard default; DASHBOARD_GLOBAL_ADMINS |
Users that skip per-target administrator checks. |
Additional options:
| Subcommand | Flag | Type | Default | Meaning |
|---|---|---|---|---|
render-startup |
--output |
Path | Required | File to write the startup script to, with mode 0644. |
ensure, rotate |
--apply |
Flag | Off | Execute the step instead of printing the plan. |
Health probes and freeze or import steps need the Cloudflare Access service
token (CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET) and the control
secret (DASHBOARD_CONTROL_SECRET). When they are missing, the manager reports
the problem and pauses lifecycle changes. The command loads .env from the
repository root, except for render-startup.
src/runner_floor.py¶
Keeps at least one minimal Spot runner host for each target in
SPOT_RUNNER_FLOOR_TARGETS, with its state and per-target leases under
SPOT_RUNNER_FLOOR_STATE_URI. The control VM watchdog runs tick --apply on
every tick. See Runner floor keeper.
It does nothing while no floor target is set.
| Subcommand | Effect |
|---|---|
status |
Prints each target's plan, floor record, hosts, and runners as JSON. Read-only. |
tick |
Plans one floor step for each target. With --apply, takes each acting target's lease and runs its step. |
replace-unmanaged |
Retires an unmanaged runner host after the managed floor host is online. Prints the plan unless you pass --apply. |
| Subcommand | Flag | Type | Default | Meaning |
|---|---|---|---|---|
| All | --project |
String | Owner's project | GCP project. Put it before the subcommand. |
status, tick |
--github-user |
String | Each target's account= |
gh account for every target. |
status |
--offers |
Flag | Off | Also price the cheapest floor host offers. Reads the billing catalog. |
tick |
--apply |
Flag | Off | Run the planned steps. |
tick |
--target |
String, repeatable | Every target | Only these targets. |
tick |
--budget-seconds |
Number | None | Time budget of the tick. The watchdog passes its deadline in SPOT_TICK_DEADLINE_EPOCH instead. |
replace-unmanaged |
--target |
owner/repo |
Required | A repository floor target. |
replace-unmanaged |
--instance, --zone |
String | Required | The unmanaged VM to retire and its zone. |
replace-unmanaged |
--old-runner |
String, repeatable | None (required) | Each runner registration of the old host. The preview lists candidates. |
replace-unmanaged |
--accept-dropped-labels |
Flag | Off | Accept that the old machine-type and region labels don't move to the managed host. |
replace-unmanaged |
--label-strip-drain |
Flag | Off | Drain an old host that can't run the job-start cordon by removing its labels instead. |
replace-unmanaged |
--github-user |
String | The target's account= |
gh account. |
replace-unmanaged |
--apply |
Flag | Off | Run the replacement. |
replace-unmanaged |
--max-wait-seconds |
Integer | 14400 |
Stop, resumable with exit status 75, after this long. |
replace-unmanaged |
--poll-seconds |
Integer | 30 |
Time between observations. |
On a workstation, the command loads .env from the repository root and reads
GitHub tokens with gh auth token --user, never from an environment variable.
src/web_dashboard.py¶
Runs the HTTP server for the fleet and Rust builder dashboard, or prints its initial overview. See Dashboard for the interface and Edge hosting and control VM for sign-in and state durability.
- Effect. The server writes local files only when you pass
--state-file: the state file, plus freeze markers, recovery markers, and renamed copies of an unreadable state file next to it.--dump-overviewwith--state-filestill creates or rewrites that file, so omit--state-filefor a dump without local writes. The server also exposes POST routes, so treat a running dashboard as able to change state./api/instances/deleteand/api/instances/scale-regionchange only the dashboard's own records, not VMs. WhenSPOT_CONFIG_GH_TOKENorGH_TOKENis set in the server's environment,POST /api/patches/savecreates a branch, commits, and opens a pull request on the config repository, which is--config-repo,SPOT_CONFIG_REPO, or<org>/spot-runners-config. WithDASHBOARD_HOT_PATCH_EXECUTOR=gcloud,/api/patches/hot-patchchanges running hosts. - Binding. The default bind address is loopback. A non-loopback
--hostneeds authentication (--auth-tokenor GitHub OAuth 2.0) and either TLS or--allow-insecure-non-loopbackbehind a trusted TLS-terminating proxy. The server refuses to start otherwise. - KV. Cloudflare KV hydration and write-behind need
--control-slotplus theCLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_KV_NAMESPACE_ID, andCLOUDFLARE_KV_API_TOKENenvironment variables. Without--control-slot, the server logs a warning when those credentials exist and keeps state local.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--host |
String | 127.0.0.1 |
Bind address. |
--port |
Integer | 8787 |
HTTP port. |
--project |
String | Owner's project | GCP project ID. |
--org |
String | swack-tools |
GitHub organization. |
--auth-token |
String | DASHBOARD_AUTH_TOKEN |
Legacy bearer token. The dashboard accepts it only from a direct loopback client with no forwarding headers, such as an SSH-forwarded connection. Binding a non-loopback address requires it or GitHub OAuth 2.0, but direct non-loopback clients can authenticate only with OAuth 2.0. |
--tls-certfile |
Path | DASHBOARD_TLS_CERTFILE |
PEM TLS certificate. Pair it with --tls-keyfile. |
--tls-keyfile |
Path | DASHBOARD_TLS_KEYFILE |
PEM TLS private key. |
--allow-insecure-non-loopback |
Flag | DASHBOARD_ALLOW_INSECURE_NON_LOOPBACK set to 1, true, yes, or on |
Allow plaintext HTTP on a non-loopback host behind a trusted TLS-terminating proxy. |
--control-slot |
a, b |
None | Control VM slot. Enables KV hydration and write-behind when the KV credentials exist, and /api/control/* when DASHBOARD_CONTROL_SECRET is set. |
--controlled-targets |
Comma-separated targets | DASHBOARD_CONTROLLED_TARGETS, else none |
Organizations and owner/repo targets on which OAuth 2.0 users must be administrators, in addition to every repository in the configuration repository. |
--oauth-redirect-uri |
Absolute URL | GITHUB_OAUTH_REDIRECT_URI, else the public origin plus /auth/github/callback, else derived from the Host header |
GitHub OAuth 2.0 callback URL. |
--config-repo |
owner/repo |
SPOT_CONFIG_REPO, else <org>/spot-runners-config |
Repository whose repos/<owner>/<repo>.yaml files are controlled targets. |
--public-origin |
Origin URL | DASHBOARD_PUBLIC_ORIGIN, else the origin of the OAuth 2.0 redirect URI |
Browser-facing origin that POSTs accept when a proxy forwards under a different Host. |
--state-file |
Path | None | Persist dashboard state JSON to this path. Freeze and recovery markers go next to it. |
--dump-overview |
Flag | Off | Print the initial JSON overview and exit without serving. With --state-file, it still creates or rewrites that file. |
src/render_builder_startup.py¶
Validates a dedicated builder configuration and an approved uploader identity,
then renders a self-contained startup payload and a single gcloud compute
instances create command. Dedicated builders have a fixed shape (n4-highcpu-16
with 16 vCPU and 32 GiB). See Dedicated builders for the
design and the full launch procedure.
- Effect. Writes four files into a new directory:
startup.sh,cloud-config.yaml,cargo-remote-env.toml, andlaunch-command.sh. It makes no cloud call. Runninglaunch-command.shcreates a billable VM and a persistent cache disk withauto-delete=no. - New directory only. The command creates
--output-directorywith mode0700and refuses an existing directory. The parent directory must already exist. The rendered files aren't executable, so run the launch command withbash. Fresh-host startup must not replay on an occupied builder. Usebuilder_runtime_update.pyfor an occupied builder. - Output. Prints the path of
launch-command.sh. - Configuration file. A JSON file with
instance_name(must start withbuilder-),project, andzone. The renderer fills in the fixed shape, disk, and container settings. Explicit values still pass the strict policy checks. Without--uploader-mapping, the file must embedauthorized_uploaderandauthorized_uploader_sha256.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--config |
Path | Required | JSON configuration file. |
--uploader-mapping |
Path | None | Approved public uploader mapping JSON. The renderer computes its checksum. Do not combine it with embedded authorized_uploader fields. |
--deployment-id |
String | instance_name from the configuration |
Deployment identity in the startup receipt. |
--output-directory |
Path | Required | New directory for the rendered files. |
python3 src/render_builder_startup.py --config /private/builder-config.json \
--uploader-mapping /private/approved-uploader.json \
--output-directory /private/builder-payload
Review the files before you run launch-command.sh.
src/lib/builder_runtime_update.py¶
Updates the runtime files of an idle, occupied dedicated builder in place. It does not provision a VM or rebuild an image. See Update a builder runtime for the procedure and recovery details.
The program refuses to run unless it runs as root on Linux with Python isolated
mode and no bytecode (python3 -I -B). Run it through the existing authenticated
root control path on the builder. The --help text works anywhere.
The workflow has these stages:
- Capture.
--capture-assetsrecords the live VM, boot, runtime files, and admission process, and prints a plan. This stage is read-only. - Check.
--planwith--plan-sha256and no action flag checks a reviewed plan and writes nothing. It returnsCHECKED_ONLY, orCHECKED_FINISH_ONLYwhen you also pass--client-launcher-sha256. - Apply.
--applytakes the build lock, refuses active builds, creates an owned drain, backs up files, and replaces the fixed runtime file set. It returnsINSTALLED_DRAINED. The route stays closed. - Client pin handoff. Coordinate the exact OxiDex launcher pin out of band. The helper does not change clients.
- Finish. Capture a new plan with
--finish-nonceand--client-launcher-sha256, check it, then run it with--finish. It reopens the route and returnsREOPENED_UNQUALIFIED. Qualification readiness stays false until real build checks pass.
The recovery modes are --recover-finish for an interrupted finish on the same
boot and --recover-committed for a committed release that a reboot
interrupted. Never replay --apply to recover.
Choose exactly one of --plan and --capture-assets. Choose at most one of the
four action flags.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--plan |
Path | One of --plan or --capture-assets is required |
Reviewed plan file. Needs --plan-sha256. |
--capture-assets |
Path | One of --plan or --capture-assets is required |
Root-owned asset JSON file with the reviewed runtime targets. Needs --assets-sha256 and --source-main. |
--plan-sha256 |
SHA-256 hex digest | None | Digest of the plan file. The plan must match it. |
--assets-sha256 |
SHA-256 hex digest | None | Digest of the asset file. |
--source-main |
Commit hash | None | Reviewed source commit for the capture. |
--apply |
Flag | Off | Apply the reviewed plan. Without it, a plan is only checked. |
--finish |
Flag | Off | Reopen an installed route. Needs --client-launcher-sha256. |
--recover-committed |
Flag | Off | Recover a committed release after a reboot. Needs --client-launcher-sha256. |
--recover-finish |
Flag | Off | Restore an interrupted finish to the held state. Needs --client-launcher-sha256 on the plan stage and --finish-nonce on capture. |
--finish-nonce |
32 hex characters | None | Installation nonce for a capture of fresh observations. Valid only with --capture-assets. |
--client-launcher-sha256 |
SHA-256 hex digest | None | Paired client launcher pin. It selects the finish checks, and you cannot combine it with --apply. |
A capture with --finish-nonce, --recover-finish, or --recover-committed
needs --client-launcher-sha256, and --recover-finish and --recover-committed need the nonce too.
The --apply and --finish flags are invalid on a capture. A committed-release error exits with status 75 after it prints the result as
JSON.
This capture is read-only:
python3 -I -B builder_runtime_update.py --capture-assets assets.json \
--assets-sha256 ASSET_FILE_SHA256 --source-main REVIEWED_SOURCE_COMMIT
src/remote_rust_build.py¶
Snapshots a local OxiDex checkout, uploads it to an explicitly selected legacy
shared-host builder, builds the oxidex binary there, verifies the checksum,
and records evidence. It uses gcloud compute scp and gcloud compute ssh, so
use it only for legacy hosts that were installed with the manager's
--remote-builder flag. Do not use it for dedicated builder-* VMs. Those use
direct authenticated SSH from the OxiDex just build recipe. See
Remote builds and clients.
- Effect. Always transfers source and starts a remote build. It writes
remote-build.json,remote-source.tar.gz, andtransaction.logto the evidence directory. - Snapshot. The archive holds only files that
git ls-fileslists. It skips the.git,.codex,.claude,.agents,target,node_modules, and.venvdirectories, files whose names start with.env, files with a.pemor.keyextension, and symbolic links. - Cleanup. When the command exits, it removes its upload and export directory from the host and records whether the cleanup succeeded.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--source |
Path | Required | Local checkout to snapshot. |
--instance |
String | Required | VM name of the builder. |
--zone |
String | Required | Zone of the builder. |
--project |
String | Owner's project | GCP project. |
--worktree-id |
1 to 64 letters, digits, underscores, or hyphens; the first character is a letter or digit | Required | Identifier of the remote project directory. |
--evidence-dir |
Path | Required | Directory for the receipt and logs. |
--profile |
debug, release |
release |
Cargo profile. |
--artifact-dir |
Path | None | Download the built oxidex binary here and verify its checksum. |
Retryable failures are exit codes 75, 137, 143, and 255; the receipt
records a retryable flag. Because this command always builds, no read-only
example exists. Use --help to check the options:
src/cargo_remote.py¶
Adapts cargo oxidex-remote to remote_rust_build.py for a
configured OxiDex checkout. It runs inside the current Git repository, reads the
[env] table of .cargo/config.toml at the repository root, and then calls
remote_rust_build.py with the repository as --source. It fails when the file
is missing. After you install the shims with
install_remote_commands.py, run it as
cargo oxidex-remote.
The command resolves each setting from the process environment first, then from
the [env] table:
| Setting | Required | Fallback |
|---|---|---|
OXIDEX_REMOTE_INSTANCE |
Yes | None |
OXIDEX_REMOTE_ZONE |
Yes | None |
OXIDEX_REMOTE_PROJECT |
No | gcloud config get-value project |
OXIDEX_REMOTE_WORKTREE |
No | oxidex- plus the first 12 hex characters of the SHA-256 of the checkout path |
The adapter sets --evidence-dir to a new directory named
cargo-remote-<UTC timestamp> under ~/oxidex-ops/evidence/. It passes the rest
of your arguments, such as --profile and --artifact-dir, to
remote_rust_build.py, so those options match the table in the previous
section. Like that command, it always builds. Its --help output is the remote_rust_build.py parser's, so it lists
--source, --instance, --zone, --worktree-id, and --evidence-dir as required even though the adapter
supplies them from the checkout and the environment. Options that you pass are appended after the adapter's and
override them. Run --help to see the options:
src/auto_remote_build.py¶
Chooses a lightly used legacy builder host and delegates the build to it. After
you install the shims, run it as cargo oxidex-auto.
- It reads the project from
OXIDEX_REMOTE_PROJECT(environment or the[env]table of.cargo/config.toml), else fromgcloud config. - It lists running VMs whose names start with
oxidex-runners-oroxidex-buildbench-, reads CPU and memory utilization from GCP Monitoring, and keeps hosts below 75 percent on both. - It probes each candidate over SSH for
/etc/oxidex-remote-build.jsonand no/run/oxidex-build-draining, and picks the best-ranked match. - It runs
cargo remote-buildwith the chosen instance and zone, writesselection.jsonand the build evidence under~/oxidex-ops/evidence/auto-remote-<UTC timestamp>/<attempt>, and downloads the artifact intotarget/remote-linux/<profile>in the checkout. - After a retryable failure, it excludes that host and selects again, up to
--max-attemptstimes.
The OxiDex checkout must provide the cargo remote-build command, because this
repository installs cargo-oxidex-remote and cargo-oxidex-auto but no
cargo-remote-build.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--profile |
debug, release |
Required | Cargo profile. |
--max-attempts |
Positive integer | 3 |
Maximum worker selections after retryable failures. |
The command always builds on a remote host. Check its options with:
src/install_remote_commands.py¶
Writes two executable shell shims that run scripts from this checkout:
cargo-oxidex-auto (runs auto_remote_build.py) and cargo-oxidex-remote
(runs cargo_remote.py). Each shim embeds the absolute path of this checkout, so
reinstall the shims if you move it. The command overwrites existing shims and
creates the directory if needed. It prints one path -> script line per shim.
| Flag | Type or choices | Default | Meaning |
|---|---|---|---|
--bin-dir |
Path | $CARGO_HOME/bin, else ~/.cargo/bin |
Directory for the shims. |
Preview the result in a throwaway directory:
Proof runners¶
tools/infra_python_test_proof.py and tools/infra_python_complete_proof.py are
continuous integration (CI) runners that measure the unit test suite inside the
admitted Linux builder container. Neither accepts flags, and you do not run them
by hand. For local test runs, see Development.
- Inputs. Both expect a read-only
/srcmount with the source tree, afleet-source-headfile, and aninfra-python-test-manifest.jsonmanifest, plus a writable/targetdirectory. The runners don't verify a signature themselves: they require the manifest'ssource_committo equalfleet-source-headand each test module to match its SHA-256 in the manifest. - Outputs. They write
infra-python-test-proof.jsonandinfra-python-test.logto/targetwith exclusive creation, and refuse to run when either file exists. - Exit codes.
0for a pass,1for a failed or incomplete measurement, and2when the runner refuses the input. - Producer.
infra_python_test_proof.pyverifies that the test modules match the manifest, runsunittestdiscovery, and enforces floors for test cases (MIN_CASES) and modules (MIN_MODULES). A skipped test, an expected failure, an unexpected success, or a module with no test cases fails the run. - Complete proof.
infra_python_complete_proof.pyloads the producer only if its SHA-256 equals the pinnedPRODUCER_SHA, replaces the producer's floors withmax(774, module count)cases andmax(76, module count)modules, requires the executed test IDs to equal the discovered IDs, and adds digests of the discovered and executed test IDs to the proof. After a reviewed change to the producer, updatePRODUCER_SHAand rerun the proof tests.
just recipes¶
The justfile defines three recipes. Run just with no arguments to list them.
| Recipe | Effect |
|---|---|
just test |
Submits this checkout to the reviewed OxiDex remote route. Needs OXIDEX_CONTROL_REPO to point to an OxiDex control checkout whose justfile has a test-infra-ci-remote recipe, and runs that recipe with this checkout's path. Exit status 2 if either is missing. |
just infra-python-complete-tests |
Runs PYTHONPATH=src python3 -B tools/infra_python_complete_proof.py. It refuses to run unless you are a non-root user on Linux inside the admitted builder container (the /run/oxidex-build-container file must exist). |
just |
Lists the recipes. Read-only. |
Do not report just test as run when you ran only the local unittest
discovery. The two prove different things.