fix: stop promising a nixpacks lane `insta deploy <dir>` does not have
The contradiction
Three surfaces disagreed about deploying a Dockerfile-less source directory. Reproduced on 0.0.47 with a minimal Node app (package.json with a start script + server.js, no Dockerfile):
$ insta build .srcapp
builder: nixpacks (providers: node)
checks:
✓ Dockerfile — generated by nixpacks (providers: node) <-- passes
$ insta build .srcapp --port 3000
verdict: deployable <-- blessed
# installer next-steps banner (also the upgrade banner: the binary channel
# re-runs `curl install.sh | sh` with inherited stdio)
insta deploy . --port 3000 # ship your app and get a live URL
$ insta deploy .srcapp --group src --port 8080
error: no Dockerfile at .../Dockerfile — add one, or use --image <url> <-- refuses
Which one is right
deploy is. A directory deploy mints a scoped Fly token (POST /projects/:id/deploy-token) and runs flyctl deploy --remote-only --build-only --push over the directory's own Dockerfile.
The nixpacks lane is real, but it is server-side and git-sourced: the build gateway's {source: {type: git, repo_url, commit_sha}, build: {type: nixpacks, context_path}} (insta-compute#55 / insta-platform#279), reachable only from a GitHub-connected repo. Nothing on the deploy <dir> path can enter it — the gateway's archive source type exists but, per docs/build-gateway.md, "There is no upload endpoint yet", and "CLI dir-deploy archive lane" is a recorded, not-started follow-up. So this is a known gap, not a regression.
Scope call: wiring the CLI archive path into that lane needs an object store + upload endpoint, a new platform route, and build-token plumbing across three repos. This PR does not build that. It makes the two lying surfaces truthful instead.
What changed
insta build— a nixpacks-generated Dockerfile no longer passes thedockerfilecheck. It fails as a warning, not a critical: the plan is real (the GitHub lane does build it), so the command must not exit 1 — it must only stop short ofdeployable. Verdict for a Dockerfile-less dir is nowneeds-attention, exit 0. The check detail and nextAction name the lane split, and thebuilder:line carries the caveat too, since that line is what gets scraped.- Neither message says "save the Dockerfile
--explainprints". That file is not standalone — itCOPYs the.nixpacks/nixpkgs-<hash>.nixsupport files nixpacks writes beside it into the--outdir, which the source dir does not have (the platform's own lane handles this by generating with--out <dir>and passing--dockerfile <dir>/.nixpacks/Dockerfile). Advising it would swap one false promise for another.--explainnow labels the dump# for inspection — not standalone. The detected install/start commands are reusable, and that is what both messages offer. insta deploy <dir>— the dead-end error names all three real options: write a Dockerfile (withinsta buildas the starting point),--image <url>, or the GitHub flow for the nixpacks lane. Extracted as the purenoDockerfileMessage()so it is unit-tested.install.shnext-steps banner — the deploy line says it needs a Dockerfile, and aninsta build .line precedes it so the plan is seen before the dead end. (One line ofinstall.sh, coordinated with #141.)insta build --help— no longer says nixpacks generates "the Dockerfile that would be used".
Unchanged: a directory with a Dockerfile plans, verdicts deployable, and deploys exactly as before — pinned by tests, and verified end-to-end (the guard passes and the run reaches the API).
Before / after
$ insta deploy .srcapp --group src --port 8080
- error: no Dockerfile at /tmp/srcapp-repro/Dockerfile — add one, or use --image <url>
+ error: no Dockerfile at /tmp/srcapp-repro/Dockerfile — a directory deploy builds the Dockerfile in the directory.
+ Options:
+ - add a Dockerfile to /tmp/srcapp-repro (`insta build /tmp/srcapp-repro` prints the install/start commands nixpacks detected, as a starting point)
+ - deploy a prebuilt image instead: `insta deploy --image <url>`
+ - connect the app's GitHub repo in the console — that lane builds Dockerfile-less repos with nixpacks server-side
$ insta build .srcapp --port 3000
- builder: nixpacks (providers: node)
- ✓ Dockerfile — generated by nixpacks (providers: node)
- verdict: deployable
+ builder: nixpacks (providers: node) — GitHub lane only; `insta deploy <dir>` needs a Dockerfile
+ ⚠ Dockerfile — generated by nixpacks (providers: node) — but `insta deploy <dir>` builds the directory's own Dockerfile; the nixpacks lane runs server-side for GitHub-connected repos only
+ → to deploy this directory, write a Dockerfile at /tmp/srcapp-repro/Dockerfile — the detected install/start commands above are the starting point; or connect the repo on GitHub to use the nixpacks lane
+ verdict: needs-attention
Tests — every one negative-controlled
13 tests added/updated. Each was run against unmodified src/ + install.sh (test files only applied) and fails there:
× installer next-steps banner > says a directory deploy needs a Dockerfile — it must not promise what deploy rejects
AssertionError: expected 'echo " insta deploy . --port 3000 …' to contain 'dockerfile'
× installer next-steps banner > points at `insta build` first, so the user sees the plan before the deploy can dead-end
AssertionError: expected 'echo "Next steps:"\necho " insta log…' to contain 'insta build .'
× buildReport > no Dockerfile: nixpacks detects, generates, and the report carries the plan + content
AssertionError: expected 'deployable' to be 'needs-attention'
× buildReport > a nixpacks-only dir is never blessed as deployable — the dockerfile check names the lane split
AssertionError: expected 'pass' to be 'fail'
× renderReport > prints plan lines with rationale, check marks, and the verdict
AssertionError: expected 'plan for /var/folders/…' to contain 'verdict: needs-attention'
× renderReport > the builder line itself carries the lane caveat — "builder: nixpacks" alone reads as a promise
AssertionError: expected ' builder: nixpacks (providers: node)' to contain 'needs a Dockerfile'
FAIL test/deploy.test.ts [ test/deploy.test.ts ] <-- noDockerfileMessage does not exist on main
Test Files 3 failed (3)
Tests 6 failed | 24 passed (30)
And passes on this branch:
Test Files 3 passed (3)
Tests 39 passed (39)
Coverage includes the negative shapes, not just the success one: a user Dockerfile still passes outright and keeps a clean builder line; the no-Dockerfile-and-no-nixpacks case keeps its critical failure and exit 1; and two assertions forbid the "save the generated Dockerfile" advice from creeping back into either message.
Mirror PR (AGENTS.md non-negotiable #4)
The agent-facing reference lives in the separate InsForge/insta-skills repo, not here. Its insta build row carried the exact old promise this PR retires ("the Dockerfile that would be used (yours, or nixpacks-generated …)"), and references/deploy.md said "no buildpacks yet". Both are updated to the new truth in InsForge/insta-skills#58 — insta build verdict semantics (nixpacks-only → needs-attention, not deployable), insta deploy <dir> requires a Dockerfile in the dir with the three options named, the GitHub lane as the home of nixpacks, and the do-not-save-the---explain-Dockerfile warning. No other commands touched there. Merge the two together; neither should land without the other.
Gates (re-run after rebasing onto main @ a5c918a, post-#144)
| Gate | Result |
|---|---|
npm run typecheck | clean |
npm run build | clean |
sh -n install.sh | clean |
npm test | 517 passed, 0 failed (40 files) |
(The 5 setup-agent/ensure-skills failures present at the first push were pre-existing on the old main baseline and are gone after the rebase — not touched by this PR.)
Files touched
src/commands/build.ts, src/commands/deploy.ts, src/index.ts (one line: the build command description), install.sh (the banner block), test/build.test.ts, test/deploy.test.ts, test/install-banner.test.ts (new).
🤖 Generated with Claude Code