diff --git a/bin/gstack-artifacts-init b/bin/gstack-artifacts-init index 8f97c33059..058ddfdb3e 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 @@ -59,14 +67,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 @@ -293,6 +308,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({});