- Python 100%
* discover no longer crashes on a mix of reserved and unreserved boxes A reserved row in discover's table has a sixth cell and every other row has five, and the table padding measured every row as long as the first. So any mix of the two crashed with an IndexError, in either order, after printing its heading and before writing anything (O22). Rows may now be different lengths, and a short one simply ends early. The test adopts a reserved box and a box with no GPU together, in both orders: both are written, and only the reserved one says it is reserved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * Every printed reset-windows-password command carries --quiet The command printed when a Windows password reset answers with no credentials went out without --quiet, so pasted into a script it stopped to ask and exited 1 (O25). Its three siblings already had the flag. The guard that should have caught it read two files by name and matched the command as one literal, and this one was in a third file, split across two literals. The guard now walks the syntax tree of every module in comfy_qa/, where adjacent literals are already one string, and checks every printed gcloud delete and reset-windows-password for --quiet. tests/printed.py holds that walk so the next guard over printed lines can use it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * A box named stockout is not a stock-out, and one whitespace fold for all readers The bare word "stockout" was one of the stock-out signs, and Google echoes the resource path in most of what it refuses, so an already-exists or not-found about a box called qatest-stockout read as "the zone is out of stock, try another" (O25). The sign is now Google's own state code, state:STOCKOUT, which a box name cannot hold. The real line-wrapped text from the live pass, and the state code on its own, are still recognised. gcloud wraps its YAML errors anywhere, and only the stock-out check folded whitespace before matching. classify and suggested_zones now go through the same fold, gcloud.folded, so a wrap can no longer hide Google's suggested zone or turn a missing project into an unknown failure. The test breaks each real gcloud text the suite holds at every space in turn and checks all three readers give the same answer as on the unbroken text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * A --region stock-out tells the truth on a one-region project, and keeps the box Three follow-ups to the live pass's --region fix (O23): When the region --region named is the only one the project can use the card in, the stock-out said "not everywhere this project can use the card" and offered "drop --region", which retried the same zones. It now says that is the only region there is and that there is nowhere left to try. Whether there is anywhere else comes from the catalogue create already reads, so no call is added. The "same box" in that remedy dropped --disk, and --name for a box that was not reserved, so pasted back it made a 200 GB box under another name. It now carries the name for every box and the disk whenever it is not the default. The untried-regions remedy beside it had the same gap and now uses the same helper. The no-GPU half of the wiring that tells the stock-out a region was named had no test; one drives create --gpu none --region through the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * comfy-qat keeps its own Google project, in the host list The project was gcloud's: setup --project ran gcloud config set project, and every command asked gcloud config get-value project. So comfy-qat and every other gcloud user on the machine shared one setting, and moving comfy-qat to a new project moved everything else with it. Now one project = "<id>" line at the top of the host list says it. setup --project writes it through the same checked rewrite as every other change to that file, and nothing in the tool can set gcloud's project any more. create, discover, quota, status and down --all read the line; without one they use gcloud's current project as before, and say so. Every gcloud call that acts on a project carries --project, so the line reaches Google whatever gcloud is set to. A box keeps the gce_project its own entry names, so after a switch the old boxes are still stopped, started and deleted where they are. list says which project each box is on and where a new one will go. The docs say how to switch and that there is no move between projects. A misspelt line, or a display name where the ID goes, is refused rather than ignored, since ignoring it would send every call to gcloud's project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * Every command the tool prints runs as pasted Two things stopped a printed command working when pasted whole (O25): A printed comfy-qat delete <name> stopped to ask for the box's name, so from a script or an agent it exited 2 with nothing deleted, and for a reserved box that is the command that stops the bill. Every printed delete now carries --yes. Typing delete yourself still asks. Offered commands carried their reason as a trailing # note, as in "comfy-qat down comfy-linux # stop the box, stop paying". An interactive zsh without interactivecomments, which is how zsh starts, passes the # and every word after it to the command, which refuses them. The reason is now a line of its own ending in a colon, with the command alone under it. say.offer builds that shape, and a test reads every string in the package and fails on a command line with a # note on the end. The docs that quote these lines are updated to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * The 1.3.0 notes say what the live run really found The 1.3.0 changelog said the live run found four defects, fixed below. It found ten, and four were fixed; the entry now says so and names the other six. It had no line for the fix to how a stock-out is recognised in Google's line-wrapped text, and now has one. The acceptance pack still said what ComfyUI calls the device on a box with no GPU had not been seen; the live run recorded it, and the pack now quotes the stamp line it printed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * Release 1.3.1 Version 1.3.1, with a changelog entry naming each fix in plain words and saying what ran on real Google boxes and what did not. The README's Status line names the version, which the suite requires; nothing else in it moved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * Every reader of gcloud's text goes through the one fold `gcloud.folded`'s docstring says every reader that matches a phrase in what gcloud printed goes through it. Three did not: `is_already_asked`, `_missing_resource` and `is_auth_failure`'s text fallback matched the raw text, so a wrap gave False, "" and False. Each failed in the safe direction, but the claim was false. All three fold now, phrases too, and the wrap test reads them with not-found and quota-preference texts added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * A --zone stock-out offers the same box, and two guards read literal names After a `--zone` stock-out a box that was not reserved was offered a bare `comfy-qat create`: no `--os`, `--gpu`, `--name` or `--disk`, so pasted with no terminal it exited 2. Both branches use `_same_box` now, and a CLI test compares the offered command with the arguments and pastes it back. The guards for a printed gcloud delete or reset without `--quiet`, and a printed `comfy-qat delete` without `--yes`, matched only an f-string hole after the verb, so a box name typed out passed. They match any word now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * setup --help says --project replaces the host list's project line The help said an existing host list is "read, never replaced" and only appended to, and `--project` said "use this project instead of asking". Neither said `--project` writes the project line, over any already there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * The docs quote the endings 1.3.1 prints Acceptance checks V4 and V6 and two troubleshooting entries still quoted commands with a `# note` on the end and a printed delete without `--yes`, so a tester following them would fail correct output. They quote the reason line, the command alone under it, and `delete --yes` now. The 1.3.1 entry names the `--zone` remedy and the three readers folded since. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * The tunnel's readers fold gcloud's text, and lifecycle prints one command a line tunnel.py's three readers of a dead tunnel's log (_iap_refused, still_booting, _credentials_expired) lowered the text and nothing more, so a log broken at `port⏎22` read a box still booting as one with nothing listening, and `go` told it to start ComfyUI first. They go through gcloud.folded now, as every other reader does, and the wrap test reads them: the real booting log, the 4003 refusal and an expired session, plus every phrase each one looks for broken at each of its spaces (round-1 re-check of F7). lifecycle's reserved-box fix was one sentence with two commands in it, `stop the box: comfy-qat down X — it is reserved, so the bill only stops with: …`; each command is alone on its line under its reason now, and the one fix that joined two commands with `&&` prints them on two lines. The quota-preference fixture in the wrap test is word for word what the suite records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * No writer breaks a command it is handed `say.wrapped` wrapped whatever it was handed, and two commands came out of it in two pieces once their values were in. `move`'s note about a slower boot disk goes through `warn`, and the `gcloud compute disks delete … --project=… --quiet` in it was broken at 96 columns, so the first line, copied, deletes on gcloud's own project and stops to ask. And `move`'s leftovers report handed `detail` each delete indented two spaces, which `_is_prose` read as prose and wrapped the same way (round-1 F7, the move leftovers block). A line that is a command is left whole at its indent now, and a command a sentence ends on after its colon is one word to textwrap, so it moves to the next line in one piece. `detail` asks `_is_prose` of the text, not its indent. Held for every line in the package that offers a command, at three value lengths and with the width falling everywhere along it, through each writer's prefix; for the move note and the leftovers report on the printed output; and for a sentence pushed a word at a time across the width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * Every printed command is the command alone, on its project, for the same box Round two of O25e, O23b and S1-11b, each fixed for the class the audit found rather than the line it quoted. A printed command carries nothing but the command. The notes that rode on command lines are on lines of their own above them, ending in the colon that introduces the command: the `# note` fragments `region_problem` and `region_shape_problem` hand back, which `setup`, `create` and `quota` spliced into whole commands (`auth.spliced` puts the command on the flag lines only); the `--allow-lower # …` remedies; the em-dash notes after commands in create, quota and gcloud's DENIED advice; `, then wait for Google`; `… --gpu t4, the same n1-standard-8 machine …`; setup's sign-off table (`comfy-qat list see them`), whose `stamp` line is now printed only for a local machine the host list declares, by its name; `quota request ... --dry-run`, which printed a literal `...`; the project-fallback note, whose command a long project ID pushed across the wrap; and a project-ID refusal that ended on a command and is put inside other sentences. Every printed gcloud command that acts on a project names it. `down --all`'s stop lines for boxes nobody declared, the ceiling refusal's stop line and listing, the zones/regions/machine-types listings, the release and delete lines, and `prune`'s "find where each one really is", which named `gcloud compute instances list` mid-sentence and now offers it on the entry's project. With no project in hand the flag reads `--project=<your-project-id>` (`reservation.NO_PROJECT`) rather than being left out, which would act on gcloud's own project; the stale comment that gcloud's project is the one this tool follows is gone. Every "the same box, somewhere else" is the box that was asked for: `_refuse_full`, `_where_the_leftover_is` and the capped plain stock-out pair use `_same_box`, and host.py's `--zone`/`--region` conflict and region-typo rewrites keep `--name` and `--disk` (`create.besides_where`). One CLI test compares each offered command with the refused create's arguments and pastes it back with --dry-run and no terminal. The guards, in the same commit because the old ones would read the new output as wrong and the new ones the old: The first S1-10 guard read one shape — two spaces and a `#` after a line starting `comfy-qat `, `gcloud ` or a hole — and the rest of the class went on printing. The question is asked of the command now (`tests/commandlines.py`): a `#` with any space before it, a dash, a run of spaces, or a word that ends a phrase, after the program, is something other than the command; an `ssh` line is a command like the others. Two readings, because neither reaches everything. `test_host_costs` walks every string in the package (a value is `{}` there). `conftest` records every line every test prints and holds it to the same rule with the real values in, parses `comfy-qat` lines with the CLI's own parser so a word it would refuse is caught whatever it looks like, and holds a printed gcloud line to its `--project` and to `--quiet` on the same physical line. Two `test_say` tests that hand `write_fix` notes of their own are marked and listed. S1-11b: a guard that every printed gcloud compute/quotas/services/billing command, offered or named in passing, carries `--project` (only `billing accounts` is exempt, by name), and CLI tests where the host list's project differs from gcloud's: `down --all`'s and the ceiling refusal's stop lines, pasted, reach the host list's project. F6: every argv the real `Gcloud.run`/`run_interactive` is handed, and every fake standing in for `run`, is refused when it acts on a project without naming it, suite-wide — so an argv built with `str.split` on a command no test drives is caught too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbT49jkR4MQGRJLjx16xaz * The 1.3.1 entry says what round two fixed and what ran live, on which commit S1-1 was run live again on |
||
|---|---|---|
| .github/workflows | ||
| comfy_qa | ||
| docs | ||
| tests | ||
| .gitignore | ||
| CHANGELOG.md | ||
| pyproject.toml | ||
| README.md | ||
comfy-qa-tools
QA tooling for testing Comfy.
Most testing time is not spent testing. It goes on standing a machine up, getting it into a state where a result means something, and working out which box or which build you were actually looking at. This tool is that layer. Less setup, less environment wrangling, less doubt about what you are pointed at, more time testing.
Features land one at a time and are documented here when they ship — so what is on this page is what the binary does today.
Not the same tool as comfy-qa
Comfy-Org/Comfy-QA, maintained by snomiao,
does AI-driven E2E test runs. The similar names are a coincidence — two different
projects, neither replacing the other. This one's binary is comfy-qat, so they
never collide on PATH.
Status
1.4.0. Every command is implemented and tested, and everything up to 1.2.0 has
run against real GCE hardware on Linux and Windows, except two Windows paths the
1.4.0 live run found broken in 1.2.0 to 1.3.1: installing ComfyUI on a box from
nothing (it called winget, which Windows Server 2022 does not have) and the
detached launch (a process started with Start-Process did not outlive the SSH
call that started it). Both are fixed in 1.4.0; see
the changelog. 1.4.0 lets you choose the build — a ComfyUI release,
PR or commit, a frontend, Manager (on by default) and a torch pin — installs it with
comfy-cli on the box, shows the build that is running in stamp, and has a Windows
GPU box install its own NVIDIA driver. The build choice ran on a real Linux box and
a real Windows box, both with no GPU, on 2026-11-03; a Windows GPU box installing
its own driver has not run on a real box yet — see the changelog
for what did and did not.
What 1.3.0 adds — reserved boxes, the limit on them, boxes with no GPU, and the
cost columns in list — was run on real Google Compute Engine boxes on
2026-10-07: a reserved L4 box, a box with no GPU, the limit, list and
delete. A reserved T4 box could not be run, for lack of stock, and nothing that
needs more than one GPU of quota has been run for real.
The verbs are at the top level: comfy-qat go linux. The host ... and
auth ... spellings were a deprecation window rather than a second permanent way
to type everything, and it closed at 1.1.0 — typing one now exits 2. An old run
sheet that says host create or auth status wants the noun taken off.
env is carried over from v0, hidden, and awaiting its own release.
Install
Needs Python 3.11 or newer.
git clone https://github.com/Comfy-Org/comfy-qa-tools.git
cd comfy-qa-tools
Into an existing virtualenv — the fewest moving parts, and it works with what you already have:
/path/to/venv/bin/pip install -e .
-e runs it from the checkout, so git pull updates it with no reinstall. The
binary lands next to that venv's other scripts, at /path/to/venv/bin/comfy-qat.
If that directory is not on your PATH — venv bin directories usually are not —
either call it by full path or add an alias:
alias comfy-qat='/path/to/venv/bin/comfy-qat'
Or standalone with uv, which keeps it out of every virtualenv:
uv tool install .
Two things to check if comfy-qat is then "not found": uv itself has to be on
your PATH (it is often installed inside a virtualenv rather than system-wide), and
uv puts binaries in ~/.local/bin, which is not on PATH by default on macOS.
uv tool update-shell fixes the second.
Upgrading from an earlier install
Earlier versions installed as comfy-qa-cli or comfy-qa. Remove them — the
second in particular installs a comfy-qa binary, which is
a different project's command:
pip uninstall -y comfy-qa-cli comfy-qa
Then update any shell alias to point at comfy-qat.
First run
One command.
comfy-qat setup
It signs you in to Google Cloud, picks your project, checks billing, sorts out GPU quota and writes your host list — saying what it is doing, and asking only where the decision is genuinely yours. If something needs you (no billing account, no project), it stops and prints the link. Run it again afterwards; it skips what is already done.
It also asks Google for the GPU quota your project is missing, so a fresh install
ends with every card this tool can drive either granted or with a request in
flight. The exact list is printed before anything is sent — a quota request cannot
be withdrawn and may be read by a person at Google — and --no-quota-request
skips it entirely.
It works without prompts too: --project, --region, --non-interactive.
# see your machines
comfy-qat list
# re-check readiness at any time
comfy-qat status
Everyday use
# start it, tunnel in, leave ComfyUI running on the box
comfy-qat go windows
# what that ComfyUI is saying, whenever you want it
comfy-qat logs windows
# the line that says what produced your result, build included
comfy-qat stamp windows
# close the tunnel, stop the box, stop paying
comfy-qat down windows
# stop every cloud box you have declared
comfy-qat down --all
windows there is not a special name — it is the machine described rather than
named. Anywhere a host name goes you can say what you want instead: an operating
system (windows, linux, ubuntu, debian, macos), a card (l4, a100),
or both (windows/l4). Whatever it resolves to is printed, one host per line:
windows -> comfy-win (Windows Server 2022, L4)
If two machines fit, it refuses and lists them with their OS and card rather than picking one. Names always win over a description.
Changing machine is one command, which stops the box you were on:
# start the Linux box, then stop the Windows one
comfy-qat switch linux
Making the box is one command too, and the card is the only real decision:
comfy-qat create --os linux --gpu t4
The machine type follows from the card — an L4 is a G2 with the GPU built in, a
T4 is an N1 with one attached — and the zone is chosen rather than typed:
regions this project holds quota in, zones inside them that offer the card and
the machine type, ranked by latency measured from your machine, and tried in
order until one has capacity. Quota is checked before anything exists, both the
card's own grant and GPUS_ALL_REGIONS, the project-wide ceiling that is the one
that usually bites. --dry-run prints the plan, the quota it read and the zone
order it would try, and creates nothing. The finished box is added to your host
list on a free port, so go works on it straight away.
The NVIDIA driver is not in either base image, and a box without one runs
ComfyUI on its CPU while looking perfectly healthy. Both kinds of box install it
themselves on first boot, each with Google's own documented method: Linux from a
startup script, Windows from Google's driver script set as the box's
sysprep-specialize-script-url. Nothing is left for you to run, and go waits
for the driver on both.
The build is a choice. create, go and switch take a ComfyUI release, PR
or commit (--version v0.43.0, --pr 17497, --commit 426f5ee), a frontend
release or PR (--frontend-version v1.58.4, --frontend-pr 5678), and a torch
pin (--torch 2.12.1+cu130); ComfyUI-Manager is on unless you say
--no-manager. comfy-cli does the installing, on the box. A build that does not
exist is refused before anything is created, a new build on an existing box
keeps its custom nodes and models, and go reports a box ready only once the
ComfyUI running on it, and git in its checkout, say it is the build you asked
for. With none of them,
it is the tip of ComfyUI's default branch, as before.
Choosing the build has the whole of it.
go is the one command worth memorising. It starts the instance, installs
ComfyUI if the box has none — the build you chose, with comfy-cli, and a torch
built for that box's CUDA — launches
it on the box, and forwards a local port to it as soon as it is listening.
The forward is an ssh -L over Identity-Aware Proxy: it reaches the box's own
loopback, so nothing is exposed on any interface and no firewall rule is
involved.
On a box you have just created, go waits for it to finish starting. Google
reports a machine as RUNNING the moment it is powered on, which is some minutes
before it accepts SSH — and on Linux the driver install reboots it once or twice
on top of that. A box that is genuinely ready is not delayed by this; a box that
is still coming up says so while it waits, rather than failing on the tunnel.
ComfyUI stays running there and you get your prompt back. It used to be
launched in the foreground with its log streamed onto your terminal, which meant
one machine per terminal and a Ctrl-C that killed ComfyUI. Now logs <host>
reads that log whenever you want it, --follow brings the old streaming back for
when you are debugging a launch, --new-window hands the whole thing to a new
macOS Terminal window, and two boxes can be up at once from one prompt. What
has not changed: go still does not return until ComfyUI has really answered on
the tunnel, because a box that booted and serves nothing bills exactly like one
that works.
down is what stops the billing — and down --all stops every declared cloud
box, because the question at the end of a session is never "is comfy-win
stopped", it is "am I still paying for anything".
docs/machines.md covers the whole loop, including what to do
when a zone has no GPUs left.
Commands
Every target is a declared host, local or cloud — naming them all is the point, so local stops being an invisible default and picking the wrong machine becomes something you do on purpose.
| command | what it does |
|---|---|
comfy-qat setup |
get this machine ready, in one command |
comfy-qat status |
signed in? which project? billing and GPU quota? |
comfy-qat login |
print the sign-in commands |
comfy-qat quota list |
what cards you can start, and where — --by-region |
comfy-qat quota request |
ask Google for cards — --gpu l4,a100 --region us-central1 |
comfy-qat init |
write a starter host list you can edit |
comfy-qat list |
every declared machine, what is up and what is reserved — --live asks Google, and adds age and disk |
comfy-qat discover |
find cloud boxes and add them — --prune drops the entries Google confirms are gone |
comfy-qat create |
make a box; the zone is chosen, not typed — --reserve holds its capacity, --gpu none has no GPU |
comfy-qat delete |
remove a box and its disk, permanently — and release its reservation, if it has one |
comfy-qat go |
start it, make sure ComfyUI is on it, hand the prompt back |
comfy-qat up |
start a machine, and succeed only if ComfyUI is already serving |
comfy-qat open |
tunnel to a machine already running |
comfy-qat disconnect |
close the tunnel, leave the machine running |
comfy-qat down |
close the tunnel and stop the machine, so it stops costing — a reserved box goes on billing, and down says so |
comfy-qat ssh |
a shell on a box, through the tunnel |
comfy-qat rdp |
reset the Windows password and forward RDP |
comfy-qat logs |
follow the ComfyUI log on a box — --tail N reads the end and stops |
comfy-qat stamp |
ask a machine what it is, and print the line you paste |
comfy-qat switch |
start the one you want, stop the one you were on |
comfy-qat move |
move a box to a zone with capacity, keeping its ComfyUI — not a reserved one |
comfy-qat --version |
what you are running, with the commit |
docs/commands.md has the flags and which to reach for.
comfy-qat <command> --help says the same at the prompt.
The host list
Lives at ~/.config/comfy-qa-tools/hosts.toml. comfy-qat setup writes it and
fills in your cloud boxes automatically — Google already knows each one's zone,
card and operating system, so none of it needs typing. comfy-qat discover
does the same on demand, and never changes an entry you already have.
discover --prune reconciles the other way, and it is the only thing here that
removes one. Boxes disappear without this tool — deleted from the console, by a
colleague, by raw gcloud — and a stale entry is not untidiness, because it holds
a name and a port that nothing else can use. Removal takes two answers from
Google that agree: the name is nowhere on the project, and then a direct
describe of that one machine comes back not-found. Anything else is reported
and kept, including the case that matters most — a box that turns up in a zone
other than the one your entry gives is a wrong entry, not a missing machine, and
deleting it would leave a running GPU with nothing naming it.
[hosts.local]
kind = "local"
port = 8188
[hosts.comfy-linux]
kind = "gce"
os = "Ubuntu 22.04"
gpu = "L4"
gce_instance = "comfy-linux"
gce_zone = "us-central1-a"
gce_project = "your-project-id"
port = 8190
Seven rules are enforced offline, before anything else runs. The two you meet first:
- No cloud host may use 8188. That is the local ComfyUI's port; a tunnel on it would silently point you at the wrong machine, so it is refused, not warned about.
- No two hosts may share a port. If two do, you cannot tell which one you reached.
The other five — one box per cloud instance, no two names differing only in case,
local reserved for this machine, no gce_ fields on a local host, and a name
that is actually typeable — are in docs/hosts.md with the
reason for each.
Unknown fields are rejected rather than ignored, and the message names the typo and what it was probably meant to be:
host 'comfy-linux': unknown field(s) 'gce_zoen' (did you mean 'gce_zone'?). Known fields: build, gce_instance, gce_project, gce_reservation, gce_zone, gpu, kind, os, port.
Switching OS means switching host: one box per OS, and nothing is ever reimaged.
os and gpu are what windows, l4 and windows/l4 match on, so with one box
per OS you never have to remember what you called it. Every field is documented in
docs/hosts.md.
What it writes
Everything lives under ~/.config/comfy-qa-tools/:
hosts.toml— the host list, and the only file you would ever edit.hosts.toml.bak, andbackups/— anything that rewrites the host list copies it first, reads the copy back, and refuses the rewrite if it could not be made..bakis the file as it was before the command you last ran; the copy it supersedes is archived under a timestamp inbackups/rather than overwritten, and that directory is capped.zone-latency.json— how long a TCP connect to each Google Cloud region took from this machine, measured bycreateand reused for a week. Delete it to re-measure; nothing else reads it.tunnels/<host>.pid,tunnels/<host>.jsonandtunnels/<host>.log— written byopen,upandgo. A tunnel outlives the command that started it, so what it is gets recorded rather than assumed: the pid, the moment that process started, and which instance, zone and port it goes to. Pids are recycled, so the number alone is not an identity and a record that no longer fits is treated as stale rather than trusted. The log is what gcloud said while opening it.downanddisconnectremove the.pidand the.json— the two that claim a tunnel is there — and keep the.log, so you can still read why one died after you have closed it. Eachopenstarts that log fresh, so it holds the most recent tunnel for that host and nothing older.
Signing in is gcloud's job, so credentials live in gcloud's own store
(~/.config/gcloud/) and are refreshed by it. This tool never sees, stores or
prints a credential.
It does not touch your ComfyUI installs, your PATH, your shell config, or any
service on this machine. It will not overwrite a host list you already have, and
running setup again is safe — it skips whatever is already done.
On a cloud box, go does install software: ComfyUI into C:\ComfyUI
(Windows) or /opt/comfyui (Linux), with a Python 3.12 environment beside it —
and on Windows, Git, Python 3.12 and the Visual C++ runtime themselves, from
their publishers' own installers, each checked before it runs (by its pinned
SHA-256, or the runtime by Microsoft's signature).
That only ever happens on a declared gce host, never locally.
Docs
getting started · every command · the everyday loop · the host list · troubleshooting · cost · when your session expires · test criteria · spot instances · tests that cannot fail
Design
Each feature is a sibling sub-app, added in one line. comfy-qat <feature> <action>;
a new feature never touches an existing one.
comfy-cli has no plugin mechanism — no entry-points table, all sub-apps registered
by static add_typer calls — so this ships as its own binary. register() in
comfy_qa/cli.py is the object a plugin entry point would take if comfy-cli ever
grows one; the reservation is already in pyproject.toml. Nothing here depends on
that happening.
Tests
pytest tests/
The docs are tested too: every error the tool can print has to appear in troubleshooting.md, and every command in the CLI has to appear in this README. Drift between the code and the page is a failing test, not something to notice six weeks later.