fix(artifacts-init): preserve user-supplied remote URL scheme under --push-protocol auto

The original git_protocol detection keyed only on --host, so it only ran
for remotes gstack creates via gh/glab. A URL the user hands us through
--remote, an existing origin, or a manual paste fell through to the SSH
default — silently rewriting an explicitly-configured HTTPS remote to
SSH. That's the July report on #1348: a user whose recovery URL was
HTTPS got it converted back to SSH and the push failed.

Track where REMOTE_URL came from. Under auto:
- explicit / existing / manual: keep the scheme the URL already states
  (git@|ssh:// -> ssh, http(s):// -> https, anything else preserved
  verbatim). Never consult git_protocol for a URL the user supplied.
- provider (gh/glab-created): honor git_protocol as before, SSH fallback.

Explicit --push-protocol https|ssh still overrides everything. The
reachability error now has a third branch for pass-through URLs.

Refs #1348.
This commit is contained in:
0xDevNinja 2026-07-16 14:14:09 +05:30
parent 78aaa2017f
commit c454e2a263
2 changed files with 119 additions and 32 deletions

View File

@ -12,9 +12,11 @@
# [--push-protocol auto|https|ssh]
#
# --push-protocol controls the URL form used for `git push origin`:
# auto (default) — honors `gh config get git_protocol` (github) or
# `glab config get git_protocol` (gitlab). Falls back to
# SSH when neither is set / readable.
# auto (default) — for a remote you supplied (--remote, an existing origin, or
# a manually pasted URL), keeps that URL's own protocol.
# For a remote gstack creates via gh/glab, honors
# `gh config get git_protocol` / `glab config get git_protocol`,
# falling back to SSH when unset / unreadable.
# https — force HTTPS (requires gh/glab credential helper or PAT).
# ssh — force SSH (legacy default; requires SSH key on remote).
#
@ -52,9 +54,13 @@ REMOTE_URL=""
HOST_PREF=""
URL_FORM_SUPPORTED="false"
PUSH_PROTOCOL="auto"
# Where REMOTE_URL came from. Only `provider` (a remote we created via gh/glab)
# gets its push form from the CLI's git_protocol — a URL the user handed us
# already states its own protocol, and rewriting it is what broke #1348.
REMOTE_SOURCE="provider"
while [ $# -gt 0 ]; do
case "$1" in
--remote) REMOTE_URL="$2"; shift 2 ;;
--remote) REMOTE_URL="$2"; REMOTE_SOURCE="explicit"; shift 2 ;;
--host) HOST_PREF="$2"; shift 2 ;;
--url-form-supported) URL_FORM_SUPPORTED="$2"; shift 2 ;;
--push-protocol)
@ -103,6 +109,7 @@ if command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1; then gla
# ---- choose remote URL ----
if [ -z "$REMOTE_URL" ] && [ -n "$EXISTING_REMOTE" ]; then
REMOTE_URL="$EXISTING_REMOTE"
REMOTE_SOURCE="existing"
echo "Using existing remote: $REMOTE_URL"
fi
@ -178,6 +185,7 @@ if [ -z "$REMOTE_URL" ]; then
echo "No URL provided. Aborting." >&2
exit 1
fi
REMOTE_SOURCE="manual"
;;
*) echo "Unknown --host: $HOST_PREF (expected github|gitlab|manual)" >&2; exit 1 ;;
esac
@ -193,42 +201,63 @@ if [ -z "$CANONICAL_HTTPS" ]; then
CANONICAL_HTTPS="$REMOTE_URL"
fi
# Resolve push protocol. `auto` honors the user's CLI git_protocol setting
# (gh / glab); explicit `https` / `ssh` skips detection. SSH is the historical
# default for repeated pushes (no token refresh dance), but HTTPS-configured
# users hit a hard wall when we force SSH and they lack an SSH key on the
# remote — see issue #1348.
# Resolve push protocol. `auto` keys off where the URL came from:
#
# explicit / existing / manual — the user already chose a protocol by handing
# us that URL. Keep it. Rewriting an explicitly-configured HTTPS remote to
# SSH is the exact failure reported in #1348, and consulting git_protocol
# here would reintroduce it for anyone whose CLI disagrees with their URL.
# Forms we don't recognize (local bare paths, file://, self-hosted) pass
# through verbatim rather than being coerced.
#
# provider — we created this remote via gh/glab, so there is no user intent to
# preserve. Honor the CLI's git_protocol. Unset/unreadable falls back to SSH,
# the historical default, so users who never set git_protocol see no change.
#
# Explicit `--push-protocol https|ssh` always wins over all of this.
RESOLVED_PROTOCOL="$PUSH_PROTOCOL"
if [ "$RESOLVED_PROTOCOL" = "auto" ]; then
case "$HOST_PREF" in
github)
DETECTED=$(gh config get git_protocol 2>/dev/null || echo "")
case "$REMOTE_SOURCE" in
explicit|existing|manual)
case "$REMOTE_URL" in
git@*|ssh://*) RESOLVED_PROTOCOL="ssh" ;;
http://*|https://*) RESOLVED_PROTOCOL="https" ;;
*) RESOLVED_PROTOCOL="preserve" ;;
esac
;;
gitlab)
DETECTED=$(glab config get git_protocol 2>/dev/null || echo "")
provider)
case "$HOST_PREF" in
github)
DETECTED=$(gh config get git_protocol 2>/dev/null || echo "")
;;
gitlab)
DETECTED=$(glab config get git_protocol 2>/dev/null || echo "")
;;
*)
DETECTED=""
;;
esac
case "$DETECTED" in
https) RESOLVED_PROTOCOL="https" ;;
ssh) RESOLVED_PROTOCOL="ssh" ;;
*) RESOLVED_PROTOCOL="ssh" ;; # historical default when unset/unknown
esac
;;
*)
DETECTED=""
;;
esac
case "$DETECTED" in
https) RESOLVED_PROTOCOL="https" ;;
ssh) RESOLVED_PROTOCOL="ssh" ;;
*) RESOLVED_PROTOCOL="ssh" ;; # historical default when unset/unknown
esac
fi
if [ "$RESOLVED_PROTOCOL" = "https" ]; then
PUSH_URL="$CANONICAL_HTTPS"
else
PUSH_URL=$("$URL_BIN" --to ssh "$CANONICAL_HTTPS" 2>/dev/null || echo "$CANONICAL_HTTPS")
fi
case "$RESOLVED_PROTOCOL" in
preserve) PUSH_URL="$REMOTE_URL" ;;
https) PUSH_URL="$CANONICAL_HTTPS" ;;
*) PUSH_URL=$("$URL_BIN" --to ssh "$CANONICAL_HTTPS" 2>/dev/null || echo "$CANONICAL_HTTPS") ;;
esac
# ---- verify push URL is reachable ----
echo "Verifying remote connectivity: $PUSH_URL"
if ! git ls-remote "$PUSH_URL" >/dev/null 2>&1; then
if [ "$RESOLVED_PROTOCOL" = "ssh" ]; then
cat >&2 <<EOF
case "$RESOLVED_PROTOCOL" in
ssh)
cat >&2 <<EOF
Remote not reachable via SSH: $PUSH_URL
This could mean:
- Wrong URL
@ -237,8 +266,9 @@ This could mean:
- You use HTTPS for git (gh config get git_protocol = https) — re-run with --push-protocol https
Fix and re-run gstack-artifacts-init.
EOF
else
cat >&2 <<EOF
;;
https)
cat >&2 <<EOF
Remote not reachable via HTTPS: $PUSH_URL
This could mean:
- Wrong URL
@ -246,7 +276,19 @@ This could mean:
- Network issue
Fix and re-run gstack-artifacts-init.
EOF
fi
;;
*)
cat >&2 <<EOF
Remote not reachable: $PUSH_URL
This URL isn't a recognized SSH or HTTPS form, so it was used as given.
This could mean:
- Wrong URL
- Credentials not configured for this remote
- Network issue
Fix and re-run gstack-artifacts-init, or pin a form with --push-protocol https|ssh.
EOF
;;
esac
exit 1
fi

