Repository: superfly/flyctl
Pull request #3592 (2024-06-04 11:16:00)

Outputting errors in a muted color and then letting them scroll by
Outputting the errors then the plan is less likely to be noticed than showing the plan then the error in a more "in your face" manner.

Current Rails failures over the past 7 days:

| Count | Error |
| --- | --- |
| 40 | launch can not continue with errors present |
| 40 | We need your payment information to continue! Add a credit card or buy credit: `https://fly.io/dashboard/` |
| 24 | interrupt |
| 14 | Your account has been marked as high risk. Please go to `https://fly.io/high-risk-unlock` to verify your account. |
| 7 | Not Found |

Current user experience:

<img width="581" alt="image" src="https://github.com/superfly/flyctl/assets/4815/d9bcd3b3-cc06-4efa-9069-00ca035d7a70">

---

Proposed new user experience:

<img width="577" alt="image" src="https://github.com/superfly/flyctl/assets/4815/72ba0536-0de3-465d-8168-844cdf928a54">



Patch:
diff --git a/agent/client.go b/agent/client.go
index 899ea9b3c..bb3d890bd 100644
--- a/agent/client.go
+++ b/agent/client.go
@@ -279,10 +279,10 @@ type EstablishResponse struct {
 	TunnelConfig   *wg.Config
 }
 
