fix(artifacts-init): honor configured push protocol

Honor the provider CLI protocol for created remotes, preserve the scheme of explicitly supplied and existing remotes, and add a push-protocol override.

Add regression coverage for HTTPS, SSH, unset configuration, GitHub, GitLab, explicit URLs, overrides, and invalid values.

Fixes #1348
This commit is contained in:
Sina 2026-07-10 12:34:51 -07:00
parent 7c9df1c568
commit d6b300cf77
2 changed files with 130 additions and 15 deletions

View File

@ -8,6 +8,7 @@
#
# Usage:
# gstack-artifacts-init [--remote <url>] [--host github|gitlab|manual]
# [--push-protocol auto|https|ssh]
# [--url-form-supported true|false]
#
# Interactive by default. Pass --remote to skip the host prompt.
@ -42,17 +43,25 @@ REMOTE_FILE="$HOME/.gstack-artifacts-remote.txt"
REMOTE_URL=""
HOST_PREF=""
PUSH_PROTOCOL="auto"
REMOTE_SOURCE="provider"
URL_FORM_SUPPORTED="false"
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 ;;
--push-protocol) PUSH_PROTOCOL="$2"; shift 2 ;;
--url-form-supported) URL_FORM_SUPPORTED="$2"; shift 2 ;;
--help|-h) sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Unknown flag: $1" >&2; exit 1 ;;
esac
done
case "$PUSH_PROTOCOL" in
auto|https|ssh) ;;
*) echo "Invalid --push-protocol: $PUSH_PROTOCOL (expected auto|https|ssh)" >&2; exit 1 ;;
esac
# ---- preconditions ----
mkdir -p "$GSTACK_HOME"
@ -89,6 +98,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
@ -164,6 +174,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
@ -171,7 +182,7 @@ fi
# ---- canonicalize to HTTPS form ----
# We store HTTPS in ~/.gstack-artifacts-remote.txt (codex Finding #10:
# canonical form, derive SSH at push time via gstack-artifacts-url --to ssh).
# canonical form, derive the configured push form via gstack-artifacts-url).
# Unrecognized forms (local bare paths, file:// URLs, self-hosted gitea, etc.)
# pass through verbatim so unusual remotes still work.
CANONICAL_HTTPS=$("$URL_BIN" --to https "$REMOTE_URL" 2>/dev/null || echo "")
@ -179,20 +190,49 @@ 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")
# Choose the push protocol without overriding an explicit URL. Provider-created
# remotes honor the provider CLI's git protocol; GitHub CLI defaults to HTTPS.
# Unknown/local URL forms pass through unchanged.
RESOLVED_PUSH_PROTOCOL="$PUSH_PROTOCOL"
if [ "$RESOLVED_PUSH_PROTOCOL" = "auto" ]; then
case "$REMOTE_SOURCE" in
explicit|existing|manual)
case "$REMOTE_URL" in
git@*|ssh://*) RESOLVED_PUSH_PROTOCOL="ssh" ;;
http://*|https://*) RESOLVED_PUSH_PROTOCOL="https" ;;
*) RESOLVED_PUSH_PROTOCOL="preserve" ;;
esac
;;
provider)
CONFIGURED_PROTOCOL=""
case "$HOST_PREF" in
github) CONFIGURED_PROTOCOL=$(gh config get git_protocol 2>/dev/null || echo "") ;;
gitlab) CONFIGURED_PROTOCOL=$(glab config get git_protocol 2>/dev/null || echo "") ;;
esac
case "$CONFIGURED_PROTOCOL" in
ssh|https) RESOLVED_PUSH_PROTOCOL="$CONFIGURED_PROTOCOL" ;;
*) RESOLVED_PUSH_PROTOCOL="https" ;;
esac
;;
esac
fi
if [ "$RESOLVED_PUSH_PROTOCOL" = "preserve" ]; then
PUSH_URL="$REMOTE_URL"
else
PUSH_URL=$("$URL_BIN" --to "$RESOLVED_PUSH_PROTOCOL" "$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
Remote not reachable via SSH: $PUSH_URL
Remote not reachable via $RESOLVED_PUSH_PROTOCOL: $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)
- Credentials for $RESOLVED_PUSH_PROTOCOL are not configured for your git host
- Network issue
Fix and re-run gstack-artifacts-init.
Fix and re-run gstack-artifacts-init, or choose --push-protocol https|ssh.
EOF
exit 1
fi
@ -373,7 +413,7 @@ cat <<EOF
gstack-artifacts-init complete.
Repo: $GSTACK_HOME (git)
Remote: $CANONICAL_HTTPS (canonical form, in ~/.gstack-artifacts-remote.txt)
Push: $PUSH_URL (derived SSH form for git push)
Push: $PUSH_URL ($RESOLVED_PUSH_PROTOCOL form for git push)
EOF