View File

@ -377,6 +377,51 @@ describe('gstack-artifacts-init push protocol (issue #1348)', () => {
});
});
describe('gstack-artifacts-init explicit-remote protocol preservation (issue #1348)', () => {
// The provider-created path (--host github/gitlab) reads git_protocol. A URL
// the USER supplies already states its own protocol, so `auto` must keep it
// rather than rewrite it. The original fix keyed only on --host, so an
// explicit `--remote https://...` with no --host fell through to the SSH
// default and silently converted the user's HTTPS URL to SSH.
test('--remote HTTPS (no --host) keeps HTTPS instead of defaulting to SSH', () => {
makeFakeGh({});
const r = run(['--remote', 'https://github.com/testuser/gstack-artifacts-testuser.git']);
if (r.status !== 0) console.error('STDERR:', r.stderr);
expect(r.status).toBe(0);
const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }).stdout.trim();
expect(remote.startsWith('https://')).toBe(true);
expect(remote.startsWith('git@')).toBe(false);
});
test('--remote SSH keeps SSH', () => {
makeFakeGh({});
const r = run(['--remote', 'git@github.com:testuser/gstack-artifacts-testuser.git']);
expect(r.status).toBe(0);
const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }).stdout.trim();
expect(remote.startsWith('git@')).toBe(true);
});
test('explicit HTTPS --remote is NOT overridden by ambient gh git_protocol=ssh', () => {
// The strongest form of the regression: even with a git_protocol=ssh config
// reachable on PATH, a user-supplied HTTPS URL must win. Consulting config
// here is exactly what would reintroduce #1348.
makeFakeGh({ gitProtocol: 'ssh' });
const r = run(['--remote', 'https://github.com/testuser/gstack-artifacts-testuser.git']);
expect(r.status).toBe(0);
const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }).stdout.trim();
expect(remote.startsWith('https://')).toBe(true);
expect(remote.startsWith('git@')).toBe(false);
});
test('--push-protocol ssh still overrides an explicit HTTPS --remote', () => {
makeFakeGh({});
const r = run(['--remote', 'https://github.com/testuser/gstack-artifacts-testuser.git', '--push-protocol', 'ssh']);
expect(r.status).toBe(0);
const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }).stdout.trim();
expect(remote.startsWith('git@')).toBe(true);
});
});
describe('gstack-artifacts-init idempotency', () => {
test('--remote <url> bypasses provider selection entirely', () => {
makeFakeGh({});