Repository: superfly/flyctl
Pull request #3582 (2024-05-28 20:52:19)

Implement mock deploy test
### Change Summary

This pull request creates a simple mock test of `fly deploy`. It moves a bunch of GraphQL calls out of `flyctl` over to `fly-go` ([#64](https://github.com/superfly/fly-go/pull/64)) so they can be mocked.

Mocking is tedious—especially for all the failure testing we need to do. The next step is to make a simplified, local, in-memory version of the Fly API so we can simulate a backend and inject failures into that as needed.


---

### Documentation

- [ ] Fresh Produce
- [ ] In superfly/docs, or asked for help from docs team
- [x] n/a



Patch:
diff --git a/go.mod b/go.mod
index 3863ccd0d..3c7a889b9 100644
--- a/go.mod
+++ b/go.mod
@@ -64,7 +64,7 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.18.2
 	github.com/stretchr/testify v1.9.0
-	github.com/superfly/fly-go v0.1.12
+	github.com/superfly/fly-go v0.1.13
 	github.com/superfly/graphql v0.2.4
 	github.com/superfly/lfsc-go v0.1.1
 	github.com/superfly/macaroon v0.2.13
diff --git a/go.sum b/go.sum
index d9b566fcb..17901334b 100644
--- a/go.sum
+++ b/go.sum
@@ -616,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.12 h1:Ql+wbDFuiWEGK+qskuSTtMft/zfh1rIBp/1Dcu7JbgQ=
-github.com/superfly/fly-go v0.1.12/go.mod h1:0wgmVsqPPDgPSTsZA0L0zI5uajxx86KfuBoPQT87B1E=
+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/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=
diff --git a/gql/generated.go b/gql/generated.go
index a29339dd0..01d866342 100644
--- a/gql/generated.go
+++ b/gql/generated.go
@@ -555,181 +555,6 @@ 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.
 //
@@ -1034,40 +859,6 @@ 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.
 //
@@ -1202,40 +993,6 @@ 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.
 //
@@ -1283,23 +1040,6 @@ 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
@@ -1409,62 +1149,6 @@ 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
@@ -1495,47 +1179,6 @@ 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:"-"`
@@ -2914,77 +2557,6 @@ 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"`
@@ -3062,67 +2634,6 @@ 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 (
@@ -3350,25 +2861,6 @@ 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"`
@@ -3483,14 +2975,6 @@ 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"`
@@ -3579,22 +3063,6 @@ 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"`
@@ -3603,22 +3071,6 @@ 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"`
@@ -4091,45 +3543,6 @@ 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) {
@@ -4717,81 +4130,6 @@ 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!) {
@@ -4829,79 +4167,6 @@ 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/internal/build/imgsrc/resolver.go b/internal/build/imgsrc/resolver.go
index c23992346..04d0e818d 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 := &gql.BuildImageOptsInput{
+	imageOps := &fly.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 := &gql.BuildImageOptsInput{
+	imageOpts := &fly.BuildImageOptsInput{
 		BuildArgs:       opts.BuildArgs,
 		BuildPacks:      opts.Buildpacks,
 		Builder:         opts.Builder,
@@ -308,31 +308,24 @@ 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 *gql.BuildImageOptsInput) (*build, error) {
+func (r *Resolver) createBuildGql(ctx context.Context, strategiesAvailable []string, imageOpts *fly.BuildImageOptsInput) (*build, error) {
 	ctx, span := tracing.GetTracer().Start(ctx, "web.create_build")
 	defer span.End()
 
-	gqlClient := flyutil.ClientFromContext(ctx).GenqClient()
-	_ = `# @genqlient
-	mutation ResolverCreateBuild($input:CreateBuildInput!) {
-		createBuild(input:$input) {
-			id
-			status
-		}
-	}
-	`
+	client := flyutil.ClientFromContext(ctx)
+
 	builderType := "local"
 	if r.dockerFactory.remote {
 		builderType = "remote"
 	}
-	input := gql.CreateBuildInput{
+	input := fly.CreateBuildInput{
 		AppName:             r.dockerFactory.appName,
 		BuilderType:         builderType,
 		ImageOpts:           *imageOpts,
 		MachineId:           "",
 		StrategiesAvailable: strategiesAvailable,
 	}
-	resp, err := gql.ResolverCreateBuild(ctx, gqlClient, input)
+	resp, err := client.CreateBuild(ctx, input)
 	if err != nil {
 		var gqlErr *gqlerror.Error
 		isAppNotFoundErr := errors.As(err, &gqlErr) && gqlErr.Path.String() == "createBuild" && gqlErr.Message == "Could not find App"
@@ -370,10 +363,10 @@ func limitLogs(log string) string {
 type build struct {
 	CreateApiFailed bool
 	BuildId         string
-	BuilderMeta     *gql.BuilderMetaInput
-	StrategyResults []gql.BuildStrategyAttemptInput
-	Timings         *gql.BuildTimingsInput
-	StartTimes      *gql.BuildTimingsInput
+	BuilderMeta     *fly.BuilderMetaInput
+	StrategyResults []fly.BuildStrategyAttemptInput
+	Timings         *fly.BuildTimingsInput
+	StartTimes      *fly.BuildTimingsInput
 }
 
 func newFailedBuild() *build {
@@ -384,10 +377,10 @@ func newBuild(buildId string, createApiFailed bool) *build {
 	return &build{
 		CreateApiFailed: createApiFailed,
 		BuildId:         buildId,
-		BuilderMeta:     &gql.BuilderMetaInput{},
-		StrategyResults: make([]gql.BuildStrategyAttemptInput, 0),
-		StartTimes:      &gql.BuildTimingsInput{},
-		Timings: &gql.BuildTimingsInput{
+		BuilderMeta:     &fly.BuilderMetaInput{},
+		StrategyResults: make([]fly.BuildStrategyAttemptInput, 0),
+		StartTimes:      &fly.BuildTimingsInput{},
+		Timings: &fly.BuildTimingsInput{
 			BuildAndPushMs: -1,
 			BuilderInitMs:  -1,
 			BuildMs:        -1,
@@ -419,8 +412,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 = &gql.BuildTimingsInput{}
-	b.Timings = &gql.BuildTimingsInput{
+	b.StartTimes = &fly.BuildTimingsInput{}
+	b.Timings = &fly.BuildTimingsInput{
 		BuildAndPushMs: -1,
 		BuilderInitMs:  -1,
 		BuildMs:        -1,
@@ -487,7 +480,7 @@ func (b *build) finishStrategyCommon(strategy string, failed bool, err error, no
 	if err != nil {
 		errMsg = err.Error()
 	}
-	r := gql.BuildStrategyAttemptInput{
+	r := fly.BuildStrategyAttemptInput{
 		Strategy: strategy,
 		Result:   result,
 		Error:    limitLogs(errMsg),
@@ -515,21 +508,13 @@ 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
 	}
-	gqlClient := flyutil.ClientFromContext(ctx).GenqClient()
-	_ = `# @genqlient
-	mutation ResolverFinishBuild($input:FinishBuildInput!) {
-		finishBuild(input:$input) {
-			id
-			status
-			wallclockTimeMs
-		}
-	}
-	`
+	client := flyutil.ClientFromContext(ctx)
+
 	status := "failed"
 	if !failed {
 		status = "completed"
 	}
-	input := gql.FinishBuildInput{
+	input := fly.FinishBuildInput{
 		BuildId:             build.BuildId,
 		AppName:             r.dockerFactory.appName,
 		MachineId:           "",
@@ -540,13 +525,13 @@ func (r *Resolver) finishBuild(ctx context.Context, build *build, failed bool, l
 		Timings:             *build.Timings,
 	}
 	if img != nil {
-		input.FinalImage = gql.BuildFinalImageInput{
+		input.FinalImage = fly.BuildFinalImageInput{
 			Id:        img.ID,
 			Tag:       img.Tag,
 			SizeBytes: img.Size,
 		}
 	}
-	resp, err := gql.ResolverFinishBuild(ctx, gqlClient, input)
+	resp, err := client.FinishBuild(ctx, input)
 	if err != nil {
 		terminal.Warnf("failed to finish build in graphql: %v\n", err)
 		sentry.CaptureException(err,
diff --git a/internal/command/deploy/machines.go b/internal/command/deploy/machines.go
index 11bb680b9..b8e2bb705 100644
--- a/internal/command/deploy/machines.go
+++ b/internal/command/deploy/machines.go
@@ -9,14 +9,12 @@ 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"
@@ -78,8 +76,7 @@ type MachineDeploymentArgs struct {
 
 type machineDeployment struct {
 	apiClient             flyutil.Client
-	gqlClient             graphql.Client
-	flapsClient           *flaps.Client
+	flapsClient           flapsutil.FlapsClient
 	io                    *iostreams.IOStreams
 	colorize              *iostreams.ColorScheme
 	app                   *fly.AppCompact
@@ -116,7 +113,7 @@ type machineDeployment struct {
 	volumeInitialSize     int
 }
 
-func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (MachineDeployment, error) {
+func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (_ MachineDeployment, err error) {
 	ctx, span := tracing.GetTracer().Start(ctx, "new_machines_deployment")
 	defer span.End()
 
@@ -142,13 +139,17 @@ func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (Mach
 	if args.AppCompact == nil {
 		return nil, fmt.Errorf("BUG: args.AppCompact should be set when calling this method")
 	}
-	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 := 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
+		}
 	}
 
 	if appConfig.Deploy != nil {
@@ -207,7 +208,6 @@ func NewMachineDeployment(ctx context.Context, args MachineDeploymentArgs) (Mach
 
 	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.latestImage(ctx)
+	latestImg, err := md.apiClient.LatestImage(ctx, md.app.Name)
 	if err == nil {
 		md.img = latestImg
 		return nil
@@ -521,28 +521,6 @@ 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 != "" {
@@ -555,24 +533,13 @@ func (md *machineDeployment) createReleaseInBackend(ctx context.Context) error {
 	ctx, span := tracing.GetTracer().Start(ctx, "create_backend_release")
 	defer span.End()
 
-	_ = `# @genqlient
-	mutation MachinesCreateRelease($input:CreateReleaseInput!) {
-		createRelease(input:$input) {
-			release {
-				id
-				version
-			}
-		}
-	}
-	`
-	input := gql.CreateReleaseInput{
+	resp, err := md.apiClient.CreateRelease(ctx, fly.CreateReleaseInput{
 		AppId:           md.app.Name,
 		PlatformVersion: "machines",
-		Strategy:        gql.DeploymentStrategy(strings.ToUpper(md.strategy)),
+		Strategy:        fly.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
@@ -589,20 +556,10 @@ func (md *machineDeployment) updateReleaseInBackend(ctx context.Context, status
 	))
 	defer span.End()
 
-	_ = `# @genqlient
-	mutation MachinesUpdateRelease($input:UpdateReleaseInput!) {
-		updateRelease(input:$input) {
-			release {
-				id
-			}
-		}
-	}
-	`
-	input := gql.UpdateReleaseInput{
+	_, err := md.apiClient.UpdateRelease(ctx, fly.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 303be1fe0..77b1fc741 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               *flaps.Client
+	flaps               flapsutil.FlapsClient
 	apiClient           flyutil.Client
 	io                  *iostreams.IOStreams
 	colorize            *iostreams.ColorScheme
diff --git a/internal/flyutil/client.go b/internal/flyutil/client.go
index 3b9e979d3..e294ea048 100644
--- a/internal/flyutil/client.go
+++ b/internal/flyutil/client.go
@@ -25,11 +25,13 @@ 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)
@@ -40,6 +42,7 @@ 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)
@@ -74,6 +77,7 @@ 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)
@@ -87,6 +91,7 @@ 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/mock/client.go b/internal/mock/client.go
index 720101bae..fb71631e6 100644
--- a/internal/mock/client.go
+++ b/internal/mock/client.go
@@ -26,11 +26,13 @@ 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)
@@ -41,6 +43,7 @@ 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)
@@ -73,6 +76,7 @@ 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)
@@ -88,6 +92,7 @@ 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)
 }
@@ -140,6 +145,10 @@ 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)
 }
@@ -160,6 +169,10 @@ 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)
 }
@@ -200,6 +213,10 @@ 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)
 }
@@ -328,6 +345,10 @@ 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)
 }
@@ -388,6 +409,10 @@ 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
deleted file mode 100644
index 0a29c422d..000000000
--- a/internal/mock/graphql_client.go
+++ /dev/null
@@ -1,15 +0,0 @@
-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)
-}


Test patch:
diff --git a/internal/command/deploy/deploy_test.go b/internal/command/deploy/deploy_test.go
index e4b98796b..7031296a3 100644
--- a/internal/command/deploy/deploy_test.go
+++ b/internal/command/deploy/deploy_test.go
@@ -4,13 +4,12 @@ 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"
@@ -25,16 +24,12 @@ 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)
 	}
-	if err := os.Chdir(dir); err != nil { // TODO: Revert working directory
-		t.Fatal(err)
-	}
+	chdir(t, dir)
 
 	var buf bytes.Buffer
 	cmd := deploy.New()
@@ -47,9 +42,7 @@ 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
@@ -63,21 +56,69 @@ 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{}, nil // TODO
+		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
 	}
 
-	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 =
-		}
+	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) {
 		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)
@@ -103,3 +144,22 @@ 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)
+		}
+	})
+}