fix(artifacts-init): honor gh/glab git_protocol when picking push URL

`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 <https url>`,
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
This commit is contained in:
0xDevNinja 2026-05-13 15:30:05 +05:30
parent a3259400a3
commit 78aaa2017f
2 changed files with 137 additions and 6 deletions

View File

@ -9,6 +9,14 @@
# Usage:
# gstack-artifacts-init [--remote <url>] [--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 <<EOF
if [ "$RESOLVED_PROTOCOL" = "ssh" ]; then
cat >&2 <<EOF
Remote not reachable via SSH: $PUSH_URL
This could mean:
- Wrong URL
- SSH key not added to your git host (GitHub: gh ssh-key list; GitLab: glab ssh-key list)
- Network issue
- 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
Remote not reachable via HTTPS: $PUSH_URL
This could mean:
- Wrong URL
- HTTPS credentials not configured (GitHub: gh auth setup-git; GitLab: glab auth git-credential)
- Network issue
Fix and re-run gstack-artifacts-init.
EOF
fi
exit 1
fi

View File

@ -32,14 +32,22 @@ let fakeBinDir: string;
let ghCallLog: string;
let glabCallLog: string;
function makeFakeGh(opts: { authStatus?: 'ok' | 'fail'; repoCreate?: 'success' | 'already-exists' | 'fail'; webUrl?: string } = {}) {
function makeFakeGh(opts: { authStatus?: 'ok' | 'fail'; repoCreate?: 'success' | 'already-exists' | 'fail'; webUrl?: string; gitProtocol?: 'https' | 'ssh' | '' } = {}) {
const authStatus = opts.authStatus ?? 'ok';
const repoCreate = opts.repoCreate ?? 'success';
const webUrl = opts.webUrl ?? `https://github.com/testuser/gstack-artifacts-testuser`;
const gitProtocol = opts.gitProtocol ?? '';
const script = `#!/bin/bash
echo "gh $@" >> "${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 <url> bypasses provider selection entirely', () => {
makeFakeGh({});