Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions bin/gstack-artifacts-init
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
82 changes: 80 additions & 2 deletions test/gstack-artifacts-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,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
Expand All @@ -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
Expand Down Expand Up @@ -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 <url> bypasses provider selection entirely', () => {
makeFakeGh({});
Expand Down