From d6b300cf771d7cb89f9480f19f0cf8c6ecba6be5 Mon Sep 17 00:00:00 2001 From: Sina Date: Fri, 10 Jul 2026 12:34:51 -0700 Subject: [PATCH] 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 --- bin/gstack-artifacts-init | 58 ++++++++++++++++---- test/gstack-artifacts-init.test.ts | 87 +++++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 15 deletions(-) diff --git a/bin/gstack-artifacts-init b/bin/gstack-artifacts-init index b8bfe830cf..1d57a7391c 100755 --- a/bin/gstack-artifacts-init +++ b/bin/gstack-artifacts-init @@ -8,6 +8,7 @@ # # Usage: # gstack-artifacts-init [--remote ] [--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 <> "${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';