Skip to content

fix(cli/compute): write fly.toml stub on fresh apps + roll back on build failure#91

Merged
tonychang04 merged 2 commits intomainfrom
fix/compute-deploy-fly-toml-stub-and-rollback
Apr 29, 2026
Merged

fix(cli/compute): write fly.toml stub on fresh apps + roll back on build failure#91
tonychang04 merged 2 commits intomainfrom
fix/compute-deploy-fly-toml-stub-and-rollback

Conversation

@tonychang04
Copy link
Copy Markdown
Contributor

@tonychang04 tonychang04 commented Apr 29, 2026

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

$ npx @insforge/cli compute deploy . --name my-api --port 8080
Detected Dockerfile at .../Dockerfile
Creating service "my-api"...
Created Fly app my-api-<projectId>
Requesting deploy token...
Building & pushing on Fly remote builder...
==> Verifying app config
Error: failed to grab app config from existing machines, error: could not create a fly.toml from any machines :-(
No machines configured for this app
Error: flyctl deploy --build-only failed (exit 1).

flyctl deploy --remote-only derives 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 minimal fly.toml (app, primary_region, internal_port + http_service block) before invoking flyctl, and removes it on exit. If the user already has a fly.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, status deploying. The user had to manually compute delete it before retrying.

Fix: deploy.ts catches 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:

  1. User-provided fly.toml (advanced) — fully respected; CLI never touches it
  2. Auto-stub (default) — written transparently for the deploy duration, deleted after; user never sees it

Telling users to write their own fly.toml would 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):

  • Stub written when no fly.toml exists, with correct app / primary_region / internal_port
  • Stub left alone when user-provided fly.toml exists
  • Stub cleaned up on success, non-zero exit, AND spawn error

Live e2e on staging today (api-beta.insforge.dev cloud v1.2.11-compute2 + OSS v2.1.3-compute):

Scenario Result
Fresh dir, only Dockerfile + index.html, no fly.toml ✓ deployed [running], live HTTP 200, no leftover fly.toml after deploy
Redeploy to existing service (in-place update) ✓ updated [running], no rollback
Broken Dockerfile (RUN false) ✓ flyctl exits 1 → Rolled back service "broken-..." after build failure → 0 zombies in compute list

Diff stats

src/commands/compute/deploy.ts  | +20  (rollback path on freshly-created service)
src/lib/flyctl.ts               | +50  (ensureFlyTomlStub + plumb region/port)
src/lib/flyctl.test.ts          | +75 (new file: 16 tests, +5 vs the WIP)
package.json                    | 0.1.60 → 0.1.61
package-lock.json               | sync

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved deployment resilience: failed remote build/push now triggers rollback for freshly created services and emits success/failure messages; existing services skip rollback.
    • Build/push failures continue to surface so deployments still fail when appropriate.
  • New Features

    • Deploy now respects explicit region and port settings and creates a minimal config stub when missing (auto-cleaned up).
  • Tests

    • Added thorough tests covering CLI availability, build/push behavior, and stub lifecycle.

…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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3676927-11d1-4ea3-8306-efc5108dd986

📥 Commits

Reviewing files that changed from the base of the PR and between f8f664e and ccc32de.

📒 Files selected for processing (1)
  • src/commands/compute/deploy.ts

Walkthrough

Adds a package version bump and updates Fly.io deploy handling: flyctl build/push now receives region and port, a minimal fly.toml stub is created/cleaned when missing, deploy rollback is attempted on build failures for newly created services, and tests for flyctl helpers are added.

Changes

Cohort / File(s) Summary
Version Bump
package.json
Bumps @insforge/cli version from 0.1.600.1.61.
Deploy flow & rollback
src/commands/compute/deploy.ts
Wraps flyctlBuildAndPush with guarded error handling; on failure for newly-created services attempts to delete the service row (rollback); passes region and port into the build/push call.
Flyctl helpers & tests
src/lib/flyctl.ts, src/lib/flyctl.test.ts
Adds region and port to FlyctlBuildPushOptions; creates a minimal fly.toml stub when absent and ensures cleanup on success or error; new comprehensive Vitest suite mocks child processes and filesystem to cover ensure/flyctl behaviors, digest extraction, stream forwarding, and stub lifecycle.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • jwfing
  • Fermionic-Lyu

Poem

🐰 I stitched a tiny fly.toml by the moon,
Gave region and port a cheerful tune,
If build trips up, I hop and mend,
Rollback a nibble, tests to the end,
Hooray—deploys hop on, safe and soon!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding a fly.toml stub for fresh apps and implementing rollback on build failure. It is concise, specific, and directly reflects the core fixes in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/compute-deploy-fly-toml-stub-and-rollback

Review rate limit: 3/5 reviews remaining, refill in 19 minutes and 14 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@jwfing jwfing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, approved.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/flyctl.test.ts (1)

294-318: Restore the stream spies in finally.

If flyctlBuildAndPush rejects or an assertion fails before the explicit mockRestore() 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

📥 Commits

Reviewing files that changed from the base of the PR and between fe06648 and f8f664e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • src/commands/compute/deploy.ts
  • src/lib/flyctl.test.ts
  • src/lib/flyctl.ts

Comment thread src/commands/compute/deploy.ts
@tonychang04 tonychang04 merged commit 7f879a7 into main Apr 29, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants