feat(deploy): source deploys against a local daemon — 501 falls back to docker build - #106
Conversation
…back to docker build `insta deploy <dir>` was cloud-only: it mints a Fly deploy token, which insta-oss answers with 501, and the command dead-ended. But a local daemon deploys from the SAME docker this shell uses — so on 501 the CLI now builds the directory locally (`docker build -t insta-src-<proj8>-<group>:<ts> .`) and hands the daemon the tag. No registry, no push, no flyctl (ensureFlyctl moved after the token mint so the local path never auto-installs it). Same workflow both targets, per the parity rule: one deploy script now runs unchanged on cloud and oss. Cloud behavior is byte-for-byte unchanged; any non-501 token error still surfaces loudly. buildFromSource is exported with the repo's injectable-runner pattern (no global mocks). Verified: 371/371 tests (4 new: tag shape, docker invocation + failure surfacing, the 501 fallback, non-501 re-throw), typecheck; live end-to-end — `insta deploy .` against instad built nginx:alpine locally and the daemon served the source-built content.
jwfing
left a comment
There was a problem hiding this comment.
Review — feat(deploy): source deploys against a local daemon (501 → docker build fallback)
Summary: A tight, well-tested change that restores one-script deploy parity by falling back to a local docker build when the deploy-token mint answers 501; cloud behavior is preserved and the fallback is correctly gated on ApiError.status === 501.
Requirements context
No matching spec/plan found — this repo has no docs/superpowers/ (nor any docs/ tree at all), so I assessed against the PR description, the referenced insta-oss "clean-501 sweep" parity work, and the surrounding code. The stated intent (source deploys must run unchanged against both cloud and a local insta-oss daemon) is well-defined and matches the implementation.
Verification I did
- Confirmed
ApiClient.rawRequestthrowsApiError(status, …)for any status ≥ 400 (src/api.ts:57-60,src/api.ts:7-8), andApiError.statusis public — so thee instanceof ApiError && e.status !== 501guard (src/commands/deploy.ts:97) is sound and won't mis-classify network errors as the 501 path. - Confirmed
BuildRunnerusesspawn(cmd, args, …)with an argv array and no shell (src/flyctl-build.ts), so the injecteddocker buildinvocation is not shell-interpolated. - Confirmed the injected
runis threaded through to bothdockerBuildLocalandflyctlBuildAndPushand defaults todefaultBuildRunner; the publicdeploy()caller passes no runner, so production uses the real spawner. - Confirmed EXPOSE-port defaulting still happens in
deploy()(src/commands/deploy.ts:43-51) ahead ofbuildFromSource, so both paths keep it.
Findings
Critical
(none)
Suggestion
- Functionality / ops — local source images accumulate, never pruned (
src/commands/deploy.ts:63-65,:98). Each local build gets a uniqueinsta-src-<proj8>-<group>:<timestamp>tag, so repeatedinsta deployruns against a daemon leave a growing pile of danglinginsta-src-*images on the host — the local equivalent of a slow disk leak. The comment "unique per build so a redeploy replaces" (:61-62) is also slightly inaccurate: a fresh timestamped tag does not replace the previous one; it adds a new one (the old tag lingers as<none>once overwritten only if the tag string collides, which it won't). Consider pruning priorinsta-src-<proj8>-<group>:*tags after a successful deploy, or reusing a stable tag per (project, group), and tightening the comment.
Information
- Software engineering / type-safety (
src/commands/deploy.ts:91). Switchingconst tok = await api.rawRequest(...)to a barelet tokinside the try drops theRawResulttype the destructure at:105relied on (tok.bodyis now effectively untyped). Annotatinglet tok: RawResult(orAwaited<ReturnType<ApiClient['rawRequest']>>) restores the compile-time check ontoken/flyAppwithout changing behavior. - Functionality — cross-repo contract (
src/commands/deploy.ts:98-102,:54-55). The fallback returns a bare local tag (no registry prefix) that is then POSTed to/projects/:id/deploy; correctness depends on insta-oss resolving a local docker image tag rather than attempting a registry pull. The PR states this was validated e2e — noting the coupling so it stays on the radar if either side changes. - Edge case — group → tag validity (
src/commands/deploy.ts:64,:98). A--groupcontaining docker-tag-illegal characters (uppercase,/, etc.) would surface only as adocker buildfailure rather than an earlier, clearer validation error. Low blast radius; left as-is is fine, but a note in the error path could help.
Security
No security-relevant regressions. The new subprocess uses argv (no shell injection surface); --group/projectId reach docker only as argv elements. No secrets are logged or added to responses; process.env is passed to the docker subprocess (standard, needed for DOCKER_HOST etc.) but not injected as build args. Auth/approval is unchanged — approval is still enforced at the /deploy step (src/commands/deploy.ts:56). No new dependencies.
Performance
No concerns. One extra local docker build occurs only on the 501 path; no new N+1 queries, hot-path work, or blocking I/O on shared paths.
Test coverage
Good — 4 focused tests (test/deploy-local-build.test.ts) cover tag shape (incl. the group ?? 'default' branch), docker invocation argv, build-failure surfacing, the 501 fallback, and non-501 rethrow, all via the injectable runner with no global mocks — consistent with the repo's DI test pattern (flyctl-build.test.ts, deploy.test.ts).
Verdict
approved (informational — no Critical findings; the GitHub green-check remains a separate human action). The Suggestion (image pruning) and Information items are non-blocking.
Why
insta deploy <dir>was cloud-only: it mints a Fly deploy token, which insta-oss answers with a 501, and the command dead-ended — breaking the parity rule that one deploy script runs unchanged on both targets (insta-oss roadmap Phase 4).What
docker build -t insta-src-<proj8>-<group>:<timestamp> .— and hands the daemon the tag. The daemon deploys from the same docker this shell uses, so no registry, no push, no flyctl.ensureFlyctl()moved after the token mint: the local path never auto-installs a third-party CLI it doesn't need.EXPOSEport defaulting applies to both paths as before.buildFromSourceexported with the repo's injectable-runner pattern (no global mocks).Verification
typecheckclean.insta deploy .against a real instad — CLI fell back, builtnginx:alpine-based source locally, daemon served the source-built content atlocalhost:80.🤖 Generated with Claude Code
Summary by cubic
Enables source deploys to a local daemon by falling back to a local Docker build when
POST /projects/:id/deploy-tokenreturns 501, restoring one-script parity across cloud and OSS. Cloud behavior is unchanged; only the 501 path differs.docker build -t insta-src-<proj8>-<group>:<timestamp> .and pass the tag to the daemon; uses the host Docker only (no registry, no push, noflyctl).ensureFlyctl()after the token mint so the local path never installsflyctl.buildFromSource,dockerBuildLocal, andlocalImageTag; builds accept an injectableBuildRunner(matches repo DI pattern).EXPOSEdefaulting unchanged.Written for commit 8b01d71. Summary will update on new commits.