-func (c *Client) doEstablish(ctx context.Context, slug string, reestablish bool, network string) (res *EstablishResponse, err error) {
+func (c *Client) doEstablish(ctx context.Context, slug string, recycle bool, network string) (res *EstablishResponse, err error) {
 	err = c.do(ctx, func(conn net.Conn) (err error) {
 		verb := "establish"
-		if reestablish {
+		if recycle {
 			verb = "reestablish"
 		}
 
diff --git a/agent/server/server.go b/agent/server/server.go
index 34b9c47b7..21672008b 100644
--- a/agent/server/server.go
+++ b/agent/server/server.go
@@ -224,20 +224,20 @@ func (s *server) checkForConfigChange() (err error) {
 	return
 }
 
-func (s *server) buildTunnel(ctx context.Context, org *fly.Organization, reestablish bool, network string, client flyutil.Client) (tunnel *wg.Tunnel, err error) {
+func (s *server) buildTunnel(ctx context.Context, org *fly.Organization, recycle bool, network string, client flyutil.Client) (tunnel *wg.Tunnel, err error) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
 	tk := tunnelKey{orgSlug: org.Slug, networkName: network}
 
 	// not checking the region is intentional, it's static during the lifetime of the agent
-	if tunnel = s.tunnels[tk]; tunnel != nil && !reestablish {
+	if tunnel = s.tunnels[tk]; tunnel != nil && !recycle {
 		// tunnel already exists
 		return
 	}
 
 	var state *wg.WireGuardState
-	if state, err = wireguard.StateForOrg(ctx, client, org, os.Getenv("FLY_AGENT_WG_REGION"), "", reestablish, network); err != nil {
+	if state, err = wireguard.StateForOrg(ctx, client, org, os.Getenv("FLY_AGENT_WG_REGION"), "", recycle, network); err != nil {
 		return
 	}
 
diff --git a/flypg/launcher.go b/flypg/launcher.go
index 2c03d90d3..c60be1382 100644
--- a/flypg/launcher.go
+++ b/flypg/launcher.go
@@ -423,11 +423,8 @@ func (l *Launcher) setSecrets(ctx context.Context, config *CreateClusterInput) (
 			return nil, err
 		}
 
-		// 100 years in hours
-		validHours := 876600
-
 		app := fly.App{Name: config.AppName}
-		cert, err := l.client.IssueSSHCertificate(ctx, config.Organization, []string{"root", "fly", "postgres"}, []string{app.Name}, &validHours, pub)
+		cert, err := l.client.IssueSSHCertificate(ctx, config.Organization, []string{"root", "fly", "postgres"}, []string{app.Name}, nil, pub)
 		if err != nil {
 			return nil, err
 		}
diff --git a/go.mod b/go.mod
index 0cbf05989..268481639 100644
--- a/go.mod
+++ b/go.mod
@@ -25,14 +25,14 @@ require (
 	github.com/docker/go-units v0.5.0
 	github.com/dustin/go-humanize v1.0.1
 	github.com/ejcx/sshcert v1.1.0
-	github.com/getsentry/sentry-go v0.28.0
+	github.com/getsentry/sentry-go v0.27.0
 	github.com/go-logr/logr v1.4.2
 	github.com/gofrs/flock v0.8.1
 	github.com/google/go-cmp v0.6.0
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/haileys/go-harlog v0.0.0-20230517070437-0f99204b5a57
 	github.com/hashicorp/go-multierror v1.1.1
-	github.com/hashicorp/go-version v1.7.0
+	github.com/hashicorp/go-version v1.6.0
 	github.com/heroku/heroku-go/v5 v5.5.0
 	github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3
 	github.com/itchyny/json2yaml v0.1.4
@@ -62,9 +62,9 @@ require (
 	github.com/sourcegraph/conc v0.3.0
 	github.com/spf13/cobra v1.8.0
 	github.com/spf13/pflag v1.0.5
-	github.com/spf13/viper v1.19.0
+	github.com/spf13/viper v1.18.2
 	github.com/stretchr/testify v1.9.0
-	github.com/superfly/fly-go v0.1.13
+	github.com/superfly/fly-go v0.1.12
 	github.com/superfly/graphql v0.2.4
 	github.com/superfly/lfsc-go v0.1.1
 	github.com/superfly/macaroon v0.2.13
@@ -186,7 +186,7 @@ require (
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
-	github.com/klauspost/compress v1.17.7 // indirect
+	github.com/klauspost/compress v1.17.4 // indirect
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
@@ -235,7 +235,7 @@ require (
 	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
 	go.opentelemetry.io/otel/metric v1.27.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.2.0 // indirect
diff --git a/go.sum b/go.sum
index 60e5ca45d..e10ea90da 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,9 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
+cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
+cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
 dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
@@ -174,6 +179,8 @@ github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUK
 github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
 github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc=
+github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
 github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
@@ -248,6 +255,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
+github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
 github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
@@ -263,8 +272,8 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo
 github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
 github.com/gdamore/tcell/v2 v2.7.0 h1:I5LiGTQuwrysAt1KS9wg1yFfOI3arI3ucFrxtd/xqaA=
 github.com/gdamore/tcell/v2 v2.7.0/go.mod h1:hl/KtAANGBecfIPxk+FzKvThTqI84oplgbPEmVX60b8=
-github.com/getsentry/sentry-go v0.28.0 h1:7Rqx9M3ythTKy2J6uZLHmc8Sz9OGgIlseuO1iBX/s0M=
-github.com/getsentry/sentry-go v0.28.0/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
+github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
+github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
 github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
 github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@@ -345,8 +354,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
-github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
+github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -389,8 +398,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
-github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
+github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@@ -586,8 +595,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
 github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -607,8 +616,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/superfly/fly-go v0.1.13 h1:cb69Yg29fdaye8Q0MmXrnKSX115V+Uv2dQrHUlSj1Sg=
-github.com/superfly/fly-go v0.1.13/go.mod h1:fopkB+iOiuZHNcukZjzZx31e4JL7hn6yE+FRasD3m2I=
+github.com/superfly/fly-go v0.1.12 h1:Ql+wbDFuiWEGK+qskuSTtMft/zfh1rIBp/1Dcu7JbgQ=
+github.com/superfly/fly-go v0.1.12/go.mod h1:0wgmVsqPPDgPSTsZA0L0zI5uajxx86KfuBoPQT87B1E=
 github.com/superfly/graphql v0.2.4 h1:Av8hSk4x8WvKJ6MTnEwrLknSVSGPc7DWpgT3z/kt3PU=
 github.com/superfly/graphql v0.2.4/go.mod h1:CVfDl31srm8HnJ9udwLu6hFNUW/P6GUM2dKcG1YQ8jc=
 github.com/superfly/lfsc-go v0.1.1 h1:dGjLgt81D09cG+aR9lJZIdmonjZSR5zYCi7s54+ZU2Q=
@@ -649,8 +658,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
 go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
@@ -729,6 +738,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/gql/generated.go b/gql/generated.go
index 01d866342..b057a8494 100644
--- a/gql/generated.go
+++ b/gql/generated.go
@@ -555,6 +555,181 @@ type AppDataSecretsSecret struct {
 // GetName returns AppDataSecretsSecret.Name, and is useful for accessing the field via an interface.
 func (v *AppDataSecretsSecret) GetName() string { return v.Name }
 
+type BuildFinalImageInput struct {
+	// Sha256 id of docker image
+	Id string `json:"id"`
+	// Size in bytes of the docker image
+	SizeBytes int64 `json:"sizeBytes"`
+	// Tag used for docker image
+	Tag string `json:"tag"`
+}
+
+// GetId returns BuildFinalImageInput.Id, and is useful for accessing the field via an interface.
+func (v *BuildFinalImageInput) GetId() string { return v.Id }
+
+// GetSizeBytes returns BuildFinalImageInput.SizeBytes, and is useful for accessing the field via an interface.
+func (v *BuildFinalImageInput) GetSizeBytes() int64 { return v.SizeBytes }
+
+// GetTag returns BuildFinalImageInput.Tag, and is useful for accessing the field via an interface.
+func (v *BuildFinalImageInput) GetTag() string { return v.Tag }
+
+type BuildImageOptsInput struct {
+	// Set of build time variables passed to cli
+	BuildArgs interface{} `json:"buildArgs"`
+	// Fly.toml build.buildpacks setting
+	BuildPacks []string `json:"buildPacks"`
+	// Fly.toml build.builder setting
+	Builder string `json:"builder"`
+	// Builtin builder to use
+	BuiltIn string `json:"builtIn"`
+	// Builtin builder settings
+	BuiltInSettings interface{} `json:"builtInSettings"`
+	// Path to dockerfile, if one exists
+	DockerfilePath string `json:"dockerfilePath"`
+	// Unused in cli?
+	ExtraBuildArgs interface{} `json:"extraBuildArgs"`
+	// Image label to use when tagging and pushing to the fly registry
+	ImageLabel string `json:"imageLabel"`
+	// Unused in cli?
+	ImageRef string `json:"imageRef"`
+	// Do not use the build cache when building the image
+	NoCache bool `json:"noCache"`
+	// Whether publishing to the registry was requested
+	Publish bool `json:"publish"`
+	// Docker tag used to publish image to registry
+	Tag string `json:"tag"`
+	// Set the target build stage to build if the Dockerfile has more than one stage
+	Target string `json:"target"`
+}
+
+// GetBuildArgs returns BuildImageOptsInput.BuildArgs, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetBuildArgs() interface{} { return v.BuildArgs }
+
+// GetBuildPacks returns BuildImageOptsInput.BuildPacks, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetBuildPacks() []string { return v.BuildPacks }
+
+// GetBuilder returns BuildImageOptsInput.Builder, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetBuilder() string { return v.Builder }
+
+// GetBuiltIn returns BuildImageOptsInput.BuiltIn, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetBuiltIn() string { return v.BuiltIn }
+
+// GetBuiltInSettings returns BuildImageOptsInput.BuiltInSettings, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetBuiltInSettings() interface{} { return v.BuiltInSettings }
+
+// GetDockerfilePath returns BuildImageOptsInput.DockerfilePath, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetDockerfilePath() string { return v.DockerfilePath }
+
+// GetExtraBuildArgs returns BuildImageOptsInput.ExtraBuildArgs, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetExtraBuildArgs() interface{} { return v.ExtraBuildArgs }
+
+// GetImageLabel returns BuildImageOptsInput.ImageLabel, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetImageLabel() string { return v.ImageLabel }
+
+// GetImageRef returns BuildImageOptsInput.ImageRef, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetImageRef() string { return v.ImageRef }
+
+// GetNoCache returns BuildImageOptsInput.NoCache, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetNoCache() bool { return v.NoCache }
+
+// GetPublish returns BuildImageOptsInput.Publish, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetPublish() bool { return v.Publish }
+
+// GetTag returns BuildImageOptsInput.Tag, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetTag() string { return v.Tag }
+
+// GetTarget returns BuildImageOptsInput.Target, and is useful for accessing the field via an interface.
+func (v *BuildImageOptsInput) GetTarget() string { return v.Target }
+
+type BuildStrategyAttemptInput struct {
+	// Optional error message from strategy
+	Error string `json:"error"`
+	// Optional note about this strategy or its result
+	Note string `json:"note"`
+	// Result attempting this strategy
+	Result string `json:"result"`
+	// Build strategy attempted
+	Strategy string `json:"strategy"`
+}
+
+// GetError returns BuildStrategyAttemptInput.Error, and is useful for accessing the field via an interface.
+func (v *BuildStrategyAttemptInput) GetError() string { return v.Error }
+
+// GetNote returns BuildStrategyAttemptInput.Note, and is useful for accessing the field via an interface.
+func (v *BuildStrategyAttemptInput) GetNote() string { return v.Note }
+
+// GetResult returns BuildStrategyAttemptInput.Result, and is useful for accessing the field via an interface.
+func (v *BuildStrategyAttemptInput) GetResult() string { return v.Result }
+
+// GetStrategy returns BuildStrategyAttemptInput.Strategy, and is useful for accessing the field via an interface.
+func (v *BuildStrategyAttemptInput) GetStrategy() string { return v.Strategy }
+
+type BuildTimingsInput struct {
+	// Time to build and push the image, measured by flyctl
+	BuildAndPushMs int64 `json:"buildAndPushMs"`
+	// Time to build the image including create context, measured by flyctl
+	BuildMs int64 `json:"buildMs"`
+	// Time to initialize client used to connect to either remote or local builder
+	BuilderInitMs int64 `json:"builderInitMs"`
+	// Time to create the build context tar file, measured by flyctl
+	ContextBuildMs int64 `json:"contextBuildMs"`
+	// Time for builder to build image after receiving context, measured by flyctl
+	ImageBuildMs int64 `json:"imageBuildMs"`
+	// Time to push completed image to registry, measured by flyctl
+	PushMs int64 `json:"pushMs"`
+}
+
+// GetBuildAndPushMs returns BuildTimingsInput.BuildAndPushMs, and is useful for accessing the field via an interface.
+func (v *BuildTimingsInput) GetBuildAndPushMs() int64 { return v.BuildAndPushMs }
+
+// GetBuildMs returns BuildTimingsInput.BuildMs, and is useful for accessing the field via an interface.
+func (v *BuildTimingsInput) GetBuildMs() int64 { return v.BuildMs }
+
+// GetBuilderInitMs returns BuildTimingsInput.BuilderInitMs, and is useful for accessing the field via an interface.
+func (v *BuildTimingsInput) GetBuilderInitMs() int64 { return v.BuilderInitMs }
+
+// GetContextBuildMs returns BuildTimingsInput.ContextBuildMs, and is useful for accessing the field via an interface.
+func (v *BuildTimingsInput) GetContextBuildMs() int64 { return v.ContextBuildMs }
+
+// GetImageBuildMs returns BuildTimingsInput.ImageBuildMs, and is useful for accessing the field via an interface.
+func (v *BuildTimingsInput) GetImageBuildMs() int64 { return v.ImageBuildMs }
+
+// GetPushMs returns BuildTimingsInput.PushMs, and is useful for accessing the field via an interface.
+func (v *BuildTimingsInput) GetPushMs() int64 { return v.PushMs }
+
+type BuilderMetaInput struct {
+	// Local or remote builder type
+	BuilderType string `json:"builderType"`
+	// Whther or not buildkit is enabled on builder
+	BuildkitEnabled bool `json:"buildkitEnabled"`
+	// Docker version reported by builder
+	DockerVersion string `json:"dockerVersion"`
+	// Platform reported by the builder
+	Platform string `json:"platform"`
+	// Remote builder app used
+	RemoteAppName string `json:"remoteAppName"`
+	// Remote builder machine used
+	RemoteMachineId string `json:"remoteMachineId"`
+}
+
+// GetBuilderType returns BuilderMetaInput.BuilderType, and is useful for accessing the field via an interface.
+func (v *BuilderMetaInput) GetBuilderType() string { return v.BuilderType }
+
+// GetBuildkitEnabled returns BuilderMetaInput.BuildkitEnabled, and is useful for accessing the field via an interface.
+func (v *BuilderMetaInput) GetBuildkitEnabled() bool { return v.BuildkitEnabled }
+
+// GetDockerVersion returns BuilderMetaInput.DockerVersion, and is useful for accessing the field via an interface.
+func (v *BuilderMetaInput) GetDockerVersion() string { return v.DockerVersion }
+
+// GetPlatform returns BuilderMetaInput.Platform, and is useful for accessing the field via an interface.
+func (v *BuilderMetaInput) GetPlatform() string { return v.Platform }
+
+// GetRemoteAppName returns BuilderMetaInput.RemoteAppName, and is useful for accessing the field via an interface.
+func (v *BuilderMetaInput) GetRemoteAppName() string { return v.RemoteAppName }
+
+// GetRemoteMachineId returns BuilderMetaInput.RemoteMachineId, and is useful for accessing the field via an interface.
+func (v *BuilderMetaInput) GetRemoteMachineId() string { return v.RemoteMachineId }
+
 // CreateAddOnCreateAddOnCreateAddOnPayload includes the requested fields of the GraphQL type CreateAddOnPayload.
 // The GraphQL type's documentation follows.
 //
@@ -859,6 +1034,40 @@ type CreateAppResponse struct {
 // GetCreateApp returns CreateAppResponse.CreateApp, and is useful for accessing the field via an interface.
 func (v *CreateAppResponse) GetCreateApp() CreateAppCreateAppCreateAppPayload { return v.CreateApp }
 
+// Autogenerated input type of CreateBuild
+type CreateBuildInput struct {
+	// The name of the app being built
+	AppName string `json:"appName"`
+	// Whether builder is remote or local
+	BuilderType string `json:"builderType"`
+	// A unique identifier for the client performing the mutation.
+	ClientMutationId string `json:"clientMutationId"`
+	// Options set for building image
+	ImageOpts BuildImageOptsInput `json:"imageOpts"`
+	// The ID of the machine being built (only set for machine builds)
+	MachineId string `json:"machineId"`
+	// List of available build strategies that will be attempted
+	StrategiesAvailable []string `json:"strategiesAvailable"`
+}
+
+// GetAppName returns CreateBuildInput.AppName, and is useful for accessing the field via an interface.
+func (v *CreateBuildInput) GetAppName() string { return v.AppName }
+
+// GetBuilderType returns CreateBuildInput.BuilderType, and is useful for accessing the field via an interface.
+func (v *CreateBuildInput) GetBuilderType() string { return v.BuilderType }
+
+// GetClientMutationId returns CreateBuildInput.ClientMutationId, and is useful for accessing the field via an interface.
+func (v *CreateBuildInput) GetClientMutationId() string { return v.ClientMutationId }
+
+// GetImageOpts returns CreateBuildInput.ImageOpts, and is useful for accessing the field via an interface.
+func (v *CreateBuildInput) GetImageOpts() BuildImageOptsInput { return v.ImageOpts }
+
+// GetMachineId returns CreateBuildInput.MachineId, and is useful for accessing the field via an interface.
+func (v *CreateBuildInput) GetMachineId() string { return v.MachineId }
+
+// GetStrategiesAvailable returns CreateBuildInput.StrategiesAvailable, and is useful for accessing the field via an interface.
+func (v *CreateBuildInput) GetStrategiesAvailable() []string { return v.StrategiesAvailable }
+
 // CreateExtensionCreateAddOnCreateAddOnPayload includes the requested fields of the GraphQL type CreateAddOnPayload.
 // The GraphQL type's documentation follows.
 //
@@ -993,6 +1202,40 @@ func (v *CreateLimitedAccessTokenResponse) GetCreateLimitedAccessToken() CreateL
 	return v.CreateLimitedAccessToken
 }
 
+// Autogenerated input type of CreateRelease
+type CreateReleaseInput struct {
+	// The ID of the app
+	AppId string `json:"appId"`
+	// A unique identifier for the client performing the mutation.
+	ClientMutationId string `json:"clientMutationId"`
+	// app definition
+	Definition interface{} `json:"definition"`
+	// The image to deploy
+	Image string `json:"image"`
+	// nomad or machines
+	PlatformVersion string `json:"platformVersion"`
+	// The strategy for replacing existing instances. Defaults to canary.
+	Strategy DeploymentStrategy `json:"strategy"`
+}
+
+// GetAppId returns CreateReleaseInput.AppId, and is useful for accessing the field via an interface.
+func (v *CreateReleaseInput) GetAppId() string { return v.AppId }
+
+// GetClientMutationId returns CreateReleaseInput.ClientMutationId, and is useful for accessing the field via an interface.
+func (v *CreateReleaseInput) GetClientMutationId() string { return v.ClientMutationId }
+
+// GetDefinition returns CreateReleaseInput.Definition, and is useful for accessing the field via an interface.
+func (v *CreateReleaseInput) GetDefinition() interface{} { return v.Definition }
+
+// GetImage returns CreateReleaseInput.Image, and is useful for accessing the field via an interface.
+func (v *CreateReleaseInput) GetImage() string { return v.Image }
+
+// GetPlatformVersion returns CreateReleaseInput.PlatformVersion, and is useful for accessing the field via an interface.
+func (v *CreateReleaseInput) GetPlatformVersion() string { return v.PlatformVersion }
+
+// GetStrategy returns CreateReleaseInput.Strategy, and is useful for accessing the field via an interface.
+func (v *CreateReleaseInput) GetStrategy() DeploymentStrategy { return v.Strategy }
+
 // CreateTosAgreementCreateExtensionTosAgreementCreateExtensionTosAgreementPayload includes the requested fields of the GraphQL type CreateExtensionTosAgreementPayload.
 // The GraphQL type's documentation follows.
 //
@@ -1040,6 +1283,23 @@ func (v *DeleteAddOnResponse) GetDeleteAddOn() DeleteAddOnDeleteAddOnDeleteAddOn
 	return v.DeleteAddOn
 }
 
+type DeploymentStrategy string
+
+const (
+	// Launch all new instances before shutting down previous instances
+	DeploymentStrategyBluegreen DeploymentStrategy = "BLUEGREEN"
+	// Ensure new instances are healthy before continuing with a rolling deployment
+	DeploymentStrategyCanary DeploymentStrategy = "CANARY"
+	// Deploy new instances all at once
+	DeploymentStrategyImmediate DeploymentStrategy = "IMMEDIATE"
+	// Incrementally replace old instances with new ones
+	DeploymentStrategyRolling DeploymentStrategy = "ROLLING"
+	// Incrementally replace old instances with new ones, 1 by 1
+	DeploymentStrategyRollingOne DeploymentStrategy = "ROLLING_ONE"
+	// Deploy new instances all at once
+	DeploymentStrategySimple DeploymentStrategy = "SIMPLE"
+)
+
 // ExtensionData includes the GraphQL fields of AddOn requested by the fragment ExtensionData.
 type ExtensionData struct {
 	// The service name according to the provider
@@ -1149,6 +1409,62 @@ type ExtensionProviderDataExcludedRegionsRegion struct {
 // GetCode returns ExtensionProviderDataExcludedRegionsRegion.Code, and is useful for accessing the field via an interface.
 func (v *ExtensionProviderDataExcludedRegionsRegion) GetCode() string { return v.Code }
 
+// Autogenerated input type of FinishBuild
+type FinishBuildInput struct {
+	// The name of the app being built
+	AppName string `json:"appName"`
+	// Build id returned by createBuild() mutation
+	BuildId string `json:"buildId"`
+	// Metadata about the builder
+	BuilderMeta BuilderMetaInput `json:"builderMeta"`
+	// A unique identifier for the client performing the mutation.
+	ClientMutationId string `json:"clientMutationId"`
+	// Information about the docker image that was built
+	FinalImage BuildFinalImageInput `json:"finalImage"`
+	// Log or error output
+	Logs string `json:"logs"`
+	// The ID of the machine being built (only set for machine builds)
+	MachineId string `json:"machineId"`
+	// Indicate whether build completed or failed
+	Status string `json:"status"`
+	// Build strategies attempted and their result, should be in order of attempt
+	StrategiesAttempted []BuildStrategyAttemptInput `json:"strategiesAttempted"`
+	// Timings for different phases of the build
+	Timings BuildTimingsInput `json:"timings"`
+}
+
+// GetAppName returns FinishBuildInput.AppName, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetAppName() string { return v.AppName }
+
+// GetBuildId returns FinishBuildInput.BuildId, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetBuildId() string { return v.BuildId }
+
+// GetBuilderMeta returns FinishBuildInput.BuilderMeta, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetBuilderMeta() BuilderMetaInput { return v.BuilderMeta }
+
+// GetClientMutationId returns FinishBuildInput.ClientMutationId, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetClientMutationId() string { return v.ClientMutationId }
+
+// GetFinalImage returns FinishBuildInput.FinalImage, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetFinalImage() BuildFinalImageInput { return v.FinalImage }
+
+// GetLogs returns FinishBuildInput.Logs, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetLogs() string { return v.Logs }
+
+// GetMachineId returns FinishBuildInput.MachineId, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetMachineId() string { return v.MachineId }
+
+// GetStatus returns FinishBuildInput.Status, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetStatus() string { return v.Status }
+
+// GetStrategiesAttempted returns FinishBuildInput.StrategiesAttempted, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetStrategiesAttempted() []BuildStrategyAttemptInput {
+	return v.StrategiesAttempted
+}
+
+// GetTimings returns FinishBuildInput.Timings, and is useful for accessing the field via an interface.
+func (v *FinishBuildInput) GetTimings() BuildTimingsInput { return v.Timings }
+
 // FlyctlConfigCurrentReleaseApp includes the requested fields of the GraphQL type App.
 type FlyctlConfigCurrentReleaseApp struct {
 	// The latest release of this application, without any config processing
@@ -1179,6 +1495,47 @@ type FlyctlConfigCurrentReleaseResponse struct {
 // GetApp returns FlyctlConfigCurrentReleaseResponse.App, and is useful for accessing the field via an interface.
 func (v *FlyctlConfigCurrentReleaseResponse) GetApp() FlyctlConfigCurrentReleaseApp { return v.App }
 
+// FlyctlDeployGetLatestImageApp includes the requested fields of the GraphQL type App.
+type FlyctlDeployGetLatestImageApp struct {
+	// The latest release of this application, without any config processing
+	CurrentReleaseUnprocessed FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed `json:"currentReleaseUnprocessed"`
+}
+
+// GetCurrentReleaseUnprocessed returns FlyctlDeployGetLatestImageApp.CurrentReleaseUnprocessed, and is useful for accessing the field via an interface.
+func (v *FlyctlDeployGetLatestImageApp) GetCurrentReleaseUnprocessed() FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed {
+	return v.CurrentReleaseUnprocessed
+}
+
+// FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed includes the requested fields of the GraphQL type ReleaseUnprocessed.
+type FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed struct {
+	// Unique ID
+	Id string `json:"id"`
+	// The version of the release
+	Version int `json:"version"`
+	// Docker image URI
+	ImageRef string `json:"imageRef"`
+}
+
+// GetId returns FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed.Id, and is useful for accessing the field via an interface.
+func (v *FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed) GetId() string { return v.Id }
+
+// GetVersion returns FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed.Version, and is useful for accessing the field via an interface.
+func (v *FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed) GetVersion() int { return v.Version }
+
+// GetImageRef returns FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed.ImageRef, and is useful for accessing the field via an interface.
+func (v *FlyctlDeployGetLatestImageAppCurrentReleaseUnprocessed) GetImageRef() string {
+	return v.ImageRef
+}
+
+// FlyctlDeployGetLatestImageResponse is returned by FlyctlDeployGetLatestImage on success.
+type FlyctlDeployGetLatestImageResponse struct {
+	// Find an app by name
+	App FlyctlDeployGetLatestImageApp `json:"app"`
+}
+
+// GetApp returns FlyctlDeployGetLatestImageResponse.App, and is useful for accessing the field via an interface.
+func (v *FlyctlDeployGetLatestImageResponse) GetApp() FlyctlDeployGetLatestImageApp { return v.App }
+
 // GetAddOnAddOn includes the requested fields of the GraphQL type AddOn.
 type GetAddOnAddOn struct {
 	AddOnData `json:"-"`
@@ -2557,6 +2914,77 @@ type LogOutResponse struct {
 // GetLogOut returns LogOutResponse.LogOut, and is useful for accessing the field via an interface.
 func (v *LogOutResponse) GetLogOut() LogOutLogOutLogOutPayload { return v.LogOut }
 
+// MachinesCreateReleaseCreateReleaseCreateReleasePayload includes the requested fields of the GraphQL type CreateReleasePayload.
+// The GraphQL type's documentation follows.
+//
+// Autogenerated return type of CreateRelease.
+type MachinesCreateReleaseCreateReleaseCreateReleasePayload struct {
+	Release MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease `json:"release"`
+}
+
+// GetRelease returns MachinesCreateReleaseCreateReleaseCreateReleasePayload.Release, and is useful for accessing the field via an interface.
+func (v *MachinesCreateReleaseCreateReleaseCreateReleasePayload) GetRelease() MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease {
+	return v.Release
+}
+
+// MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease includes the requested fields of the GraphQL type Release.
+type MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease struct {
+	// Unique ID
+	Id string `json:"id"`
+	// The version of the release
+	Version int `json:"version"`
+}
+
+// GetId returns MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease.Id, and is useful for accessing the field via an interface.
+func (v *MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease) GetId() string { return v.Id }
+
+// GetVersion returns MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease.Version, and is useful for accessing the field via an interface.
+func (v *MachinesCreateReleaseCreateReleaseCreateReleasePayloadRelease) GetVersion() int {
+	return v.Version
+}
+
+// MachinesCreateReleaseResponse is returned by MachinesCreateRelease on success.
+type MachinesCreateReleaseResponse struct {
+	CreateRelease MachinesCreateReleaseCreateReleaseCreateReleasePayload `json:"createRelease"`
+}
+
+// GetCreateRelease returns MachinesCreateReleaseResponse.CreateRelease, and is useful for accessing the field via an interface.
+func (v *MachinesCreateReleaseResponse) GetCreateRelease() MachinesCreateReleaseCreateReleaseCreateReleasePayload {
+	return v.CreateRelease
+}
+
+// MachinesUpdateReleaseResponse is returned by MachinesUpdateRelease on success.
+type MachinesUpdateReleaseResponse struct {
+	UpdateRelease MachinesUpdateReleaseUpdateReleaseUpdateReleasePayload `json:"updateRelease"`
+}
+
+// GetUpdateRelease returns MachinesUpdateReleaseResponse.UpdateRelease, and is useful for accessing the field via an interface.
+func (v *MachinesUpdateReleaseResponse) GetUpdateRelease() MachinesUpdateReleaseUpdateReleaseUpdateReleasePayload {
+	return v.UpdateRelease
+}
+
+// MachinesUpdateReleaseUpdateReleaseUpdateReleasePayload includes the requested fields of the GraphQL type UpdateReleasePayload.
+// The GraphQL type's documentation follows.
+//
+// Autogenerated return type of UpdateRelease.
+type MachinesUpdateReleaseUpdateReleaseUpdateReleasePayload struct {
+	Release MachinesUpdateReleaseUpdateReleaseUpdateReleasePayloadRelease `json:"release"`
+}
+
+// GetRelease returns MachinesUpdateReleaseUpdateReleaseUpdateReleasePayload.Release, and is useful for accessing the field via an interface.
+func (v *MachinesUpdateReleaseUpdateReleaseUpdateReleasePayload) GetRelease() MachinesUpdateReleaseUpdateReleaseUpdateReleasePayloadRelease {
+	return v.Release
+}
+
+// MachinesUpdateReleaseUpdateReleaseUpdateReleasePayloadRelease includes the requested fields of the GraphQL type Release.
+type MachinesUpdateReleaseUpdateReleaseUpdateReleasePayloadRelease struct {
+	// Unique ID
+	Id string `json:"id"`
+}
+
+// GetId returns MachinesUpdateReleaseUpdateReleaseUpdateReleasePayloadRelease.Id, and is useful for accessing the field via an interface.
+func (v *MachinesUpdateReleaseUpdateReleaseUpdateReleasePayloadRelease) GetId() string { return v.Id }
+
 // OrganizationData includes the GraphQL fields of Organization requested by the fragment OrganizationData.
 type OrganizationData struct {
 	Id string `json:"id"`
@@ -2634,6 +3062,67 @@ func (v *ResetAddOnPasswordResponse) GetResetAddOnPassword() ResetAddOnPasswordR
 	return v.ResetAddOnPassword
 }
 
+// ResolverCreateBuildCreateBuildCreateBuildPayload includes the requested fields of the GraphQL type CreateBuildPayload.
+// The GraphQL type's documentation follows.
+//
+// Autogenerated return type of CreateBuild.
+type ResolverCreateBuildCreateBuildCreateBuildPayload struct {
+	// build id
+	Id string `json:"id"`
+	// stored build status
+	Status string `json:"status"`
+}
+
+// GetId returns ResolverCreateBuildCreateBuildCreateBuildPayload.Id, and is useful for accessing the field via an interface.
+func (v *ResolverCreateBuildCreateBuildCreateBuildPayload) GetId() string { return v.Id }
+
+// GetStatus returns ResolverCreateBuildCreateBuildCreateBuildPayload.Status, and is useful for accessing the field via an interface.
+func (v *ResolverCreateBuildCreateBuildCreateBuildPayload) GetStatus() string { return v.Status }
+
+// ResolverCreateBuildResponse is returned by ResolverCreateBuild on success.
+type ResolverCreateBuildResponse struct {
+	CreateBuild ResolverCreateBuildCreateBuildCreateBuildPayload `json:"createBuild"`
+}
+
+// GetCreateBuild returns ResolverCreateBuildResponse.CreateBuild, and is useful for accessing the field via an interface.
+func (v *ResolverCreateBuildResponse) GetCreateBuild() ResolverCreateBuildCreateBuildCreateBuildPayload {
+	return v.CreateBuild
+}
+
+// ResolverFinishBuildFinishBuildFinishBuildPayload includes the requested fields of the GraphQL type FinishBuildPayload.
+// The GraphQL type's documentation follows.
+//
+// Autogenerated return type of FinishBuild.
+type ResolverFinishBuildFinishBuildFinishBuildPayload struct {
+	// build id
+	Id string `json:"id"`
+	// stored build status
+	Status string `json:"status"`
+	// wall clock time for this build
+	WallclockTimeMs int `json:"wallclockTimeMs"`
+}
+
+// GetId returns ResolverFinishBuildFinishBuildFinishBuildPayload.Id, and is useful for accessing the field via an interface.
+func (v *ResolverFinishBuildFinishBuildFinishBuildPayload) GetId() string { return v.Id }
+
+// GetStatus returns ResolverFinishBuildFinishBuildFinishBuildPayload.Status, and is useful for accessing the field via an interface.
+func (v *ResolverFinishBuildFinishBuildFinishBuildPayload) GetStatus() string { return v.Status }
+
+// GetWallclockTimeMs returns ResolverFinishBuildFinishBuildFinishBuildPayload.WallclockTimeMs, and is useful for accessing the field via an interface.
+func (v *ResolverFinishBuildFinishBuildFinishBuildPayload) GetWallclockTimeMs() int {
+	return v.WallclockTimeMs
+}
+
+// ResolverFinishBuildResponse is returned by ResolverFinishBuild on success.
+type ResolverFinishBuildResponse struct {
+	FinishBuild ResolverFinishBuildFinishBuildFinishBuildPayload `json:"finishBuild"`
+}
+
+// GetFinishBuild returns ResolverFinishBuildResponse.FinishBuild, and is useful for accessing the field via an interface.
+func (v *ResolverFinishBuildResponse) GetFinishBuild() ResolverFinishBuildFinishBuildFinishBuildPayload {
+	return v.FinishBuild
+}
+
 type RuntimeType string
 
 const (
@@ -2861,6 +3350,25 @@ type UpdateAddOnUpdateAddOnUpdateAddOnPayloadAddOn struct {
 // GetId returns UpdateAddOnUpdateAddOnUpdateAddOnPayloadAddOn.Id, and is useful for accessing the field via an interface.
 func (v *UpdateAddOnUpdateAddOnUpdateAddOnPayloadAddOn) GetId() string { return v.Id }
 
+// Autogenerated input type of UpdateRelease
+type UpdateReleaseInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationId string `json:"clientMutationId"`
+	// The ID of the release
+	ReleaseId string `json:"releaseId"`
+	// The new status for the release
+	Status string `json:"status"`
+}
+
+// GetClientMutationId returns UpdateReleaseInput.ClientMutationId, and is useful for accessing the field via an interface.
+func (v *UpdateReleaseInput) GetClientMutationId() string { return v.ClientMutationId }
+
+// GetReleaseId returns UpdateReleaseInput.ReleaseId, and is useful for accessing the field via an interface.
+func (v *UpdateReleaseInput) GetReleaseId() string { return v.ReleaseId }
+
+// GetStatus returns UpdateReleaseInput.Status, and is useful for accessing the field via an interface.
+func (v *UpdateReleaseInput) GetStatus() string { return v.Status }
+
 type VMCountInput struct {
 	// The desired count
 	Count int `json:"count"`
@@ -2975,18 +3483,22 @@ type __FlyctlConfigCurrentReleaseInput struct {
 // GetAppName returns __FlyctlConfigCurrentReleaseInput.AppName, and is useful for accessing the field via an interface.
 func (v *__FlyctlConfigCurrentReleaseInput) GetAppName() string { return v.AppName }
 
+// __FlyctlDeployGetLatestImageInput is used internally by genqlient
+type __FlyctlDeployGetLatestImageInput struct {
+	AppName string `json:"appName"`
+}
+
+// GetAppName returns __FlyctlDeployGetLatestImageInput.AppName, and is useful for accessing the field via an interface.
+func (v *__FlyctlDeployGetLatestImageInput) GetAppName() string { return v.AppName }
+
 // __GetAddOnInput is used internally by genqlient
 type __GetAddOnInput struct {
-	Name     string `json:"name"`
-	Provider string `json:"provider"`
+	Name string `json:"name"`
 }
 
 // GetName returns __GetAddOnInput.Name, and is useful for accessing the field via an interface.
 func (v *__GetAddOnInput) GetName() string { return v.Name }
 
-// GetProvider returns __GetAddOnInput.Provider, and is useful for accessing the field via an interface.
-func (v *__GetAddOnInput) GetProvider() string { return v.Provider }
-
 // __GetAddOnProviderInput is used internally by genqlient
 type __GetAddOnProviderInput struct {
 	Name string `json:"name"`
@@ -3063,6 +3575,22 @@ type __ListAddOnsInput struct {
 // GetAddOnType returns __ListAddOnsInput.AddOnType, and is useful for accessing the field via an interface.
 func (v *__ListAddOnsInput) GetAddOnType() AddOnType { return v.AddOnType }
 
+// __MachinesCreateReleaseInput is used internally by genqlient
+type __MachinesCreateReleaseInput struct {
+	Input CreateReleaseInput `json:"input"`
+}
+
+// GetInput returns __MachinesCreateReleaseInput.Input, and is useful for accessing the field via an interface.
+func (v *__MachinesCreateReleaseInput) GetInput() CreateReleaseInput { return v.Input }
+
+// __MachinesUpdateReleaseInput is used internally by genqlient
+type __MachinesUpdateReleaseInput struct {
+	Input UpdateReleaseInput `json:"input"`
+}
+
+// GetInput returns __MachinesUpdateReleaseInput.Input, and is useful for accessing the field via an interface.
+func (v *__MachinesUpdateReleaseInput) GetInput() UpdateReleaseInput { return v.Input }
+
 // __ResetAddOnPasswordInput is used internally by genqlient
 type __ResetAddOnPasswordInput struct {
 	Name string `json:"name"`
@@ -3071,6 +3599,22 @@ type __ResetAddOnPasswordInput struct {
 // GetName returns __ResetAddOnPasswordInput.Name, and is useful for accessing the field via an interface.
 func (v *__ResetAddOnPasswordInput) GetName() string { return v.Name }
 
+// __ResolverCreateBuildInput is used internally by genqlient
+type __ResolverCreateBuildInput struct {
+	Input CreateBuildInput `json:"input"`
+}
+
+// GetInput returns __ResolverCreateBuildInput.Input, and is useful for accessing the field via an interface.
+func (v *__ResolverCreateBuildInput) GetInput() CreateBuildInput { return v.Input }
+
+// __ResolverFinishBuildInput is used internally by genqlient
+type __ResolverFinishBuildInput struct {
+	Input FinishBuildInput `json:"input"`
+}
+
+// GetInput returns __ResolverFinishBuildInput.Input, and is useful for accessing the field via an interface.
+func (v *__ResolverFinishBuildInput) GetInput() FinishBuildInput { return v.Input }
+
 // __SetNomadVMCountInput is used internally by genqlient
 type __SetNomadVMCountInput struct {
 	Input SetVMCountInput `json:"input"`
@@ -3543,10 +4087,49 @@ func FlyctlConfigCurrentRelease(
 	return &data_, err_
 }
 
+// The query or mutation executed by FlyctlDeployGetLatestImage.
+const FlyctlDeployGetLatestImage_Operation = `
+query FlyctlDeployGetLatestImage ($appName: String!) {
+	app(name: $appName) {
+		currentReleaseUnprocessed {
+			id
+			version
+			imageRef
+		}
+	}
+}
+`
+
+func FlyctlDeployGetLatestImage(
+	ctx_ context.Context,
+	client_ graphql.Client,
+	appName string,
+) (*FlyctlDeployGetLatestImageResponse, error) {
+	req_ := &graphql.Request{
+		OpName: "FlyctlDeployGetLatestImage",
+		Query:  FlyctlDeployGetLatestImage_Operation,
+		Variables: &__FlyctlDeployGetLatestImageInput{
+			AppName: appName,
+		},
+	}
+	var err_ error
+
+	var data_ FlyctlDeployGetLatestImageResponse
+	resp_ := &graphql.Response{Data: &data_}
+
+	err_ = client_.MakeRequest(
+		ctx_,
+		req_,
+		resp_,
+	)
+
+	return &data_, err_
+}
+
 // The query or mutation executed by GetAddOn.
 const GetAddOn_Operation = `
-query GetAddOn ($name: String, $provider: String) {
-	addOn(name: $name, provider: $provider) {
+query GetAddOn ($name: String) {
+	addOn(name: $name) {
 		... AddOnData
 		publicUrl
 		privateIp
@@ -3630,14 +4213,12 @@ func GetAddOn(
 	ctx_ context.Context,
 	client_ graphql.Client,
 	name string,
-	provider string,
 ) (*GetAddOnResponse, error) {
 	req_ := &graphql.Request{
 		OpName: "GetAddOn",
 		Query:  GetAddOn_Operation,
 		Variables: &__GetAddOnInput{
-			Name:     name,
-			Provider: provider,
+			Name: name,
 		},
 	}
 	var err_ error
@@ -4130,6 +4711,81 @@ func LogOut(
 	return &data_, err_
 }
 
+// The query or mutation executed by MachinesCreateRelease.
+const MachinesCreateRelease_Operation = `
+mutation MachinesCreateRelease ($input: CreateReleaseInput!) {
+	createRelease(input: $input) {
+		release {
+			id
+			version
+		}
+	}
+}
+`
+
+func MachinesCreateRelease(
+	ctx_ context.Context,
+	client_ graphql.Client,
+	input CreateReleaseInput,
+) (*MachinesCreateReleaseResponse, error) {
+	req_ := &graphql.Request{
+		OpName: "MachinesCreateRelease",
+		Query:  MachinesCreateRelease_Operation,
+		Variables: &__MachinesCreateReleaseInput{
+			Input: input,
+		},
+	}
+	var err_ error
+
+	var data_ MachinesCreateReleaseResponse
+	resp_ := &graphql.Response{Data: &data_}
+
+	err_ = client_.MakeRequest(
+		ctx_,
+		req_,
+		resp_,
+	)
+
+	return &data_, err_
+}
+
+// The query or mutation executed by MachinesUpdateRelease.
+const MachinesUpdateRelease_Operation = `
+mutation MachinesUpdateRelease ($input: UpdateReleaseInput!) {
+	updateRelease(input: $input) {
+		release {
+			id
+		}
+	}
+}
+`
+
+func MachinesUpdateRelease(
+	ctx_ context.Context,
+	client_ graphql.Client,
+	input UpdateReleaseInput,
+) (*MachinesUpdateReleaseResponse, error) {
+	req_ := &graphql.Request{
+		OpName: "MachinesUpdateRelease",
+		Query:  MachinesUpdateRelease_Operation,
+		Variables: &__MachinesUpdateReleaseInput{
+			Input: input,
+		},
+	}
+	var err_ error
+
+	var data_ MachinesUpdateReleaseResponse
+	resp_ := &graphql.Response{Data: &data_}
+
+	err_ = client_.MakeRequest(
+		ctx_,
+		req_,
+		resp_,
+	)
+
+	return &data_, err_
+}
+
 // The query or mutation executed by ResetAddOnPassword.
 const ResetAddOnPassword_Operation = `
 mutation ResetAddOnPassword ($name: String!) {
@@ -4167,6 +4823,79 @@ func ResetAddOnPassword(
 	return &data_, err_
 }
 
+// The query or mutation executed by ResolverCreateBuild.
+const ResolverCreateBuild_Operation = `
+mutation ResolverCreateBuild ($input: CreateBuildInput!) {
+	createBuild(input: $input) {
+		id
+		status
+	}
+}
+`
+
+func ResolverCreateBuild(
+	ctx_ context.Context,
+	client_ graphql.Client,
+	input CreateBuildInput,
+) (*ResolverCreateBuildResponse, error) {
+	req_ := &graphql.Request{
+		OpName: "ResolverCreateBuild",
+		Query:  ResolverCreateBuild_Operation,
+		Variables: &__ResolverCreateBuildInput{
+			Input: input,
+		},
+	}
+	var err_ error
+
+	var data_ ResolverCreateBuildResponse
+	resp_ := &graphql.Response{Data: &data_}
+
+	err_ = client_.MakeRequest(
+		ctx_,
+		req_,
+		resp_,
+	)
+
+	return &data_, err_
+}
+
+// The query or mutation executed by ResolverFinishBuild.
+const ResolverFinishBuild_Operation = `
+mutation ResolverFinishBuild ($input: FinishBuildInput!) {
+	finishBuild(input: $input) {
+		id
+		status
+		wallclockTimeMs
+	}
+}
+`
+
+func ResolverFinishBuild(
+	ctx_ context.Context,
+	client_ graphql.Client,
+	input FinishBuildInput,
+) (*ResolverFinishBuildResponse, error) {
+	req_ := &graphql.Request{
+		OpName: "ResolverFinishBuild",
+		Query:  ResolverFinishBuild_Operation,
+		Variables: &__ResolverFinishBuildInput{
+			Input: input,
+		},
+	}
+	var err_ error
+
+	var data_ ResolverFinishBuildResponse
+	resp_ := &graphql.Response{Data: &data_}
+
+	err_ = client_.MakeRequest(
+		ctx_,
+		req_,
+		resp_,
+	)
+
+	return &data_, err_
+}
+
 // The query or mutation executed by SetNomadVMCount.
 const SetNomadVMCount_Operation = `
 mutation SetNomadVMCount ($input: SetVMCountInput!) {
diff --git a/gql/genqclient.graphql b/gql/genqclient.graphql
index a439a3a4f..ae852ee29 100644
--- a/gql/genqclient.graphql
+++ b/gql/genqclient.graphql
@@ -8,8 +8,8 @@ fragment AddOnData on AddOn {
 	options
 }
 
-query GetAddOn($name: String, $provider: String) {
-	addOn(name: $name, provider: $provider) {
+query GetAddOn($name: String) {
+	addOn(name: $name) {
 		...AddOnData
 		publicUrl
 		privateIp
diff --git a/gql/schema.graphql b/gql/schema.graphql
index d7a419071..b40e9ecce 100644
--- a/gql/schema.graphql
+++ b/gql/schema.graphql
@@ -8277,7 +8277,7 @@ type Queries {
   """
   Find an add-on by ID or name
   """
-  addOn(id: ID, name: String, provider: String): AddOn
+  addOn(id: ID, name: String): AddOn
 
   """
   List add-on service plans
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go
index 8a257e3d2..3e4cbdc51 100644
--- a/internal/appconfig/config.go
+++ b/internal/appconfig/config.go
@@ -349,10 +349,3 @@ func (cfg *Config) MergeFiles(files []*fly.File) error {
 
 	return nil
 }
-
-func (cfg *Config) DeployStrategy() string {
-	if cfg.Deploy == nil {
-		return ""
-	}
-	return cfg.Deploy.Strategy
-}
diff --git a/internal/build/imgsrc/resolver.go b/internal/build/imgsrc/resolver.go
index 04d0e818d..c23992346 100644
--- a/internal/build/imgsrc/resolver.go
+++ b/internal/build/imgsrc/resolver.go
@@ -18,8 +18,8 @@ import (
 	"go.opentelemetry.io/otel/trace"
 
 	dockerclient "github.com/docker/docker/client"
-	fly "github.com/superfly/fly-go"
 	"github.com/superfly/flyctl/agent"
+	"github.com/superfly/flyctl/gql"
 	"github.com/superfly/flyctl/internal/buildinfo"
 	"github.com/superfly/flyctl/internal/config"
 	"github.com/superfly/flyctl/internal/flyutil"
@@ -276,7 +276,7 @@ func (r *Resolver) createImageBuild(ctx context.Context, strategies []imageResol
 	for _, r := range strategies {
 		strategiesAvailable = append(strategiesAvailable, r.Name())
 	}
-	imageOps := &fly.BuildImageOptsInput{
+	imageOps := &gql.BuildImageOptsInput{
 		ImageLabel: opts.ImageLabel,
 		ImageRef:   opts.ImageRef,
 		Publish:    opts.Publish,
@@ -290,7 +290,7 @@ func (r *Resolver) createBuild(ctx context.Context, strategies []imageBuilder, o
 	for _, s := range strategies {
 		strategiesAvailable = append(strategiesAvailable, s.Name())
 	}
-	imageOpts := &fly.BuildImageOptsInput{
+	imageOpts := &gql.BuildImageOptsInput{
 		BuildArgs:       opts.BuildArgs,
 		BuildPacks:      opts.Buildpacks,
 		Builder:         opts.Builder,
@@ -308,24 +308,31 @@ func (r *Resolver) createBuild(ctx context.Context, strategies []imageBuilder, o
 	return r.createBuildGql(ctx, strategiesAvailable, imageOpts)
 }
 
-func (r *Resolver) createBuildGql(ctx context.Context, strategiesAvailable []string, imageOpts *fly.BuildImageOptsInput) (*build, error) {
+func (r *Resolver) createBuildGql(ctx context.Context, strategiesAvailable []string, imageOpts *gql.BuildImageOptsInput) (*build, error) {
 	ctx, span := tracing.GetTracer().Start(ctx, "web.create_build")
 	defer span.End()
 
-	client := flyutil.ClientFromContext(ctx)
-
+	gqlClient := flyutil.ClientFromContext(ctx).GenqClient()
+	_ = `# @genqlient
+	mutation ResolverCreateBuild($input:CreateBuildInput!) {
+		createBuild(input:$input) {
+			id
+			status
+		}
+	}
+	`
 	builderType := "local"
 	if r.dockerFactory.remote {
 		builderType = "remote"
 	}
-	input := fly.CreateBuildInput{
+	input := gql.CreateBuildInput{
 		AppName:             r.dockerFactory.appName,
 		BuilderType:         builderType,
 		ImageOpts:           *imageOpts,
 		MachineId:           "",
 		StrategiesAvailable: strategiesAvailable,
 	}
-	resp, err := client.CreateBuild(ctx, input)
+	resp, err := gql.ResolverCreateBuild(ctx, gqlClient, input)
 	if err != nil {
 		var gqlErr *gqlerror.Error
 		isAppNotFoundErr := errors.As(err, &gqlErr) && gqlErr.Path.String() == "createBuild" && gqlErr.Message == "Could not find App"
@@ -363,10 +370,10 @@ func limitLogs(log string) string {
 type build struct {
 	CreateApiFailed bool
 	BuildId         string
-	BuilderMeta     *fly.BuilderMetaInput
-	StrategyResults []fly.BuildStrategyAttemptInput
-	Timings         *fly.BuildTimingsInput
-	StartTimes      *fly.BuildTimingsInput
+	BuilderMeta     *gql.BuilderMetaInput
+	StrategyResults []gql.BuildStrategyAttemptInput
+	Timings         *gql.BuildTimingsInput
+	StartTimes      *gql.BuildTimingsInput
 }
 
 func newFailedBuild() *build {
@@ -377,10 +384,10 @@ func newBuild(buildId string, createApiFailed bool) *build {
 	return &build{
 		CreateApiFailed: createApiFailed,
 		BuildId:         buildId,
-		BuilderMeta:     &fly.BuilderMetaInput{},
-		StrategyResults: make([]fly.BuildStrategyAttemptInput, 0),
-		StartTimes:      &fly.BuildTimingsInput{},
-		Timings: &fly.BuildTimingsInput{
+		BuilderMeta:     &gql.BuilderMetaInput{},
+		StrategyResults: make([]gql.BuildStrategyAttemptInput, 0),
+		StartTimes:      &gql.BuildTimingsInput{},
+		Timings: &gql.BuildTimingsInput{
 			BuildAndPushMs: -1,
 			BuilderInitMs:  -1,
 			BuildMs:        -1,
@@ -412,8 +419,8 @@ func (b *build) SetBuilderMetaPart2(buildkitEnabled bool, dockerVersion string,
 
 // call this at the start of each strategy to restart all the timers
 func (b *build) ResetTimings() {
-	b.StartTimes = &fly.BuildTimingsInput{}
-	b.Timings = &fly.BuildTimingsInput{
+	b.StartTimes = &gql.BuildTimingsInput{}
+	b.Timings = &gql.BuildTimingsInput{
 		BuildAndPushMs: -1,
 		BuilderInitMs:  -1,
 		BuildMs:        -1,
@@ -480,7 +487,7 @@ func (b *build) finishStrategyCommon(strategy string, failed bool, err error, no
 	if err != nil {
 		errMsg = err.Error()
 	}
-	r := fly.BuildStrategyAttemptInput{
+	r := gql.BuildStrategyAttemptInput{
 		Strategy: strategy,
 		Result:   result,
 		Error:    limitLogs(errMsg),
@@ -508,13 +515,21 @@ func (r *Resolver) finishBuild(ctx context.Context, build *build, failed bool, l
 		terminal.Debug("Skipping FinishBuild() gql call, because CreateBuild() failed.\n")
 		return nil, nil
 	}
-	client := flyutil.ClientFromContext(ctx)
-
+	gqlClient := flyutil.ClientFromContext(ctx).GenqClient()
+	_ = `# @genqlient
+	mutation ResolverFinishBuild($input:FinishBuildInput!) {
+		finishBuild(input:$input) {
+			id
+			status
+			wallclockTimeMs
+		}
+	}
+	`
 	status := "failed"
 	if !failed {
 		status = "completed"
 	}
-	input := fly.FinishBuildInput{
+	input := gql.FinishBuildInput{
 		BuildId:             build.BuildId,
 		AppName:             r.dockerFactory.appName,
 		MachineId:           "",
@@ -525,13 +540,13 @@ func (r *Resolver) finishBuild(ctx context.Context, build *build, failed bool, l
 		Timings:             *build.Timings,
 	}
 	if img != nil {
-		input.FinalImage = fly.BuildFinalImageInput{
+		input.FinalImage = gql.BuildFinalImageInput{
 			Id:        img.ID,
 			Tag:       img.Tag,
 			SizeBytes: img.Size,
 		}
 	}
-	resp, err := client.FinishBuild(ctx, input)
+	resp, err := gql.ResolverFinishBuild(ctx, gqlClient, input)
 	if err != nil {
 		terminal.Warnf("failed to finish build in graphql: %v\n", err)
 		sentry.CaptureException(err,
diff --git a/internal/command/apps/errors.go b/internal/command/apps/errors.go
index b87d80003..8da184db7 100644
--- a/internal/command/apps/errors.go
+++ b/internal/command/apps/errors.go
@@ -33,5 +33,5 @@ func RunDashboard(ctx context.Context) error {
 		return err
 	}
 
-	return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeSentry)
+	return extensions_core.OpenDashboard(ctx, extension.Name)
 }
diff --git a/internal/command/command.go b/internal/command/command.go
index b1021347c..4da2a9d4b 100644
--- a/internal/command/command.go
+++ b/internal/command/command.go
@@ -538,16 +538,11 @@ func RequireSession(ctx context.Context) (context.Context, error) {
 			}
 
 			// Reload the config
-			logger.FromContext(ctx).Debug("reloading config after login")
 			if ctx, err = prepare(ctx, preparers.LoadConfig); err != nil {
 				return nil, err
 			}
 
-			// first reset the client
-			ctx = flyutil.NewContextWithClient(ctx, nil)
-
 			// Re-run the auth preparers to update the client with the new token
-			logger.FromContext(ctx).Debug("re-running auth preparers after login")
 			if ctx, err = prepare(ctx, authPreparers...); err != nil {
 				return nil, err
 			}
diff --git a/internal/command/deploy/deploy.go b/internal/command/deploy/deploy.go
index 793792d1e..0ae145abb 100644
--- a/internal/command/deploy/deploy.go
+++ b/internal/command/deploy/deploy.go
@@ -13,7 +13,6 @@ import (
 	"github.com/superfly/fly-go/flaps"
 	"github.com/superfly/flyctl/internal/appconfig"
 	"github.com/superfly/flyctl/internal/build/imgsrc"
-	"github.com/superfly/flyctl/internal/buildinfo"
 	"github.com/superfly/flyctl/internal/cmdutil"
 	"github.com/superfly/flyctl/internal/command"
 	"github.com/superfly/flyctl/internal/config"
@@ -229,26 +228,13 @@ func (cmd *Command) run(ctx context.Context) (err error) {
 	ctx, span := tracing.CMDSpan(ctx, "cmd.deploy")
 	defer span.End()
 
-	// Instantiate FLAPS client if we haven't initialized one via a unit test.
-	startTime := time.Now()
-	var status metrics.DeployStatusPayload
-	metrics.Started(ctx, "deploy")
-
-	defer func() {
-		if err != nil {
-			status.Error = err.Error()
-		}
-		status.TraceID = span.SpanContext().TraceID().String()
-		status.Duration = time.Since(startTime)
-		metrics.DeployStatus(ctx, status)
-	}()
-
 	defer func() {
 		if err != nil {
 			tracing.RecordError(span, err, "error deploying")
 		}
 	}()
 
+	// Instantiate FLAPS client if we haven't initialized one via a unit test.
 	if flapsutil.ClientFromContext(ctx) == nil {
 		flapsClient, err := flapsutil.NewClientWithOptions(ctx, flaps.NewClientOpts{
 			AppName: appName,
@@ -374,25 +360,11 @@ func deployToMachines(
 	app *fly.AppCompact,
 	img *imgsrc.DeploymentImage,
 ) (err error) {
-	ctx, span := tracing.GetTracer().Start(ctx, "deploy_to_machines")
-	defer span.End()
 	// It's important to push appConfig into context because MachineDeployment will fetch it from there
 	ctx = appconfig.WithConfig(ctx, cfg)
 
-	startTime := time.Now()
-	var status metrics.DeployStatusPayload
-
-	metrics.Started(ctx, "deploy")
-	// TODO: remove this once there is nothing upstream using it
 	metrics.Started(ctx, "deploy_machines")
-
 	defer func() {
-		if err != nil {
-			status.Error = err.Error()
-		}
-		status.TraceID = span.SpanContext().TraceID().String()
-		status.Duration = time.Since(startTime)
-		metrics.DeployStatus(ctx, status)
 		metrics.Status(ctx, "deploy_machines", err == nil)
 	}()
 
@@ -464,16 +436,6 @@ func deployToMachines(
 		maxConcurrent = immediateMaxConcurrent
 	}
 
-	status.AppName = app.Name
-	status.OrgSlug = app.Organization.Slug
-	status.Image = img.Tag
-	status.Strategy = cfg.DeployStrategy()
-	if flag.GetString(ctx, "strategy") != "" {
-		status.Strategy = flag.GetString(ctx, "strategy")
-	}
-
-	status.FlyctlVersion = buildinfo.Info().Version.String()
-
 	md, err := NewMachineDeployment(ctx, MachineDeploymentArgs{
 		AppCompact:            app,
 		DeploymentImage:       img.Tag,
diff --git a/internal/command/deploy/machines.go b/internal/command/deploy/machines.go
index b8e2bb705..11bb680b9 100644
--- a/internal/command/deploy/machines.go
+++ b/internal/command/deploy/machines.go
@@ -9,12 +9,14 @@ import (
 	"sync"
 	"time"
 
+	"github.com/Khan/genqlient/graphql"
 	"github.com/google/shlex"
 	"github.com/logrusorgru/aurora"
 	"github.com/morikuni/aec"
 	"github.com/samber/lo"
 	fly "github.com/superfly/fly-go"
 	"github.com/superfly/fly-go/flaps"
+	"github.com/superfly/flyctl/gql"
 	"github.com/superfly/flyctl/internal/appconfig"
 	"github.com/superfly/flyctl/internal/buildinfo"
 	"github.com/superfly/flyctl/internal/cmdutil"
@@ -76,7 +78,8 @@ type MachineDeploymentArgs struct {
 
 type machineDeployment struct {
 	apiClient             flyutil.Client
-	flapsClient           flapsutil.FlapsClient
+	gqlClient             graphql.Client
+	flapsClient           *flaps.Client
 	io                    *iostreams.IOStreams
 	colorize              *iostreams.ColorScheme
 	app                   *fly.AppCompact
@@ -113,7 +116,7 @@ type machineDeployment struct {
 	volumeInitialSize     int
 }
 
-func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (_ MachineDeployment, err error) {
+func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (MachineDeployment, error) {
 	ctx, span := tracing.GetTracer().Start(ctx, "new_machines_deployment")
 	defer span.End()
 
@@ -139,17 +142,13 @@ func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (_ Ma
 	if args.AppCompact == nil {
 		return nil, fmt.Errorf("BUG: args.AppCompact should be set when calling this method")
 	}
-
-	flapsClient := flapsutil.ClientFromContext(ctx)
-	if flapsClient == nil {
-		flapsClient, err = flapsutil.NewClientWithOptions(ctx, flaps.NewClientOpts{
-			AppCompact: args.AppCompact,
-			AppName:    args.AppCompact.Name,
-		})
-		if err != nil {
-			tracing.RecordError(span, err, "failed to init flaps client")
-			return nil, err
-		}
+	flapsClient, err := flapsutil.NewClientWithOptions(ctx, flaps.NewClientOpts{
+		AppCompact: args.AppCompact,
+		AppName:    args.AppCompact.Name,
+	})
+	if err != nil {
+		tracing.RecordError(span, err, "failed to init flaps client")
+		return nil, err
 	}
 
 	if appConfig.Deploy != nil {
@@ -208,6 +207,7 @@ func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (_ Ma
 
 	md := &machineDeployment{
 		apiClient:             apiClient,
+		gqlClient:             apiClient.GenqClient(),
 		flapsClient:           flapsClient,
 		io:                    io,
 		colorize:              io.ColorScheme(),
@@ -509,7 +509,7 @@ func (md *machineDeployment) setImg(ctx context.Context) error {
 	if md.img != "" {
 		return nil
 	}
-	latestImg, err := md.apiClient.LatestImage(ctx, md.app.Name)
+	latestImg, err := md.latestImage(ctx)
 	if err == nil {
 		md.img = latestImg
 		return nil
@@ -521,6 +521,28 @@ func (md *machineDeployment) setImg(ctx context.Context) error {
 	return fmt.Errorf("could not find image to use for deployment; backend error was: %w", err)
 }
 
+func (md *machineDeployment) latestImage(ctx context.Context) (string, error) {
+	_ = `# @genqlient
+	       query FlyctlDeployGetLatestImage($appName:String!) {
+	               app(name:$appName) {
+	                       currentReleaseUnprocessed {
+	                               id
+	                               version
+	                               imageRef
+	                       }
+	               }
+	       }
+	      `
+	resp, err := gql.FlyctlDeployGetLatestImage(ctx, md.gqlClient, md.app.Name)
+	if err != nil {
+		return "", err
+	}
+	if resp.App.CurrentReleaseUnprocessed.ImageRef == "" {
+		return "", fmt.Errorf("current release not found for app %s", md.app.Name)
+	}
+	return resp.App.CurrentReleaseUnprocessed.ImageRef, nil
+}
+
 func (md *machineDeployment) setStrategy() error {
 	md.strategy = "rolling"
 	if md.appConfig.Deploy != nil && md.appConfig.Deploy.Strategy != "" {
@@ -533,13 +555,24 @@ func (md *machineDeployment) createReleaseInBackend(ctx context.Context) error {
 	ctx, span := tracing.GetTracer().Start(ctx, "create_backend_release")
 	defer span.End()
 
-	resp, err := md.apiClient.CreateRelease(ctx, fly.CreateReleaseInput{
+	_ = `# @genqlient
+	mutation MachinesCreateRelease($input:CreateReleaseInput!) {
+		createRelease(input:$input) {
+			release {
+				id
+				version
+			}
+		}
+	}
+	`
+	input := gql.CreateReleaseInput{
 		AppId:           md.app.Name,
 		PlatformVersion: "machines",
-		Strategy:        fly.DeploymentStrategy(strings.ToUpper(md.strategy)),
+		Strategy:        gql.DeploymentStrategy(strings.ToUpper(md.strategy)),
 		Definition:      md.appConfig,
 		Image:           md.img,
-	})
+	}
+	resp, err := gql.MachinesCreateRelease(ctx, md.gqlClient, input)
 	if err != nil {
 		tracing.RecordError(span, err, "failed to create machine release")
 		return err
@@ -556,10 +589,20 @@ func (md *machineDeployment) updateReleaseInBackend(ctx context.Context, status
 	))
 	defer span.End()
 
-	_, err := md.apiClient.UpdateRelease(ctx, fly.UpdateReleaseInput{
+	_ = `# @genqlient
+	mutation MachinesUpdateRelease($input:UpdateReleaseInput!) {
+		updateRelease(input:$input) {
+			release {
+				id
+			}
+		}
+	}
+	`
+	input := gql.UpdateReleaseInput{
 		ReleaseId: md.releaseId,
 		Status:    status,
-	})
+	}
+	_, err := gql.MachinesUpdateRelease(ctx, md.gqlClient, input)
 	if err != nil {
 		tracing.RecordError(span, err, "failed to update machine release")
 		return err
diff --git a/internal/command/deploy/strategy_bluegreen.go b/internal/command/deploy/strategy_bluegreen.go
index 77b1fc741..303be1fe0 100644
--- a/internal/command/deploy/strategy_bluegreen.go
+++ b/internal/command/deploy/strategy_bluegreen.go
@@ -18,9 +18,9 @@ import (
 	"go.opentelemetry.io/otel/attribute"
 
 	fly "github.com/superfly/fly-go"
+	"github.com/superfly/fly-go/flaps"
 	"github.com/superfly/flyctl/internal/appconfig"
 	"github.com/superfly/flyctl/internal/ctrlc"
-	"github.com/superfly/flyctl/internal/flapsutil"
 	"github.com/superfly/flyctl/internal/flyutil"
 	"github.com/superfly/flyctl/internal/machine"
 	"github.com/superfly/flyctl/internal/tracing"
@@ -57,7 +57,7 @@ type RollbackLog struct {
 type blueGreen struct {
 	greenMachines       machineUpdateEntries
 	blueMachines        machineUpdateEntries
-	flaps               flapsutil.FlapsClient
+	flaps               *flaps.Client
 	apiClient           flyutil.Client
 	io                  *iostreams.IOStreams
 	colorize            *iostreams.ColorScheme
diff --git a/internal/command/extensions/core/core.go b/internal/command/extensions/core/core.go
index 8c0c0e2e6..17a1978d5 100644
--- a/internal/command/extensions/core/core.go
+++ b/internal/command/extensions/core/core.go
@@ -64,6 +64,7 @@ func ProvisionExtension(ctx context.Context, params ExtensionParams) (extension
 
 	// Ensure users have agreed to the provider terms of service
 	err = AgreeToProviderTos(ctx, provider)
+
 	if err != nil {
 		return extension, err
 	}
@@ -108,6 +109,7 @@ func ProvisionExtension(ctx context.Context, params ExtensionParams) (extension
 					name = targetApp.Name + "-" + provider.NameSuffix
 				}
 				err = prompt.String(ctx, &name, "Choose a name, use the default, or leave blank to generate one:", name, false)
+
 				if err != nil {
 					return
 				}
@@ -180,6 +182,7 @@ func ProvisionExtension(ctx context.Context, params ExtensionParams) (extension
 		}
 
 		detectedPlatform, err = scanner.Scan(absDir, &scanner.ScannerConfig{Colorize: io.ColorScheme()})
+
 		if err != nil {
 			return extension, err
 		}
@@ -202,7 +205,7 @@ func ProvisionExtension(ctx context.Context, params ExtensionParams) (extension
 	extension.App = &targetApp
 
 	if provider.AsyncProvisioning {
-		err = WaitForProvision(ctx, extension.Data.Name, params.Provider)
+		err = WaitForProvision(ctx, extension.Data.Name)
 		if err != nil {
 			return
 		}
@@ -277,7 +280,7 @@ func AgreeToProviderTos(ctx context.Context, provider gql.ExtensionProviderData)
 	return err
 }
 
-func WaitForProvision(ctx context.Context, name string, provider string) error {
+func WaitForProvision(ctx context.Context, name string) error {
 	io := iostreams.FromContext(ctx)
 	client := flyutil.ClientFromContext(ctx).GenqClient()
 
@@ -294,7 +297,7 @@ func WaitForProvision(ctx context.Context, name string, provider string) error {
 
 	for {
 
-		resp, err := gql.GetAddOn(ctx, client, name, provider)
+		resp, err := gql.GetAddOn(ctx, client, name)
 		if err != nil {
 			return err
 		}
@@ -340,6 +343,7 @@ func OpenOrgDashboard(ctx context.Context, orgSlug string, providerName string)
 	provider := resp.AddOnProvider.ExtensionProviderData
 
 	err = AgreeToProviderTos(ctx, provider)
+
 	if err != nil {
 		return err
 	}
@@ -365,15 +369,16 @@ func openUrl(ctx context.Context, url string) (err error) {
 	return
 }
 
-func OpenDashboard(ctx context.Context, extensionName string, provider gql.AddOnType) (err error) {
+func OpenDashboard(ctx context.Context, extensionName string) (err error) {
 	client := flyutil.ClientFromContext(ctx).GenqClient()
 
-	result, err := gql.GetAddOn(ctx, client, extensionName, string(provider))
+	result, err := gql.GetAddOn(ctx, client, extensionName)
 	if err != nil {
 		return err
 	}
 
 	err = AgreeToProviderTos(ctx, result.AddOn.AddOnProvider.ExtensionProviderData)
+
 	if err != nil {
 		return err
 	}
@@ -389,7 +394,8 @@ func Discover(ctx context.Context, provider gql.AddOnType) (addOn *gql.AddOnData
 	appName := appconfig.NameFromContext(ctx)
 
 	if len(flag.Args(ctx)) == 1 {
-		response, err := gql.GetAddOn(ctx, client, flag.FirstArg(ctx), string(provider))
+
+		response, err := gql.GetAddOn(ctx, client, flag.FirstArg(ctx))
 		if err != nil {
 			return nil, nil, err
 		}
@@ -463,12 +469,13 @@ func setSecretsFromExtension(ctx context.Context, app *gql.AppData, extension *E
 		}
 		for _, key := range keys {
 			input.Secrets = append(input.Secrets, gql.SecretInput{Key: key, Value: secrets[key].(string)})
-			fmt.Fprintf(io.Out, "%s: %s\n", key, secrets[key].(string))
+			fmt.Fprintln(io.Out, key)
 		}
 
 		fmt.Fprintln(io.Out)
 
 		_, err = gql.SetSecrets(ctx, client, input)
+
 		if err != nil {
 			return err
 		}
diff --git a/internal/command/extensions/enveloop/create.go b/internal/command/extensions/enveloop/create.go
index ca212b88f..f97fbb520 100644
--- a/internal/command/extensions/enveloop/create.go
+++ b/internal/command/extensions/enveloop/create.go
@@ -15,7 +15,7 @@ import (
 
 func create() (cmd *cobra.Command) {
 	const (
-		short = "Provision an Enveloop project"
+		short = "Provision a Upstash Enveloop project"
 		long  = short + "\n"
 	)
 
diff --git a/internal/command/extensions/enveloop/dashboard.go b/internal/command/extensions/enveloop/dashboard.go
index 0f602e6c1..8352f39f9 100644
--- a/internal/command/extensions/enveloop/dashboard.go
+++ b/internal/command/extensions/enveloop/dashboard.go
@@ -12,7 +12,7 @@ import (
 
 func dashboard() (cmd *cobra.Command) {
 	const (
-		long = `Open the Enveloop dashboard via your web browser`
+		long = `Visit the Enveloop dashboard on the Upstash web console`
 
 		short = long
 		usage = "dashboard"
@@ -39,5 +39,5 @@ func runDashboard(ctx context.Context) (err error) {
 	if err != nil {
 		return err
 	}
-	return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeEnveloop)
+	return extensions_core.OpenDashboard(ctx, extension.Name)
 }
diff --git a/internal/command/extensions/enveloop/destroy.go b/internal/command/extensions/enveloop/destroy.go
index 5242a65a2..36f9b9f75 100644
--- a/internal/command/extensions/enveloop/destroy.go
+++ b/internal/command/extensions/enveloop/destroy.go
@@ -16,7 +16,7 @@ import (
 
 func destroy() (cmd *cobra.Command) {
 	const (
-		long = `Permanently destroy an Enveloop project`
+		long = `Permanently destroy an Upstash Enveloop project`
 
 		short = long
 		usage = "destroy [name]"
@@ -45,10 +45,10 @@ func runDestroy(ctx context.Context) (err error) {
 	}
 
 	if !flag.GetYes(ctx) {
-		const msg = "Destroying an Enveloop project is not reversible. All Enveloop templates, message settings, and message logs will be lost."
+		const msg = "Destroying an Upstash Enveloop project is not reversible."
 		fmt.Fprintln(io.ErrOut, colorize.Red(msg))
 
-		switch confirmed, err := prompt.Confirmf(ctx, "Do you want to destroy the Enveloop project named %s?", extension.Name); {
+		switch confirmed, err := prompt.Confirmf(ctx, "Do you want to destroy the index named %s?", extension.Name); {
 		case err == nil:
 			if !confirmed {
 				return nil
@@ -66,7 +66,7 @@ func runDestroy(ctx context.Context) (err error) {
 	}
 
 	out := iostreams.FromContext(ctx).Out
-	fmt.Fprintf(out, "Your Enveloop project %s was destroyed\n", extension.Name)
+	fmt.Fprintf(out, "Your Upstash Enveloop project %s was destroyed\n", extension.Name)
 
 	return nil
 }
diff --git a/internal/command/extensions/enveloop/list.go b/internal/command/extensions/enveloop/list.go
index e036a4d26..ee768ff57 100644
--- a/internal/command/extensions/enveloop/list.go
+++ b/internal/command/extensions/enveloop/list.go
@@ -15,7 +15,7 @@ import (
 
 func list() (cmd *cobra.Command) {
 	const (
-		long  = `List your Enveloop projects`
+		long  = `List your Upstash Enveloop project`
 		short = long
 		usage = "list"
 	)
diff --git a/internal/command/extensions/enveloop/status.go b/internal/command/extensions/enveloop/status.go
index dfa898369..b92618aec 100644
--- a/internal/command/extensions/enveloop/status.go
+++ b/internal/command/extensions/enveloop/status.go
@@ -14,7 +14,7 @@ import (
 
 func status() *cobra.Command {
 	const (
-		short = "Show details about an Enveloop project"
+		short = "Show details about an Upstash Enveloop project"
 		long  = short + "\n"
 
 		usage = "status [name]"
diff --git a/internal/command/extensions/kafka/dashboard.go b/internal/command/extensions/kafka/dashboard.go
index 393ab07f3..15d94b42b 100644
--- a/internal/command/extensions/kafka/dashboard.go
+++ b/internal/command/extensions/kafka/dashboard.go
@@ -39,5 +39,5 @@ func runDashboard(ctx context.Context) (err error) {
 	if err != nil {
 		return err
 	}
-	return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeUpstashKafka)
+	return extensions_core.OpenDashboard(ctx, extension.Name)
 }
diff --git a/internal/command/extensions/kafka/update.go b/internal/command/extensions/kafka/update.go
index 8cc4a9d85..718d75458 100644
--- a/internal/command/extensions/kafka/update.go
+++ b/internal/command/extensions/kafka/update.go
@@ -32,7 +32,7 @@ func runUpdate(ctx context.Context) (err error) {
 	client := flyutil.ClientFromContext(ctx).GenqClient()
 
 	id := flag.FirstArg(ctx)
-	response, err := gql.GetAddOn(ctx, client, id, string(gql.AddOnTypeUpstashKafka))
+	response, err := gql.GetAddOn(ctx, client, id)
 	if err != nil {
 		return
 	}
diff --git a/internal/command/extensions/kubernetes/create.go b/internal/command/extensions/kubernetes/create.go
index 3d23c65a6..696e3916a 100644
--- a/internal/command/extensions/kubernetes/create.go
+++ b/internal/command/extensions/kubernetes/create.go
@@ -65,7 +65,7 @@ func runK8sCreate(ctx context.Context) (err error) {
 		return err
 	}
 
-	resp, err := gql.GetAddOn(ctx, client, extension.Data.Name, string(gql.AddOnTypeKubernetes))
+	resp, err := gql.GetAddOn(ctx, client, extension.Data.Name)
 	if err != nil {
 		return err
 	}
diff --git a/internal/command/extensions/kubernetes/kubeconfig.go b/internal/command/extensions/kubernetes/kubeconfig.go
index dec94e865..391b182e1 100644
--- a/internal/command/extensions/kubernetes/kubeconfig.go
+++ b/internal/command/extensions/kubernetes/kubeconfig.go
@@ -36,7 +36,7 @@ func runSaveKubeconfig(ctx context.Context) error {
 	client := flyutil.ClientFromContext(ctx).GenqClient()
 	clusterName := flag.FirstArg(ctx)
 
-	resp, err := gql.GetAddOn(ctx, client, clusterName, string(gql.AddOnTypeKubernetes))
+	resp, err := gql.GetAddOn(ctx, client, clusterName)
 	if err != nil {
 		return err
 	}
diff --git a/internal/command/extensions/sentry/dashboard.go b/internal/command/extensions/sentry/dashboard.go
index 06111da65..aa8dcd60c 100644
--- a/internal/command/extensions/sentry/dashboard.go
+++ b/internal/command/extensions/sentry/dashboard.go
@@ -39,6 +39,6 @@ func RunDashboard(ctx context.Context) (err error) {
 		return err
 	}
 
-	err = extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeSentry)
+	err = extensions_core.OpenDashboard(ctx, extension.Name)
 	return
 }
diff --git a/internal/command/extensions/supabase/dashboard.go b/internal/command/extensions/supabase/dashboard.go
index 1674aa386..3c51f5eca 100644
--- a/internal/command/extensions/supabase/dashboard.go
+++ b/internal/command/extensions/supabase/dashboard.go
@@ -43,5 +43,5 @@ func runDashboard(ctx context.Context) error {
 		return err
 	}
 
-	return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeSupabase)
+	return extensions_core.OpenDashboard(ctx, extension.Name)
 }
diff --git a/internal/command/extensions/tigris/dashboard.go b/internal/command/extensions/tigris/dashboard.go
index 99e7f6152..5149f3fc7 100644
--- a/internal/command/extensions/tigris/dashboard.go
+++ b/internal/command/extensions/tigris/dashboard.go
@@ -45,6 +45,6 @@ func runDashboard(ctx context.Context) (err error) {
 		return err
 	}
 
-	err = extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeTigris)
+	err = extensions_core.OpenDashboard(ctx, extension.Name)
 	return
 }
diff --git a/internal/command/extensions/tigris/update.go b/internal/command/extensions/tigris/update.go
index 2b5a35abe..5d43bf8e5 100644
--- a/internal/command/extensions/tigris/update.go
+++ b/internal/command/extensions/tigris/update.go
@@ -59,7 +59,7 @@ func runUpdate(ctx context.Context) (err error) {
 	client := flyutil.ClientFromContext(ctx).GenqClient()
 
 	id := flag.FirstArg(ctx)
-	response, err := gql.GetAddOn(ctx, client, id, string(gql.AddOnTypeTigris))
+	response, err := gql.GetAddOn(ctx, client, id)
 	if err != nil {
 		return
 	}
diff --git a/internal/command/extensions/vector/dashboard.go b/internal/command/extensions/vector/dashboard.go
index 7acb3ad73..9f9512a13 100644
--- a/internal/command/extensions/vector/dashboard.go
+++ b/internal/command/extensions/vector/dashboard.go
@@ -39,5 +39,5 @@ func runDashboard(ctx context.Context) (err error) {
 	if err != nil {
 		return err
 	}
-	return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeUpstashVector)
+	return extensions_core.OpenDashboard(ctx, extension.Name)
 }
diff --git a/internal/command/launch/cmd.go b/internal/command/launch/cmd.go
index 6c2d6d925..e0d4ec630 100644
--- a/internal/command/launch/cmd.go
+++ b/internal/command/launch/cmd.go
@@ -199,7 +199,7 @@ func run(ctx context.Context) (err error) {
 
 		status.TraceID = span.SpanContext().TraceID().String()
 		status.Duration = time.Since(startTime)
-		metrics.LaunchStatus(ctx, status)
+		metrics.LaunchStatus(ctx, "launch", status)
 	}()
 
 	if err := warnLegacyBehavior(ctx); err != nil {
@@ -271,12 +271,6 @@ func run(ctx context.Context) (err error) {
 		return err
 	}
 
-	if errors := recoverableErrors.build(); errors != "" {
-
-		fmt.Fprintf(io.ErrOut, "\n%s\n%s\n", aurora.Yellow("The following problems must be fixed in the Launch UI:"), errors)
-		incompleteLaunchManifest = true
-	}
-
 	summary, err := state.PlanSummary(ctx)
 	if err != nil {
 		return err
@@ -294,6 +288,12 @@ func run(ctx context.Context) (err error) {
 		summary,
 	)
 
+	if errors := recoverableErrors.build(); errors != "" {
+
+		fmt.Fprintf(io.ErrOut, "\n%s\n%s\n", aurora.Reverse(aurora.Red("The following problems must be fixed in the Launch UI:")), errors)
+		incompleteLaunchManifest = true
+	}
+
 	editInUi := false
 	if !flag.GetBool(ctx, "yes") {
 		message := lo.Ternary(
diff --git a/internal/command/launch/launch_frameworks.go b/internal/command/launch/launch_frameworks.go
index 14cdcd081..ca5868272 100644
--- a/internal/command/launch/launch_frameworks.go
+++ b/internal/command/launch/launch_frameworks.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"io/fs"
-	"maps"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -136,7 +135,7 @@ func (state *launchState) scannerRunCallback(ctx context.Context) error {
 			cfg, err := appconfig.LoadConfig(state.sourceInfo.MergeConfig.Name)
 			if err == nil {
 				// In theory, any part of the configuration could be merged here, but for now
-				// we will only copy over the processes, release command, env, volume, and statics
+				// we will only copy over the processes, release command, volume, and statics
 				if state.sourceInfo.Processes == nil {
 					state.sourceInfo.Processes = cfg.Processes
 				}
@@ -145,16 +144,6 @@ func (state *launchState) scannerRunCallback(ctx context.Context) error {
 					state.sourceInfo.ReleaseCmd = cfg.Deploy.ReleaseCommand
 				}
 
-				if len(cfg.Env) > 0 {
-					if len(state.sourceInfo.Env) == 0 {
-						state.sourceInfo.Env = cfg.Env
-					} else {
-						clone := maps.Clone(state.sourceInfo.Env)
-						maps.Copy(clone, cfg.Env)
-						state.sourceInfo.Env = clone
-					}
-				}
-
 				if len(state.sourceInfo.Volumes) == 0 && len(cfg.Mounts) > 0 {
 					state.sourceInfo.Volumes = []scanner.Volume{cfg.Mounts[0]}
 				}
diff --git a/internal/command/postgres/postgres.go b/internal/command/postgres/postgres.go
index a266abb8e..7064cab47 100644
--- a/internal/command/postgres/postgres.go
+++ b/internal/command/postgres/postgres.go
@@ -32,7 +32,6 @@ func New() *cobra.Command {
 		newDb(),
 		newDetach(),
 		newList(),
-		newRenewSSHCerts(),
 		newRestart(),
 		newUsers(),
 		newFailover(),
diff --git a/internal/command/postgres/renew_certs.go b/internal/command/postgres/renew_certs.go
deleted file mode 100644
index cfad231bc..000000000
--- a/internal/command/postgres/renew_certs.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package postgres
-
-import (
-	"context"
-	"crypto/ed25519"
-	"fmt"
-
-	"github.com/spf13/cobra"
-	fly "github.com/superfly/fly-go"
-	"github.com/superfly/flyctl/internal/appconfig"
-	"github.com/superfly/flyctl/internal/command"
-	"github.com/superfly/flyctl/internal/command/apps"
-	"github.com/superfly/flyctl/internal/command/ssh"
-	"github.com/superfly/flyctl/internal/flag"
-	"github.com/superfly/flyctl/internal/flyutil"
-	mach "github.com/superfly/flyctl/internal/machine"
-	"github.com/superfly/flyctl/iostreams"
-)
-
-func newRenewSSHCerts() *cobra.Command {
-	const (
-		short = "Renews the SSH certificates for the Postgres cluster."
-		long  = "Renews the SSH certificates for the Postgres cluster. This is useful when the certificates have expired or need to be rotated."
-		usage = "renew-certs"
-	)
-
-	cmd := command.New(usage, short, long, runRefreshSSHCerts,
-		command.RequireSession,
-		command.RequireAppName,
-	)
-
-	flag.Add(cmd,
-		flag.App(),
-		flag.AppConfig(),
-		flag.Int{
-			Name:        "valid-days",
-			Description: "The number of days the certificate should be valid for.",
-			Default:     36525,
-		},
-	)
-
-	return cmd
-}
-
-func runRefreshSSHCerts(ctx context.Context) error {
-	var (
-		appName = appconfig.NameFromContext(ctx)
-		client  = flyutil.ClientFromContext(ctx)
-	)
-
-	app, err := client.GetAppCompact(ctx, appName)
-	if err != nil {
-		return err
-	}
-
-	if !app.IsPostgresApp() {
-		return fmt.Errorf("app %s is not a postgres app", appName)
-	}
-
-	ctx, err = apps.BuildContext(ctx, app)
-	if err != nil {
-		return err
-	}
-
-	return refreshSSHCerts(ctx, app)
-}
-
-func refreshSSHCerts(ctx context.Context, app *fly.AppCompact) error {
-	var (
-		io        = iostreams.FromContext(ctx)
-		client    = flyutil.ClientFromContext(ctx)
-		colorize  = io.ColorScheme()
-		validDays = flag.GetInt(ctx, "valid-days")
-	)
-
-	machines, releaseLeaseFunc, err := mach.AcquireAllLeases(ctx)
-	defer releaseLeaseFunc()
-	if err != nil {
-		return err
-	}
-
-	leader, err := pickLeader(ctx, machines)
-	if err != nil {
-		return fmt.Errorf("failed to resolve leader: %w", err)
-	}
-
-	if !IsFlex(leader) {
-		return fmt.Errorf("app %s is not a flex cluster", app.Name)
-	}
-
-	pub, priv, err := ed25519.GenerateKey(nil)
-	if err != nil {
-		return fmt.Errorf("failed to generate ssh key: %w", err)
-	}
-
-	validHours := validDays * 24
-	cert, err := client.IssueSSHCertificate(ctx, app.Organization, []string{"root", "fly", "postgres"}, []string{app.Name}, &validHours, pub)
-	if err != nil {
-		return fmt.Errorf("failed to issue ssh certificate: %w", err)
-	}
-
-	pemkey := ssh.MarshalED25519PrivateKey(priv, "postgres inter-machine ssh")
-
-	secrets := map[string]string{
-		"SSH_KEY":  string(pemkey),
-		"SSH_CERT": cert.Certificate,
-	}
-
-	_, err = client.SetSecrets(ctx, app.Name, secrets)
-	if err != nil {
-		return fmt.Errorf("failed to set ssh secrets: %w", err)
-	}
-
-	command := fmt.Sprintf("fly deploy --app %s --image %s", app.Name, leader.FullImageRef())
-
-	fmt.Fprintf(io.Out, "Your SSH certificate(s) have been renewed are set to expire in %d day(s)\n", validDays)
-	fmt.Fprintf(io.Out, "Run %s to apply the changes!\n", colorize.Bold(command))
-
-	return nil
-}
diff --git a/internal/command/redis/proxy.go b/internal/command/redis/proxy.go
index 0545ceef6..d3a8ee8eb 100644
--- a/internal/command/redis/proxy.go
+++ b/internal/command/redis/proxy.go
@@ -67,7 +67,7 @@ func getRedisProxyParams(ctx context.Context, localProxyPort string) (*proxy.Con
 		return nil, "", err
 	}
 
-	response, err := gql.GetAddOn(ctx, client.GenqClient(), databases[index].Name, string(gql.AddOnTypeUpstashRedis))
+	response, err := gql.GetAddOn(ctx, client.GenqClient(), databases[index].Name)
 	if err != nil {
 		return nil, "", err
 	}
diff --git a/internal/command/redis/status.go b/internal/command/redis/status.go
index 1646ea568..27fc8c306 100644
--- a/internal/command/redis/status.go
+++ b/internal/command/redis/status.go
@@ -39,7 +39,7 @@ func runStatus(ctx context.Context) (err error) {
 		client = flyutil.ClientFromContext(ctx).GenqClient()
 	)
 
-	response, err := gql.GetAddOn(ctx, client, name, string(gql.AddOnTypeUpstashRedis))
+	response, err := gql.GetAddOn(ctx, client, name)
 	if err != nil {
 		return err
 	}
diff --git a/internal/command/redis/update.go b/internal/command/redis/update.go
index 9b8143842..7dfeb544b 100644
--- a/internal/command/redis/update.go
+++ b/internal/command/redis/update.go
@@ -42,7 +42,7 @@ func runUpdate(ctx context.Context) (err error) {
 
 	id := flag.FirstArg(ctx)
 
-	response, err := gql.GetAddOn(ctx, client, id, string(gql.AddOnTypeUpstashRedis))
+	response, err := gql.GetAddOn(ctx, client, id)
 	if err != nil {
 		return
 	}
diff --git a/internal/flyutil/client.go b/internal/flyutil/client.go
index e294ea048..3b9e979d3 100644
--- a/internal/flyutil/client.go
+++ b/internal/flyutil/client.go
@@ -25,13 +25,11 @@ type Client interface {
 	ClosestWireguardGatewayRegion(ctx context.Context) (*fly.Region, error)
 	CreateAndRegisterDomain(organizationID string, name string) (*fly.Domain, error)
 	CreateApp(ctx context.Context, input fly.CreateAppInput) (*fly.App, error)
-	CreateBuild(ctx context.Context, input fly.CreateBuildInput) (*fly.CreateBuildResponse, error)
 	CreateDelegatedWireGuardToken(ctx context.Context, org *fly.Organization, name string) (*fly.DelegatedWireGuardToken, error)
 	CreateDoctorUrl(ctx context.Context) (putUrl string, err error)
 	CreateDomain(organizationID string, name string) (*fly.Domain, error)
 	CreateOrganization(ctx context.Context, organizationname string) (*fly.Organization, error)
 	CreateOrganizationInvite(ctx context.Context, id, email string) (*fly.Invitation, error)
-	CreateRelease(ctx context.Context, input fly.CreateReleaseInput) (*fly.CreateReleaseResponse, error)
 	CreateWireGuardPeer(ctx context.Context, org *fly.Organization, region, name, pubkey, network string) (*fly.CreatedWireGuardPeer, error)
 	DeleteApp(ctx context.Context, appName string) error
 	DeleteCertificate(ctx context.Context, appName, hostname string) (*fly.DeleteCertificatePayload, error)
@@ -42,7 +40,6 @@ type Client interface {
 	EnablePostgresConsul(ctx context.Context, appName string) (*fly.PostgresEnableConsulPayload, error)
 	EnsureRemoteBuilder(ctx context.Context, orgID, appName, region string) (*fly.GqlMachine, *fly.App, error)
 	ExportDNSRecords(ctx context.Context, domainId string) (string, error)
-	FinishBuild(ctx context.Context, input fly.FinishBuildInput) (*fly.FinishBuildResponse, error)
 	GetApp(ctx context.Context, appName string) (*fly.App, error)
 	GetAppBasic(ctx context.Context, appName string) (*fly.AppBasic, error)
 	GetAppCertificates(ctx context.Context, appName string) ([]fly.AppCertificateCompact, error)
@@ -77,7 +74,6 @@ type Client interface {
 	GenqClient() genq.Client
 	ImportDNSRecords(ctx context.Context, domainId string, zonefile string) ([]fly.ImportDnsWarning, []fly.ImportDnsChange, error)
 	IssueSSHCertificate(ctx context.Context, org fly.OrganizationImpl, principals []string, appNames []string, valid_hours *int, publicKey ed25519.PublicKey) (*fly.IssuedCertificate, error)
-	LatestImage(ctx context.Context, appName string) (string, error)
 	ListPostgresClusterAttachments(ctx context.Context, appName, postgresAppName string) ([]*fly.PostgresClusterAttachment, error)
 	Logger() fly.Logger
 	MoveApp(ctx context.Context, appName string, orgID string) (*fly.App, error)
@@ -91,7 +87,6 @@ type Client interface {
 	RunWithContext(ctx context.Context, req *graphql.Request) (fly.Query, error)
 	SetGenqClient(client genq.Client)
 	SetSecrets(ctx context.Context, appName string, secrets map[string]string) (*fly.Release, error)
-	UpdateRelease(ctx context.Context, input fly.UpdateReleaseInput) (*fly.UpdateReleaseResponse, error)
 	UnsetSecrets(ctx context.Context, appName string, keys []string) (*fly.Release, error)
 	ValidateWireGuardPeers(ctx context.Context, peerIPs []string) (invalid []string, err error)
 }
diff --git a/internal/metrics/helpers.go b/internal/metrics/helpers.go
index a2cf50215..292a0f616 100644
--- a/internal/metrics/helpers.go
+++ b/internal/metrics/helpers.go
@@ -52,6 +52,22 @@ func Status(ctx context.Context, metricSlug string, success bool) {
 	Send(ctx, metricSlug+"/status", map[string]bool{"success": success})
 }
 
+func LaunchStatus(ctx context.Context, metricSlug string, payload LaunchStatusPayload) {
+	ok := withUnmatchedStatuses(func(unmatchedStatuses map[string]struct{}) bool {
+		if _, ok := unmatchedStatuses[metricSlug]; ok {
+			delete(unmatchedStatuses, metricSlug)
+			return true
+		}
+		return false
+	})
+	if !ok {
+		terminal.Debugf("Metrics: Attempted to send launch status for %s, but no start event was sent", metricSlug)
+		return
+	}
+
+	Send(ctx, metricSlug+"/status", payload)
+}
+
 type LaunchStatusPayload struct {
 	TraceID  string        `json:"traceId"`
 	Error    string        `json:"error"`
@@ -76,54 +92,6 @@ type LaunchStatusPayload struct {
 	FlyctlVersion string `json:"flyctlVersion"`
 }
 
-func LaunchStatus(ctx context.Context, payload LaunchStatusPayload) {
-	ok := withUnmatchedStatuses(func(unmatchedStatuses map[string]struct{}) bool {
-		if _, ok := unmatchedStatuses["launch"]; ok {
-			delete(unmatchedStatuses, "launch")
-			return true
-		}
-		return false
-	})
-	if !ok {
-		terminal.Debug("Metrics: Attempted to send launch status for launch, but no start event was sent")
-		// logger.FromContext(ctx).Debug("Metrics: Attempted to send launch status for launch, but no start event was sent")
-		return
-	}
-
-	Send(ctx, "launch/status", payload)
-}
-
-type DeployStatusPayload struct {
-	TraceID  string        `json:"traceId"`
-	Error    string        `json:"error"`
-	Duration time.Duration `json:"duration"`
-
-	AppName       string `json:"app"`
-	OrgSlug       string `json:"org"`
-	PrimaryRegion string `json:"primary_region"`
-	Image         string `json:"image"`
-	Strategy      string `json:"strategy"`
-
-	FlyctlVersion string `json:"flyctlVersion"`
-}
-
-func DeployStatus(ctx context.Context, payload DeployStatusPayload) {
-	ok := withUnmatchedStatuses(func(unmatchedStatuses map[string]struct{}) bool {
-		if _, ok := unmatchedStatuses["deploy"]; ok {
-			delete(unmatchedStatuses, "deploy")
-			return true
-		}
-		return false
-	})
-
-	if !ok {
-		terminal.Debug("Metrics: Attempted to send deploy status for deploy, but no start event was sent")
-		return
-	}
-
-	Send(ctx, "deploy/status", payload)
-}
-
 func Send[T any](ctx context.Context, metricSlug string, value T) {
 	valJson, err := json.Marshal(value)
 	if err != nil {
diff --git a/internal/mock/client.go b/internal/mock/client.go
index fb71631e6..720101bae 100644
--- a/internal/mock/client.go
+++ b/internal/mock/client.go
@@ -26,13 +26,11 @@ type Client struct {
 	ClosestWireguardGatewayRegionFunc  func(ctx context.Context) (*fly.Region, error)
 	CreateAndRegisterDomainFunc        func(organizationID string, name string) (*fly.Domain, error)
 	CreateAppFunc                      func(ctx context.Context, input fly.CreateAppInput) (*fly.App, error)
-	CreateBuildFunc                    func(ctx context.Context, input fly.CreateBuildInput) (*fly.CreateBuildResponse, error)
 	CreateDelegatedWireGuardTokenFunc  func(ctx context.Context, org *fly.Organization, name string) (*fly.DelegatedWireGuardToken, error)
 	CreateDoctorUrlFunc                func(ctx context.Context) (putUrl string, err error)
 	CreateDomainFunc                   func(organizationID string, name string) (*fly.Domain, error)
 	CreateOrganizationFunc             func(ctx context.Context, organizationname string) (*fly.Organization, error)
 	CreateOrganizationInviteFunc       func(ctx context.Context, id, email string) (*fly.Invitation, error)
-	CreateReleaseFunc                  func(ctx context.Context, input fly.CreateReleaseInput) (*fly.CreateReleaseResponse, error)
 	CreateWireGuardPeerFunc            func(ctx context.Context, org *fly.Organization, region, name, pubkey, network string) (*fly.CreatedWireGuardPeer, error)
 	DeleteAppFunc                      func(ctx context.Context, appName string) error
 	DeleteCertificateFunc              func(ctx context.Context, appName, hostname string) (*fly.DeleteCertificatePayload, error)
@@ -43,7 +41,6 @@ type Client struct {
 	EnablePostgresConsulFunc           func(ctx context.Context, appName string) (*fly.PostgresEnableConsulPayload, error)
 	EnsureRemoteBuilderFunc            func(ctx context.Context, orgID, appName, region string) (*fly.GqlMachine, *fly.App, error)
 	ExportDNSRecordsFunc               func(ctx context.Context, domainId string) (string, error)
-	FinishBuildFunc                    func(ctx context.Context, input fly.FinishBuildInput) (*fly.FinishBuildResponse, error)
 	GetAppFunc                         func(ctx context.Context, appName string) (*fly.App, error)
 	GetAppBasicFunc                    func(ctx context.Context, appName string) (*fly.AppBasic, error)
 	GetAppCertificatesFunc             func(ctx context.Context, appName string) ([]fly.AppCertificateCompact, error)
@@ -76,7 +73,6 @@ type Client struct {
 	GetWireGuardPeerFunc               func(ctx context.Context, slug, name string) (*fly.WireGuardPeer, error)
 	GetWireGuardPeersFunc              func(ctx context.Context, slug string) ([]*fly.WireGuardPeer, error)
 	GenqClientFunc                     func() genq.Client
-	LatestImageFunc                    func(ctx context.Context, appName string) (string, error)
 	ImportDNSRecordsFunc               func(ctx context.Context, domainId string, zonefile string) ([]fly.ImportDnsWarning, []fly.ImportDnsChange, error)
 	IssueSSHCertificateFunc            func(ctx context.Context, org fly.OrganizationImpl, principals []string, appNames []string, valid_hours *int, publicKey ed25519.PublicKey) (*fly.IssuedCertificate, error)
 	ListPostgresClusterAttachmentsFunc func(ctx context.Context, appName, postgresAppName string) ([]*fly.PostgresClusterAttachment, error)
@@ -92,7 +88,6 @@ type Client struct {
 	RunWithContextFunc                 func(ctx context.Context, req *graphql.Request) (fly.Query, error)
 	SetGenqClientFunc                  func(client genq.Client)
 	SetSecretsFunc                     func(ctx context.Context, appName string, secrets map[string]string) (*fly.Release, error)
-	UpdateReleaseFunc                  func(ctx context.Context, input fly.UpdateReleaseInput) (*fly.UpdateReleaseResponse, error)
 	UnsetSecretsFunc                   func(ctx context.Context, appName string, keys []string) (*fly.Release, error)
 	ValidateWireGuardPeersFunc         func(ctx context.Context, peerIPs []string) (invalid []string, err error)
 }
@@ -145,10 +140,6 @@ func (m *Client) CreateApp(ctx context.Context, input fly.CreateAppInput) (*fly.
 	return m.CreateAppFunc(ctx, input)
 }
 
-func (m *Client) CreateBuild(ctx context.Context, input fly.CreateBuildInput) (*fly.CreateBuildResponse, error) {
-	return m.CreateBuildFunc(ctx, input)
-}
-
 func (m *Client) CreateDelegatedWireGuardToken(ctx context.Context, org *fly.Organization, name string) (*fly.DelegatedWireGuardToken, error) {
 	return m.CreateDelegatedWireGuardTokenFunc(ctx, org, name)
 }
@@ -169,10 +160,6 @@ func (m *Client) CreateOrganizationInvite(ctx context.Context, id, email string)
 	return m.CreateOrganizationInviteFunc(ctx, id, email)
 }
 
-func (m *Client) CreateRelease(ctx context.Context, input fly.CreateReleaseInput) (*fly.CreateReleaseResponse, error) {
-	return m.CreateReleaseFunc(ctx, input)
-}
-
 func (m *Client) CreateWireGuardPeer(ctx context.Context, org *fly.Organization, region, name, pubkey, network string) (*fly.CreatedWireGuardPeer, error) {
 	return m.CreateWireGuardPeerFunc(ctx, org, region, name, pubkey, network)
 }
@@ -213,10 +200,6 @@ func (m *Client) ExportDNSRecords(ctx context.Context, domainId string) (string,
 	return m.ExportDNSRecordsFunc(ctx, domainId)
 }
 
-func (m *Client) FinishBuild(ctx context.Context, input fly.FinishBuildInput) (*fly.FinishBuildResponse, error) {
-	return m.FinishBuildFunc(ctx, input)
-}
-
 func (m *Client) GetApp(ctx context.Context, appName string) (*fly.App, error) {
 	return m.GetAppFunc(ctx, appName)
 }
@@ -345,10 +328,6 @@ func (m *Client) GenqClient() genq.Client {
 	return m.GenqClientFunc()
 }
 
-func (m *Client) LatestImage(ctx context.Context, appName string) (string, error) {
-	return m.LatestImageFunc(ctx, appName)
-}
-
 func (m *Client) ImportDNSRecords(ctx context.Context, domainId string, zonefile string) ([]fly.ImportDnsWarning, []fly.ImportDnsChange, error) {
 	return m.ImportDNSRecordsFunc(ctx, domainId, zonefile)
 }
@@ -409,10 +388,6 @@ func (m *Client) SetSecrets(ctx context.Context, appName string, secrets map[str
 	return m.SetSecretsFunc(ctx, appName, secrets)
 }
 
-func (m *Client) UpdateRelease(ctx context.Context, input fly.UpdateReleaseInput) (*fly.UpdateReleaseResponse, error) {
-	return m.UpdateReleaseFunc(ctx, input)
-}
-
 func (m *Client) UnsetSecrets(ctx context.Context, appName string, keys []string) (*fly.Release, error) {
 	return m.UnsetSecretsFunc(ctx, appName, keys)
 }
diff --git a/internal/mock/graphql_client.go b/internal/mock/graphql_client.go
new file mode 100644
index 000000000..0a29c422d
--- /dev/null
+++ b/internal/mock/graphql_client.go
@@ -0,0 +1,15 @@
+package mock
+
+import (
+	"context"
+
+	genq "github.com/Khan/genqlient/graphql"
+)
+
+type GraphQLClient struct {
+	MakeRequestFunc func(ctx context.Context, req *genq.Request, resp *genq.Response) error
+}
+
+func (m *GraphQLClient) MakeRequest(ctx context.Context, req *genq.Request, resp *genq.Response) error {
+	return m.MakeRequestFunc(ctx, req, resp)
+}
diff --git a/internal/wireguard/wg.go b/internal/wireguard/wg.go
index a465e517b..ddf8de879 100644
--- a/internal/wireguard/wg.go
+++ b/internal/wireguard/wg.go
@@ -41,12 +41,12 @@ func generatePeerName(ctx context.Context, apiClient flyutil.Client) (string, er
 	return name, nil
 }
 
-func StateForOrg(ctx context.Context, apiClient flyutil.Client, org *fly.Organization, regionCode string, name string, reestablish bool, network string) (*wg.WireGuardState, error) {
+func StateForOrg(ctx context.Context, apiClient flyutil.Client, org *fly.Organization, regionCode string, name string, recycle bool, network string) (*wg.WireGuardState, error) {
 	state, err := getWireGuardStateForOrg(org.Slug, network)
 	if err != nil {
 		return nil, err
 	}
-	if state != nil && !reestablish && (regionCode == "" || state.Region == regionCode) {
+	if state != nil && !recycle && state.Region == regionCode {
 		return state, nil
 	}
 


Test patch:
diff --git a/internal/command/deploy/deploy_test.go b/internal/command/deploy/deploy_test.go
index 7031296a3..e4b98796b 100644
--- a/internal/command/deploy/deploy_test.go
+++ b/internal/command/deploy/deploy_test.go
@@ -4,12 +4,13 @@ import (
 	"bytes"
 	"context"
 	"embed"
+	"encoding/json"
 	"io/fs"
 	"os"
 	"path/filepath"
 	"testing"
-	"time"
 
+	genq "github.com/Khan/genqlient/graphql"
 	"github.com/superfly/fly-go"
 	"github.com/superfly/flyctl/internal/command/deploy"
 	"github.com/superfly/flyctl/internal/flapsutil"
@@ -24,12 +25,16 @@ import (
 var testdata embed.FS
 
 func TestCommand_Execute(t *testing.T) {
+	t.Skip("in progress")
+
 	dir := t.TempDir()
 	fsys, _ := fs.Sub(testdata, "testdata/basic")
 	if err := copyFS(fsys, dir); err != nil {
 		t.Fatal(err)
 	}
-	chdir(t, dir)
+	if err := os.Chdir(dir); err != nil { // TODO: Revert working directory
+		t.Fatal(err)
+	}
 
 	var buf bytes.Buffer
 	cmd := deploy.New()
@@ -42,7 +47,9 @@ func TestCommand_Execute(t *testing.T) {
 	ctx = task.NewWithContext(ctx)
 	ctx = logger.NewContext(ctx, logger.New(&buf, logger.Info, true))
 
+	var genqlient mock.GraphQLClient
 	var client mock.Client
+	client.GenqClientFunc = func() genq.Client { return &genqlient }
 	ctx = flyutil.NewContextWithClient(ctx, &client)
 
 	var flapsClient mock.FlapsClient
@@ -56,69 +63,21 @@ func TestCommand_Execute(t *testing.T) {
 		if got, want := appName, "test-basic"; got != want {
 			t.Fatalf("appName=%s, want %s", got, want)
 		}
-		return &fly.AppCompact{
-			Organization: &fly.OrganizationBasic{Slug: "my-org"},
-		}, nil
-	}
-	client.CreateBuildFunc = func(ctx context.Context, input fly.CreateBuildInput) (*fly.CreateBuildResponse, error) {
-		return &fly.CreateBuildResponse{
-			CreateBuild: fly.CreateBuildCreateBuildCreateBuildPayload{
-				Id: "BUILD1",
-			},
-		}, nil
-	}
-	client.ResolveImageForAppFunc = func(ctx context.Context, appName, imageRef string) (*fly.Image, error) {
-		return &fly.Image{
-			ID:             "IMAGE1",
-			Ref:            "test-registry.fly.io/test-basic/deployment-123",
-			CompressedSize: "1000",
-		}, nil
-	}
-	client.FinishBuildFunc = func(ctx context.Context, input fly.FinishBuildInput) (*fly.FinishBuildResponse, error) {
-		return &fly.FinishBuildResponse{
-			FinishBuild: fly.FinishBuildFinishBuildFinishBuildPayload{
-				Id:              "BUILD1",
-				Status:          "",
-				WallclockTimeMs: 2000,
-			},
-		}, nil
-	}
-	client.CreateReleaseFunc = func(ctx context.Context, input fly.CreateReleaseInput) (*fly.CreateReleaseResponse, error) {
-		return &fly.CreateReleaseResponse{}, nil
-	}
-	client.UpdateReleaseFunc = func(ctx context.Context, input fly.UpdateReleaseInput) (*fly.UpdateReleaseResponse, error) {
-		return &fly.UpdateReleaseResponse{}, nil
-	}
-	client.GetIPAddressesFunc = func(ctx context.Context, appName string) ([]fly.IPAddress, error) {
-		return nil, nil
+		return &fly.AppCompact{}, nil // TODO
 	}
 
-	flapsClient.ListFlyAppsMachinesFunc = func(ctx context.Context) ([]*fly.Machine, *fly.Machine, error) {
-		return nil, nil, nil // no machines
-	}
-	flapsClient.ListActiveFunc = func(ctx context.Context) ([]*fly.Machine, error) {
-		return nil, nil // no active machines
-	}
-	flapsClient.LaunchFunc = func(ctx context.Context, builder fly.LaunchMachineInput) (out *fly.Machine, err error) {
-		return &fly.Machine{
-			ID:     "m0",
-			Region: "ord",
-			Config: &fly.MachineConfig{},
-		}, nil
-	}
-	flapsClient.GetFunc = func(ctx context.Context, machineID string) (*fly.Machine, error) {
-		return &fly.Machine{
-			ID:     "m0",
-			Region: "ord",
-			Config: &fly.MachineConfig{},
-		}, nil
-	}
-	flapsClient.WaitFunc = func(ctx context.Context, machine *fly.Machine, state string, timeout time.Duration) (err error) {
+	genqlient.MakeRequestFunc = func(ctx context.Context, req *genq.Request, resp *genq.Response) error {
+		vars, _ := json.Marshal(req.Variables)
+
+		switch req.OpName {
+		case "ResolverCreateBuild":
+			if got, want := string(vars), `-`; got != want {
+				t.Fatalf("unexpected vars: %s", vars)
+			}
+			// resp.Data =
+		}
 		return nil
 	}
-	flapsClient.GetProcessesFunc = func(ctx context.Context, machineID string) (fly.MachinePsResponse, error) {
-		return nil, nil
-	}
 
 	if err := cmd.ExecuteContext(ctx); err != nil {
 		t.Fatal(err)
@@ -144,22 +103,3 @@ func copyFS(fsys fs.FS, dst string) error {
 		return os.WriteFile(target, b, 0o666)
 	})
 }
-
-func chdir(tb testing.TB, dir string) {
-	tb.Helper()
-
-	prev, err := os.Getwd()
-	if err != nil {
-		tb.Fatalf("cannot read working directory: %s", err)
-	}
-	if err := os.Chdir(dir); err != nil {
-		tb.Fatal(err)
-	}
-
-	tb.Cleanup(func() {
-		tb.Helper()
-		if err := os.Chdir(prev); err != nil {
-			tb.Fatalf("cannot revert working directory: %s", err)
-		}
-	})
-}