fix(cli/compute): write fly.toml stub on fresh apps + roll back on build failure#91
Conversation
…ild failure
Two bugs in 0.1.60 source-mode deploy that block first-time users:
1. **Missing fly.toml stub.** flyctl on a freshly-created Fly app with zero
machines errors with "could not create a fly.toml from any machines :-(".
The src/lib/flyctl.ts comment claimed "flyctl will invent a fly.toml if
none exists" — that was wrong. flyctl only invents one by introspecting
existing machines, of which a brand-new app has zero.
Fix: ensureFlyTomlStub() writes a minimal stub (app/primary_region/
internal_port + http_service block) into the user's directory before
spawning flyctl, and removes it on exit (success OR failure OR spawn
error). If the user already has a fly.toml, leave it untouched —
advanced users get full override.
2. **Zombie service on build failure.** Before the fix, a flyctl build error
left a half-created service (DB row + empty Fly app, machineId null)
that the user had to manually `compute delete`. Reproducible 100% on the
first deploy because of bug #1.
Fix: deploy.ts catches build failure on a freshly-created service
(existing === undefined) and DELETEs the service row through the OSS
proxy — which destroys the Fly app too. On redeploy of an existing
service we don't roll back: the running machine should survive a
transient build error.
flyctlBuildAndPush signature gains required region and port for the stub.
New tests cover stub creation, the user-fly.toml-respected path, and
stub cleanup on every exit path (success / non-zero exit / spawn error).
Bumped to 0.1.61.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughAdds a package version bump and updates Fly.io deploy handling: flyctl build/push now receives region and port, a minimal Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Review rate limit: 3/5 reviews remaining, refill in 19 minutes and 14 seconds. Comment |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/lib/flyctl.test.ts (1)
294-318: Restore the stream spies infinally.If
flyctlBuildAndPushrejects or an assertion fails before the explicitmockRestore()calls, these spies leak into later tests and can cascade failures.♻️ Proposed fix
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); - setImmediate(() => { - child.stdout!.emit('data', Buffer.from('build step 1/3\n')); - child.stderr!.emit('data', Buffer.from('warn: deprecated flag\n')); - child.stdout!.emit( - 'data', - Buffer.from('pushing manifest for registry.fly.io/foo@sha256:' + 'e'.repeat(64) + '\n') - ); - child.emit('exit', 0); - }); - - await flyctlBuildAndPush(baseOpts); - - expect(stdoutSpy).toHaveBeenCalledWith('build step 1/3\n'); - expect(stderrSpy).toHaveBeenCalledWith('warn: deprecated flag\n'); - - stdoutSpy.mockRestore(); - stderrSpy.mockRestore(); + try { + setImmediate(() => { + child.stdout!.emit('data', Buffer.from('build step 1/3\n')); + child.stderr!.emit('data', Buffer.from('warn: deprecated flag\n')); + child.stdout!.emit( + 'data', + Buffer.from('pushing manifest for registry.fly.io/foo@sha256:' + 'e'.repeat(64) + '\n') + ); + child.emit('exit', 0); + }); + + await flyctlBuildAndPush(baseOpts); + + expect(stdoutSpy).toHaveBeenCalledWith('build step 1/3\n'); + expect(stderrSpy).toHaveBeenCalledWith('warn: deprecated flag\n'); + } finally { + stdoutSpy.mockRestore(); + stderrSpy.mockRestore(); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/flyctl.test.ts` around lines 294 - 318, The test creates process.stdout/stderr spies (stdoutSpy, stderrSpy) but restores them only at the end, so failures or rejections leak mocks; update the test 'tees child stdout/stderr to process streams (live progress visibility)' to ensure stdoutSpy.mockRestore() and stderrSpy.mockRestore() are always called by moving the restore calls into a finally block (or use a try/finally around the await flyctlBuildAndPush(baseOpts) and subsequent assertions) so the spies are restored even if flyctlBuildAndPush or assertions fail; reference the stdoutSpy, stderrSpy, makeFakeChild, spawnMock, and flyctlBuildAndPush identifiers when applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/compute/deploy.ts`:
- Around line 223-229: In the catch block in deploy.ts change the single-line
conditional to a braced block: wrap the existing if (!json) outputInfo(...) call
in braces so the catch handler uses a proper block; specifically update the
catch surrounding the call to outputInfo that references serviceId to use { ...
} around the if body to satisfy the curly rule.
---
Nitpick comments:
In `@src/lib/flyctl.test.ts`:
- Around line 294-318: The test creates process.stdout/stderr spies (stdoutSpy,
stderrSpy) but restores them only at the end, so failures or rejections leak
mocks; update the test 'tees child stdout/stderr to process streams (live
progress visibility)' to ensure stdoutSpy.mockRestore() and
stderrSpy.mockRestore() are always called by moving the restore calls into a
finally block (or use a try/finally around the await
flyctlBuildAndPush(baseOpts) and subsequent assertions) so the spies are
restored even if flyctlBuildAndPush or assertions fail; reference the stdoutSpy,
stderrSpy, makeFakeChild, spawnMock, and flyctlBuildAndPush identifiers when
applying the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e0a63852-8d03-4f89-a058-9b6538b284de
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (4)
package.jsonsrc/commands/compute/deploy.tssrc/lib/flyctl.test.tssrc/lib/flyctl.ts
Summary
Fixes two source-mode deploy bugs in 0.1.60 that block first-time users (closes the issue caught while testing 0.1.60 today). Bumps to 0.1.61.
Bug 1: Missing fly.toml stub → 100% failure on first deploy
flyctl deploy --remote-onlyderives app config from existing machines; on a brand-new Fly app there are zero machines to derive from, so flyctl errors out. The old code comment claimed "flyctl will invent a fly.toml if none exists" — that was wrong.Fix:
ensureFlyTomlStub()writes a minimalfly.toml(app,primary_region,internal_port+http_serviceblock) before invoking flyctl, and removes it on exit. If the user already has afly.toml, we leave it alone — power users keep full override.Bug 2: Zombie service on build failure
Before the fix, any build failure (which was 100% on first deploy because of Bug 1) left a half-created service: DB row + empty Fly app,
flyMachineId: null, statusdeploying. The user had to manuallycompute deleteit before retrying.Fix:
deploy.tscatches build failure on a freshly-created service and DELETEs the row through the OSS proxy (which also destroys the Fly app). For redeploys of an existing service we don't roll back — the running machine should survive a transient build error.Design choice: auto-stub vs. user-provided fly.toml
The fix gives both, in priority order:
fly.toml(advanced) — fully respected; CLI never touches itTelling users to write their own
fly.tomlwould push a flyctl-specific quirk into the user's workflow when they're just trying to ship a Dockerfile. The stub is a 30-line workaround in the CLI — invisible to 95% of users, while leaving the door open for the 5% who need custom fly.toml config.Test plan
Unit tests (
src/lib/flyctl.test.ts, +5 new tests, 16/16 pass):app/primary_region/internal_portLive e2e on staging today (
api-beta.insforge.devcloudv1.2.11-compute2+ OSSv2.1.3-compute):[running], live HTTP 200, no leftoverfly.tomlafter deploy[running], no rollbackRUN false)Rolled back service "broken-..." after build failure→ 0 zombies incompute listDiff stats
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
New Features
Tests