View File

@ -32,14 +32,25 @@ 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' | 'unset';
} = {}) {
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 ?? 'https';
const script = `#!/bin/bash
echo "gh $@" >> "${ghCallLog}"
case "$1" in
auth) ${authStatus === 'ok' ? 'exit 0' : 'exit 1'} ;;
config)
if [ "$2" = "get" ] && [ "$3" = "git_protocol" ]; then
${gitProtocol === 'unset' ? 'exit 1' : `echo "${gitProtocol}"; exit 0`}
fi
;;
repo)
shift
case "$1" in
@ -65,14 +76,25 @@ 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' | 'unset';
} = {}) {
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 ?? 'https';
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 === 'unset' ? 'exit 1' : `echo "${gitProtocol}"; exit 0`}
fi
;;
repo)
shift
case "$1" in
@ -251,13 +273,40 @@ describe('gstack-artifacts-init canonical URL storage (codex Finding #10)', () =
expect(stored).toBe('https://github.com/testuser/gstack-artifacts-testuser');
});
test('configures git origin with SSH form (derived from canonical HTTPS)', () => {
test('configures git origin with HTTPS when gh git_protocol is https', () => {
makeFakeGh({ 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('https://github.com/testuser/gstack-artifacts-testuser');
});
test('configures git origin with SSH when gh git_protocol is ssh', () => {
makeFakeGh({
webUrl: 'https://github.com/testuser/gstack-artifacts-testuser',
gitProtocol: 'ssh',
});
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('defaults provider-created remotes to HTTPS when git_protocol is unset', () => {
makeFakeGh({ gitProtocol: 'unset' });
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('https://github.com/testuser/gstack-artifacts-testuser');
});
test('honors glab git_protocol when configured', () => {
makeFakeGlab({ gitProtocol: 'ssh' });
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('git@gitlab.com:testuser/gstack-artifacts-testuser.git');
});
});
describe('gstack-artifacts-init brain-admin hookup printout (codex Finding #3)', () => {
@ -293,9 +342,7 @@ describe('gstack-artifacts-init brain-admin hookup printout (codex Finding #3)',
expect(gbrainLine).toBeDefined();
expect(gbrainLine).toContain('https://github.com/testuser/gstack-artifacts-testuser');
expect(gbrainLine).not.toContain('git@github.com');
// Note: the SSH form does appear in the printout as informational
// (the "Push: ..." line), which is intentional — that's the URL git
// actually uses for push.
// The Push line follows the provider CLI preference independently.
});
});
@ -308,6 +355,34 @@ describe('gstack-artifacts-init idempotency', () => {
expect(readCalls(ghCallLog).some((c) => c.startsWith('gh repo create'))).toBe(false);
});
test('explicit HTTPS --remote stays HTTPS even when gh prefers SSH', () => {
makeFakeGh({ gitProtocol: 'ssh' });
const r = run(['--remote', 'https://github.com/testuser/gstack-artifacts-testuser']);
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 overrides the inferred protocol', () => {
makeFakeGh({ gitProtocol: 'https' });
const r = run([
'--remote',
'https://github.com/testuser/gstack-artifacts-testuser',
'--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('rejects an invalid --push-protocol value', () => {
makeFakeGh({});
const r = run(['--push-protocol', 'ftp']);
expect(r.status).toBe(1);
expect(r.stderr).toContain('expected auto|https|ssh');
});
test('re-run with same --remote is safe (no conflict error)', () => {
makeFakeGh({});
const url = 'https://github.com/testuser/gstack-artifacts-testuser';