detached faec534

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 {
3434
3535 GetStatus(ctx context.Context) (*remote.Status, error)
3636 Reload(ctx context.Context) error
37+ DeployIntent(ctx context.Context, imageName string) error
3738 Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error)
3839 Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error)
3940 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) error36 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 {
223223 return app.SendReloadSignal()
224224 }
225225
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+
226233 func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) {
227234 if l.containerSvc != nil && l.configSvc != nil {
228235 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 {
8888 return r.client.Reload(ctx)
8989 }
9090
91+func (r *remoteControlPlane) DeployIntent(ctx context.Context, imageName string) error {
92+ return r.client.DeployIntent(ctx, imageName)
93+}
94+
9195 func (r *remoteControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) {
9296 return r.client.Deploy(ctx, deployDomain)
9397 }
@@ -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,
154154 }
155155 fmt.Printf("Domain: %s\n", styles.Theme.Bold.Render(pushDomain))
156156
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+
157166 if build {
158167 if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {
159168 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 err168 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
724724 return &result, nil
725725 }
726726
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+
727741 // Restart API
728742
729743 // RestartResult contains the result of a restart.
@@ -724,6 +724,20 @@ func (c *Client) Deploy(ctx context.Context, deployDomain string) (*DeployResult
724 return &result, nil724 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 API741 // Restart API
728 742
729 // RestartResult contains the result of a restart.743 // RestartResult contains the result of a restart.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close