From c454e2a2634823165a638e0f48a3dc03f0f189b5 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Thu, 16 Jul 2026 14:14:09 +0530 Subject: [PATCH] fix(artifacts-init): preserve user-supplied remote URL scheme under --push-protocol auto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- bin/gstack-artifacts-init | 106 ++++++++++++++++++++--------- test/gstack-artifacts-init.test.ts | 45 ++++++++++++ 2 files changed, 119 insertions(+), 32 deletions(-) diff --git a/bin/gstack-artifacts-init b/bin/gstack-artifacts-init index e8249efba..695ef09bd 100755 --- a/bin/gstack-artifacts-init +++ b/bin/gstack-artifacts-init @@ -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 <&2 <&2 <&2 <&2 < { }); }); +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 bypasses provider selection entirely', () => { makeFakeGh({});