From 78aaa2017f57900ff77a54067f36f643a651699d Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Wed, 13 May 2026 15:30:05 +0530 Subject: [PATCH 1/2] fix(artifacts-init): honor gh/glab git_protocol when picking push URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gstack-artifacts-init` always converted the canonical HTTPS URL to its SSH form for `git push origin`. Users on `gh config set git_protocol https` (the `gh auth login` default) lack an SSH key on the remote, so the post-create `git ls-remote $PUSH_URL` verification fails with a cryptic SSH prompt and the script exits without finishing setup. Same hazard on the gitlab branch. Pre-fix the only recovery was rerunning with `--remote `, which bypassed the canonicalization step entirely and required the user to know the workaround. Resolution order for the push URL: 1. `--push-protocol https|ssh` flag wins. 2. `auto` (default) reads `gh config get git_protocol` (github branch) or `glab config get git_protocol` (gitlab branch). 3. Empty / unreadable config falls back to SSH — the historical default, kept so users who never set `git_protocol` see no change. Error message after a failed `ls-remote` also points HTTPS users at the override flag, so the next attempt is one rerun away. Fixes #1348 --- bin/gstack-artifacts-init | 61 ++++++++++++++++++++-- test/gstack-artifacts-init.test.ts | 82 +++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/bin/gstack-artifacts-init b/bin/gstack-artifacts-init index b8bfe830c..e8249efba 100755 --- a/bin/gstack-artifacts-init +++ b/bin/gstack-artifacts-init @@ -9,6 +9,14 @@ # Usage: # gstack-artifacts-init [--remote ] [--host github|gitlab|manual] # [--url-form-supported true|false] +# [--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. +# https — force HTTPS (requires gh/glab credential helper or PAT). +# ssh — force SSH (legacy default; requires SSH key on remote). # # Interactive by default. Pass --remote to skip the host prompt. # @@ -43,11 +51,17 @@ REMOTE_FILE="$HOME/.gstack-artifacts-remote.txt" REMOTE_URL="" HOST_PREF="" URL_FORM_SUPPORTED="false" +PUSH_PROTOCOL="auto" while [ $# -gt 0 ]; do case "$1" in --remote) REMOTE_URL="$2"; shift 2 ;; --host) HOST_PREF="$2"; shift 2 ;; --url-form-supported) URL_FORM_SUPPORTED="$2"; shift 2 ;; + --push-protocol) + case "$2" in + auto|https|ssh) PUSH_PROTOCOL="$2"; shift 2 ;; + *) echo "Invalid --push-protocol: $2 (expected auto|https|ssh)" >&2; exit 1 ;; + esac ;; --help|-h) sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Unknown flag: $1" >&2; exit 1 ;; esac @@ -179,21 +193,60 @@ if [ -z "$CANONICAL_HTTPS" ]; then CANONICAL_HTTPS="$REMOTE_URL" fi -# Use SSH for git push (more reliable for repeated pushes than HTTPS+token). -# Fall back to the canonical input if derivation fails. -PUSH_URL=$("$URL_BIN" --to ssh "$CANONICAL_HTTPS" 2>/dev/null || echo "$CANONICAL_HTTPS") +# 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. +RESOLVED_PROTOCOL="$PUSH_PROTOCOL" +if [ "$RESOLVED_PROTOCOL" = "auto" ]; then + 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 +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 # ---- verify push URL is reachable ---- echo "Verifying remote connectivity: $PUSH_URL" if ! git ls-remote "$PUSH_URL" >/dev/null 2>&1; then - cat >&2 <&2 <&2 <> "${ghCallLog}" case "$1" in auth) ${authStatus === 'ok' ? 'exit 0' : 'exit 1'} ;; + config) + # gh config get git_protocol + if [ "$2" = "get" ] && [ "$3" = "git_protocol" ]; then + ${gitProtocol ? `echo "${gitProtocol}"; exit 0` : 'exit 1'} + fi + exit 1 + ;; repo) shift case "$1" in @@ -65,14 +73,21 @@ exit 0 fs.writeFileSync(path.join(fakeBinDir, 'gh'), script, { mode: 0o755 }); } -function makeFakeGlab(opts: { authStatus?: 'ok' | 'fail'; repoCreate?: 'success' | 'fail'; webUrl?: string } = {}) { +function makeFakeGlab(opts: { authStatus?: 'ok' | 'fail'; repoCreate?: 'success' | 'fail'; webUrl?: string; gitProtocol?: 'https' | 'ssh' | '' } = {}) { const authStatus = opts.authStatus ?? 'ok'; const repoCreate = opts.repoCreate ?? 'success'; const webUrl = opts.webUrl ?? 'https://gitlab.com/testuser/gstack-artifacts-testuser'; + const gitProtocol = opts.gitProtocol ?? ''; const script = `#!/bin/bash echo "glab $@" >> "${glabCallLog}" case "$1" in auth) ${authStatus === 'ok' ? 'exit 0' : 'exit 1'} ;; + config) + if [ "$2" = "get" ] && [ "$3" = "git_protocol" ]; then + ${gitProtocol ? `echo "${gitProtocol}"; exit 0` : 'exit 1'} + fi + exit 1 + ;; repo) shift case "$1" in @@ -299,6 +314,69 @@ describe('gstack-artifacts-init brain-admin hookup printout (codex Finding #3)', }); }); +describe('gstack-artifacts-init push protocol (issue #1348)', () => { + test('--push-protocol=auto + gh git_protocol=https → origin uses HTTPS', () => { + // Issue #1348: users with `gh config set git_protocol https` (the + // `gh auth login` default) lack an SSH key on the remote, so forcing + // an SSH push URL fails at the ls-remote verification step. + makeFakeGh({ gitProtocol: 'https', webUrl: 'https://github.com/testuser/gstack-artifacts-testuser' }); + const r = run(['--host', 'github']); + 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' }); + expect(remote.stdout.trim()).toBe('https://github.com/testuser/gstack-artifacts-testuser'); + }); + + test('--push-protocol=auto + gh git_protocol=ssh → origin uses SSH (existing default)', () => { + makeFakeGh({ gitProtocol: 'ssh', webUrl: 'https://github.com/testuser/gstack-artifacts-testuser' }); + const r = run(['--host', 'github']); + expect(r.status).toBe(0); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + expect(remote.stdout.trim()).toBe('git@github.com:testuser/gstack-artifacts-testuser.git'); + }); + + test('--push-protocol=auto + gh git_protocol unset → falls back to SSH (legacy default)', () => { + // gh config get returns non-zero / empty when never set. We default to + // SSH to keep the historical behavior for users who haven't opted in. + makeFakeGh({ gitProtocol: '', webUrl: 'https://github.com/testuser/gstack-artifacts-testuser' }); + const r = run(['--host', 'github']); + expect(r.status).toBe(0); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + expect(remote.stdout.trim()).toBe('git@github.com:testuser/gstack-artifacts-testuser.git'); + }); + + test('--push-protocol=https overrides gh git_protocol=ssh', () => { + makeFakeGh({ gitProtocol: 'ssh', webUrl: 'https://github.com/testuser/gstack-artifacts-testuser' }); + const r = run(['--host', 'github', '--push-protocol', 'https']); + expect(r.status).toBe(0); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + expect(remote.stdout.trim()).toBe('https://github.com/testuser/gstack-artifacts-testuser'); + }); + + test('--push-protocol=ssh overrides gh git_protocol=https', () => { + makeFakeGh({ gitProtocol: 'https', webUrl: 'https://github.com/testuser/gstack-artifacts-testuser' }); + const r = run(['--host', 'github', '--push-protocol', 'ssh']); + expect(r.status).toBe(0); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + expect(remote.stdout.trim()).toBe('git@github.com:testuser/gstack-artifacts-testuser.git'); + }); + + test('--push-protocol with invalid value exits 1', () => { + makeFakeGh({}); + const r = run(['--host', 'github', '--push-protocol', 'bogus']); + expect(r.status).not.toBe(0); + expect(r.stderr).toContain('Invalid --push-protocol'); + }); + + test('gitlab branch: glab git_protocol=https → origin uses HTTPS', () => { + makeFakeGlab({ gitProtocol: 'https' }); + const r = run(['--host', 'gitlab']); + expect(r.status).toBe(0); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + expect(remote.stdout.trim()).toBe('https://gitlab.com/testuser/gstack-artifacts-testuser'); + }); +}); + describe('gstack-artifacts-init idempotency', () => { test('--remote bypasses provider selection entirely', () => { makeFakeGh({}); From c454e2a2634823165a638e0f48a3dc03f0f189b5 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Thu, 16 Jul 2026 14:14:09 +0530 Subject: [PATCH 2/2] 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({});