feat(cli): call deploy-intent before push to prevent event-based double deploy
- commit
- faec5345f8720c0e09dbd52570be9b3b086d89cc
- author
- brice · · 2026-03-04 06:41 UTC
- parent
- 8365f31
- signature
- unsigned
5 files · +35 -0
M internal/adapters/in/cli/controlplane.go +1 -0
| @@ -34,6 +34,7 @@ type ControlPlane interface { | |||
| 34 | 34 | ||
| 35 | 35 | GetStatus(ctx context.Context) (*remote.Status, error) | |
| 36 | 36 | Reload(ctx context.Context) error | |
| 37 | + | DeployIntent(ctx context.Context, imageName string) error | |
| 37 | 38 | Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) | |
| 38 | 39 | Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) | |
| 39 | 40 | ListTags(ctx context.Context, repository string) ([]string, error) | |
| @@ -34,6 +34,7 @@ type ControlPlane interface { | |||||
| 34 | 34 | ||||
| 35 | GetStatus(ctx context.Context) (*remote.Status, error) | 35 | GetStatus(ctx context.Context) (*remote.Status, error) | ||
| 36 | Reload(ctx context.Context) error | 36 | Reload(ctx context.Context) error | ||
| 37 | + | DeployIntent(ctx context.Context, imageName string) error | |||
| 37 | Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) | 38 | Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) | ||
| 38 | Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) | 39 | Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) | ||
| 39 | ListTags(ctx context.Context, repository string) ([]string, error) | 40 | ListTags(ctx context.Context, repository string) ([]string, error) | ||
M internal/adapters/in/cli/controlplane_local.go +7 -0
| @@ -223,6 +223,13 @@ func (l *localControlPlane) Reload(_ context.Context) error { | |||
| 223 | 223 | return app.SendReloadSignal() | |
| 224 | 224 | } | |
| 225 | 225 | ||
| 226 | + | func (l *localControlPlane) DeployIntent(_ context.Context, imageName string) error { | |
| 227 | + | if l.registrySvc != nil { | |
| 228 | + | l.registrySvc.SuppressDeployEvent(imageName) | |
| 229 | + | } | |
| 230 | + | return nil | |
| 231 | + | } | |
| 232 | + | ||
| 226 | 233 | func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) { | |
| 227 | 234 | if l.containerSvc != nil && l.configSvc != nil { | |
| 228 | 235 | route, err := l.configSvc.GetRoute(ctx, deployDomain) | |
| @@ -223,6 +223,13 @@ func (l *localControlPlane) Reload(_ context.Context) error { | |||||
| 223 | return app.SendReloadSignal() | 223 | return app.SendReloadSignal() | ||
| 224 | } | 224 | } | ||
| 225 | 225 | ||||
| 226 | + | func (l *localControlPlane) DeployIntent(_ context.Context, imageName string) error { | |||
| 227 | + | if l.registrySvc != nil { | |||
| 228 | + | l.registrySvc.SuppressDeployEvent(imageName) | |||
| 229 | + | } | |||
| 230 | + | return nil | |||
| 231 | + | } | |||
| 232 | + | ||||
| 226 | func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) { | 233 | func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) { | ||
| 227 | if l.containerSvc != nil && l.configSvc != nil { | 234 | if l.containerSvc != nil && l.configSvc != nil { | ||
| 228 | route, err := l.configSvc.GetRoute(ctx, deployDomain) | 235 | route, err := l.configSvc.GetRoute(ctx, deployDomain) | ||
M internal/adapters/in/cli/controlplane_remote.go +4 -0
| @@ -88,6 +88,10 @@ func (r *remoteControlPlane) Reload(ctx context.Context) error { | |||
| 88 | 88 | return r.client.Reload(ctx) | |
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | + | func (r *remoteControlPlane) DeployIntent(ctx context.Context, imageName string) error { | |
| 92 | + | return r.client.DeployIntent(ctx, imageName) | |
| 93 | + | } | |
| 94 | + | ||
| 91 | 95 | func (r *remoteControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) { | |
| 92 | 96 | return r.client.Deploy(ctx, deployDomain) | |
| 93 | 97 | } | |
| @@ -88,6 +88,10 @@ func (r *remoteControlPlane) Reload(ctx context.Context) error { | |||||
| 88 | return r.client.Reload(ctx) | 88 | return r.client.Reload(ctx) | ||
| 89 | } | 89 | } | ||
| 90 | 90 | ||||
| 91 | + | func (r *remoteControlPlane) DeployIntent(ctx context.Context, imageName string) error { | |||
| 92 | + | return r.client.DeployIntent(ctx, imageName) | |||
| 93 | + | } | |||
| 94 | + | ||||
| 91 | func (r *remoteControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) { | 95 | func (r *remoteControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) { | ||
| 92 | return r.client.Deploy(ctx, deployDomain) | 96 | return r.client.Deploy(ctx, deployDomain) | ||
| 93 | } | 97 | } | ||
M internal/adapters/in/cli/push.go +9 -0
| @@ -154,6 +154,15 @@ func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool, | |||
| 154 | 154 | } | |
| 155 | 155 | fmt.Printf("Domain: %s\n", styles.Theme.Bold.Render(pushDomain)) | |
| 156 | 156 | ||
| 157 | + | // Signal the server to suppress event-based deploys for this image. | |
| 158 | + | // The CLI will trigger an explicit deploy after push completes. | |
| 159 | + | if !noDeploy { | |
| 160 | + | if err := handle.plane.DeployIntent(ctx, imageName); err != nil { | |
| 161 | + | // Non-fatal: worst case we get a redundant deploy via event | |
| 162 | + | fmt.Fprintf(os.Stderr, "warning: failed to register deploy intent: %v\n", err) | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 157 | 166 | if build { | |
| 158 | 167 | if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil { | |
| 159 | 168 | return err | |
| @@ -154,6 +154,15 @@ func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool, | |||||
| 154 | } | 154 | } | ||
| 155 | fmt.Printf("Domain: %s\n", styles.Theme.Bold.Render(pushDomain)) | 155 | fmt.Printf("Domain: %s\n", styles.Theme.Bold.Render(pushDomain)) | ||
| 156 | 156 | ||||
| 157 | + | // Signal the server to suppress event-based deploys for this image. | |||
| 158 | + | // The CLI will trigger an explicit deploy after push completes. | |||
| 159 | + | if !noDeploy { | |||
| 160 | + | if err := handle.plane.DeployIntent(ctx, imageName); err != nil { | |||
| 161 | + | // Non-fatal: worst case we get a redundant deploy via event | |||
| 162 | + | fmt.Fprintf(os.Stderr, "warning: failed to register deploy intent: %v\n", err) | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | ||||
| 157 | if build { | 166 | if build { | ||
| 158 | if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil { | 167 | if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil { | ||
| 159 | return err | 168 | return err | ||
M internal/adapters/in/cli/remote/client.go +14 -0
| @@ -724,6 +724,20 @@ func (c *Client) Deploy(ctx context.Context, deployDomain string) (*DeployResult | |||
| 724 | 724 | return &result, nil | |
| 725 | 725 | } | |
| 726 | 726 | ||
| 727 | + | // DeployIntent tells the server that a CLI-managed push is about to happen, | |
| 728 | + | // suppressing event-based deploys for this image. | |
| 729 | + | func (c *Client) DeployIntent(ctx context.Context, imageName string) error { | |
| 730 | + | resp, err := c.requestWithRetry(ctx, http.MethodPost, "/deploy-intent/"+url.PathEscape(imageName), nil) | |
| 731 | + | if err != nil { | |
| 732 | + | return err | |
| 733 | + | } | |
| 734 | + | defer resp.Body.Close() | |
| 735 | + | if resp.StatusCode != http.StatusOK { | |
| 736 | + | return fmt.Errorf("deploy-intent failed: %s", resp.Status) | |
| 737 | + | } | |
| 738 | + | return nil | |
| 739 | + | } | |
| 740 | + | ||
| 727 | 741 | // Restart API | |
| 728 | 742 | ||
| 729 | 743 | // RestartResult contains the result of a restart. | |
| @@ -724,6 +724,20 @@ func (c *Client) Deploy(ctx context.Context, deployDomain string) (*DeployResult | |||||
| 724 | return &result, nil | 724 | return &result, nil | ||
| 725 | } | 725 | } | ||
| 726 | 726 | ||||
| 727 | + | // DeployIntent tells the server that a CLI-managed push is about to happen, | |||
| 728 | + | // suppressing event-based deploys for this image. | |||
| 729 | + | func (c *Client) DeployIntent(ctx context.Context, imageName string) error { | |||
| 730 | + | resp, err := c.requestWithRetry(ctx, http.MethodPost, "/deploy-intent/"+url.PathEscape(imageName), nil) | |||
| 731 | + | if err != nil { | |||
| 732 | + | return err | |||
| 733 | + | } | |||
| 734 | + | defer resp.Body.Close() | |||
| 735 | + | if resp.StatusCode != http.StatusOK { | |||
| 736 | + | return fmt.Errorf("deploy-intent failed: %s", resp.Status) | |||
| 737 | + | } | |||
| 738 | + | return nil | |||
| 739 | + | } | |||
| 740 | + | ||||
| 727 | // Restart API | 741 | // Restart API | ||
| 728 | 742 | ||||
| 729 | // RestartResult contains the result of a restart. | 743 | // RestartResult contains the result of a restart. | ||