fix(checkpoint): ssh-agent hint on git-refs BatchMode auth failure · Entire
fix(checkpoint): ssh-agent hint on git-refs BatchMode auth failure
960335f→main·
suhaanthayyil·2d ago·3 files·+37 added/-4 removed
Surface the same actionable hint from flushCheckpointRefsQueue that doPushRef prints on the v1 path, and skip useless per-ref recovery when SSH auth failed under non-interactive BatchMode.
Co-authored-by: Cursor cursoragent@cursor.com
Changes
3
cmd/entire/cli/strategy
Mmanual_commit_push.go+15/-1
Mpush_common.go+10/-3
Mpush_common_test.go+12
359 unmodified lines
360
361
362
363
363
364
365
366
367
3 unmodified lines
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
7 unmodified lines
394
395
396
397
398
399
400
401
402
359 unmodified lines
// Fast path: push all refs in one round-trip (fast-forward-only). If every
// ref was up to date or fast-forwarded, we're done.
if err := batchPushRefs(pushCtx, pushTarget, existing); err == nil {
batchErr := batchPushRefs(pushCtx, pushTarget, existing)
if batchErr == nil {
stop(" done")
if removeErr := queue.Remove(existing); removeErr != nil {
logging.Warn(ctx, "git-refs push: clear pushed refs from queue failed",
}
}
stop("")
// Non-interactive SSH auth failures cannot be fixed by per-ref
// fetch+replay. Surface the same actionable hint as the v1 doPushRef path
// (issue #1523) instead of only logging to .entire/logs/.
if nonInteractiveSSHAuthFailure(pushCtx, batchErr) {
fmt.Fprintf(os.Stderr, "[entire] Warning: couldn't push checkpoint refs: %v\n", batchErr)
printNonInteractiveSSHAuthHint()
printCheckpointRemoteHint(pushTarget)
return 0, batchErr
}
// At least one ref was rejected — typically a non-fast-forward divergence
// (the same checkpoint re-written on another machine). Retry per ref with
// fetch+replay recovery, and remove from the queue only the refs that land
7 unmodified lines
if err := pushCheckpointRefWithRecovery(pushCtx, pushTarget, ref); err != nil {
logging.Warn(ctx, "git-refs push: checkpoint ref push/sync failed; left queued, not overwritten",
slog.String("ref", ref.String()), slog.String("error", err.Error()))
if nonInteractiveSSHAuthFailure(pushCtx, err) {
printNonInteractiveSSHAuthHint()
}
if firstErr == nil {
firstErr = err
}
}
Mcmd/entire/cli/strategy/manual_commit_push.go+15/-1
180 unmodified lines
181
182
183
184
184
185
186
187
13 unmodified lines
201
202
203
204
204
205
206
207
8 unmodified lines
216
217
218
219
219
220
221
222
13 unmodified lines
236
237
238
239
240
241
242
243
244
245
246
247
248
180 unmodified lines
// Non-interactive SSH (pre-push BatchMode): auth failures cannot be fixed by
// fetch+rebase, and retrying would just reprint the same opaque error.
// Surface an actionable ssh-agent hint and skip recovery (issue #1523).
if remote.IsNonInteractiveSSH(ctx) && remote.LooksLikeSSHAuthFailure(err.Error()) {
if nonInteractiveSSHAuthFailure(ctx, err) {
fmt.Fprintf(os.Stderr, "[entire] Warning: couldn't push %s: %v\n", refLabel, err)
printNonInteractiveSSHAuthHint()
printCheckpointRemoteHint(target)
}
}
if syncErr != nil {
stop("")
fmt.Fprintf(os.Stderr, "[entire] Warning: couldn't sync %s: %v\n", refLabel, syncErr)
if remote.IsNonInteractiveSSH(ctx) && remote.LooksLikeSSHAuthFailure(syncErr.Error()) {
if nonInteractiveSSHAuthFailure(ctx, syncErr) {
printNonInteractiveSSHAuthHint()
}
printCheckpointRemoteHint(target)
}
}
if result, err := tryPushRefCommon(ctx, target, ref); err != nil {
stop("")
fmt.Fprintf(os.Stderr, "[entire] Warning: failed to push %s after sync: %v\n", refLabel, err)
if remote.IsNonInteractiveSSH(ctx) && remote.LooksLikeSSHAuthFailure(err.Error()) {
if nonInteractiveSSHAuthFailure(ctx, err) {
printNonInteractiveSSHAuthHint()
}
printCheckpointRemoteHint(target)
}
}
return ref.String()
}
// nonInteractiveSSHAuthFailure reports whether err is an SSH auth-shaped
// failure under a BatchMode (non-interactive) context. Used to print the
// actionable ssh-agent hint and skip useless recovery retries.
func nonInteractiveSSHAuthFailure(ctx context.Context, err error) bool {
return err != nil && remote.IsNonInteractiveSSH(ctx) && remote.LooksLikeSSHAuthFailure(err.Error())
}
// printCheckpointRemoteHint prints a hint when a push to a checkpoint URL fails.
// Only prints when the target is a URL (not the user's default remote).
func printCheckpointRemoteHint(target string) {
Mcmd/entire/cli/strategy/push_common.go+10/-3
12 unmodified lines
13
14
15
16
17
18
19
1670 unmodified lines
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
12 unmodified lines
"testing"
"github.com/entireio/cli/cmd/entire/cli/checkpoint"
"github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/testutil"
1670 unmodified lines
assert.Contains(t, out, "Checkpoint push skipped")
assert.Equal(t, 1, strings.Count(out, "Checkpoint push skipped"), "hint must print once")
}
func TestNonInteractiveSSHAuthFailure(t *testing.T) {
t.Parallel()
authErr := errors.New("git push: Permission denied (publickey).")
ctx := remote.WithNonInteractiveSSH(context.Background())
assert.True(t, nonInteractiveSSHAuthFailure(ctx, authErr))
assert.False(t, nonInteractiveSSHAuthFailure(context.Background(), authErr),
"interactive context must not treat auth errors as BatchMode hints")
assert.False(t, nonInteractiveSSHAuthFailure(ctx, errors.New("non-fast-forward")))
assert.False(t, nonInteractiveSSHAuthFailure(ctx, nil))
}