mirror of https://github.com/garrytan/gstack.git
fix(artifacts-init): honor the provider CLI's git_protocol instead of forcing SSH
gstack-artifacts-init unconditionally rewrote the push remote to SSH and hard-failed setup for users whose gh/glab auth is HTTPS-only. Now: - provider-created remotes follow `gh config get git_protocol` / `glab config get git_protocol` (HTTPS when unset — the gh default) - explicit/existing/manual remotes keep their given protocol; unknown URL forms (local bare paths, file://, self-hosted) pass through - new --push-protocol auto|https|ssh flag overrides the inference - the unreachable-remote error names the actual protocol and points at --push-protocol instead of assuming a missing SSH key Closes #1348. Contributed by @time-attack (PR #2225). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a73d868413
commit
b50aa963ec
|
|
@ -8,6 +8,7 @@
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# gstack-artifacts-init [--remote <url>] [--host github|gitlab|manual]
|
# gstack-artifacts-init [--remote <url>] [--host github|gitlab|manual]
|
||||||
|
# [--push-protocol auto|https|ssh]
|
||||||
# [--url-form-supported true|false]
|
# [--url-form-supported true|false]
|
||||||
#
|
#
|
||||||
# Interactive by default. Pass --remote to skip the host prompt.
|
# Interactive by default. Pass --remote to skip the host prompt.
|
||||||
|
|
@ -52,17 +53,25 @@ _artifacts_host() {
|
||||||
|
|
||||||
REMOTE_URL=""
|
REMOTE_URL=""
|
||||||
HOST_PREF=""
|
HOST_PREF=""
|
||||||
|
PUSH_PROTOCOL="auto"
|
||||||
|
REMOTE_SOURCE="provider"
|
||||||
URL_FORM_SUPPORTED="false"
|
URL_FORM_SUPPORTED="false"
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--remote) REMOTE_URL="$2"; shift 2 ;;
|
--remote) REMOTE_URL="$2"; REMOTE_SOURCE="explicit"; shift 2 ;;
|
||||||
--host) HOST_PREF="$2"; shift 2 ;;
|
--host) HOST_PREF="$2"; shift 2 ;;
|
||||||
|
--push-protocol) PUSH_PROTOCOL="$2"; shift 2 ;;
|
||||||
--url-form-supported) URL_FORM_SUPPORTED="$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 ;;
|
--help|-h) sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
case "$PUSH_PROTOCOL" in
|
||||||
|
auto|https|ssh) ;;
|
||||||
|
*) echo "Invalid --push-protocol: $PUSH_PROTOCOL (expected auto|https|ssh)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# ---- preconditions ----
|
# ---- preconditions ----
|
||||||
mkdir -p "$GSTACK_HOME"
|
mkdir -p "$GSTACK_HOME"
|
||||||
|
|
||||||
|
|
@ -99,6 +108,7 @@ if command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1; then gla
|
||||||
# ---- choose remote URL ----
|
# ---- choose remote URL ----
|
||||||
if [ -z "$REMOTE_URL" ] && [ -n "$EXISTING_REMOTE" ]; then
|
if [ -z "$REMOTE_URL" ] && [ -n "$EXISTING_REMOTE" ]; then
|
||||||
REMOTE_URL="$EXISTING_REMOTE"
|
REMOTE_URL="$EXISTING_REMOTE"
|
||||||
|
REMOTE_SOURCE="existing"
|
||||||
echo "Using existing remote: $REMOTE_URL"
|
echo "Using existing remote: $REMOTE_URL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -174,6 +184,7 @@ if [ -z "$REMOTE_URL" ]; then
|
||||||
echo "No URL provided. Aborting." >&2
|
echo "No URL provided. Aborting." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
REMOTE_SOURCE="manual"
|
||||||
;;
|
;;
|
||||||
*) echo "Unknown --host: $HOST_PREF (expected github|gitlab|manual)" >&2; exit 1 ;;
|
*) echo "Unknown --host: $HOST_PREF (expected github|gitlab|manual)" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -181,7 +192,7 @@ fi
|
||||||
|
|
||||||
# ---- canonicalize to HTTPS form ----
|
# ---- canonicalize to HTTPS form ----
|
||||||
# We store HTTPS in ~/.gstack-artifacts-remote.txt (codex Finding #10:
|
# 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.)
|
# Unrecognized forms (local bare paths, file:// URLs, self-hosted gitea, etc.)
|
||||||
# pass through verbatim so unusual remotes still work.
|
# pass through verbatim so unusual remotes still work.
|
||||||
CANONICAL_HTTPS=$("$URL_BIN" --to https "$REMOTE_URL" 2>/dev/null || echo "")
|
CANONICAL_HTTPS=$("$URL_BIN" --to https "$REMOTE_URL" 2>/dev/null || echo "")
|
||||||
|
|
@ -189,21 +200,50 @@ if [ -z "$CANONICAL_HTTPS" ]; then
|
||||||
CANONICAL_HTTPS="$REMOTE_URL"
|
CANONICAL_HTTPS="$REMOTE_URL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Use SSH for git push (more reliable for repeated pushes than HTTPS+token).
|
# Choose the push protocol without overriding an explicit URL. Provider-created
|
||||||
# Fall back to the canonical input if derivation fails.
|
# remotes honor the provider CLI's git protocol; GitHub CLI defaults to HTTPS.
|
||||||
PUSH_URL=$("$URL_BIN" --to ssh "$CANONICAL_HTTPS" 2>/dev/null || echo "$CANONICAL_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 ----
|
# ---- verify push URL is reachable ----
|
||||||
echo "Verifying remote connectivity: $PUSH_URL"
|
echo "Verifying remote connectivity: $PUSH_URL"
|
||||||
if ! _receipted_git open artifacts-init "$(_artifacts_host)" artifacts-remote-ls-remote "user ran gstack-artifacts-init" \
|
if ! _receipted_git open artifacts-init "$(_artifacts_host)" artifacts-remote-ls-remote "user ran gstack-artifacts-init" \
|
||||||
bash -c 'git ls-remote "$1" >/dev/null 2>&1' _ "$PUSH_URL"; then
|
bash -c 'git ls-remote "$1" >/dev/null 2>&1' _ "$PUSH_URL"; then
|
||||||
cat >&2 <<EOF
|
cat >&2 <<EOF
|
||||||
Remote not reachable via SSH: $PUSH_URL
|
Remote not reachable via $RESOLVED_PUSH_PROTOCOL: $PUSH_URL
|
||||||
This could mean:
|
This could mean:
|
||||||
- Wrong URL
|
- 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
|
- Network issue
|
||||||
Fix and re-run gstack-artifacts-init.
|
Fix and re-run gstack-artifacts-init, or choose --push-protocol https|ssh.
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -389,7 +429,7 @@ cat <<EOF
|
||||||
gstack-artifacts-init complete.
|
gstack-artifacts-init complete.
|
||||||
Repo: $GSTACK_HOME (git)
|
Repo: $GSTACK_HOME (git)
|
||||||
Remote: $CANONICAL_HTTPS (canonical form, in ~/.gstack-artifacts-remote.txt)
|
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
|
EOF
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,25 @@ let fakeBinDir: string;
|
||||||
let ghCallLog: string;
|
let ghCallLog: string;
|
||||||
let glabCallLog: 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 authStatus = opts.authStatus ?? 'ok';
|
||||||
const repoCreate = opts.repoCreate ?? 'success';
|
const repoCreate = opts.repoCreate ?? 'success';
|
||||||
const webUrl = opts.webUrl ?? `https://github.com/testuser/gstack-artifacts-testuser`;
|
const webUrl = opts.webUrl ?? `https://github.com/testuser/gstack-artifacts-testuser`;
|
||||||
|
const gitProtocol = opts.gitProtocol ?? 'https';
|
||||||
const script = `#!/bin/bash
|
const script = `#!/bin/bash
|
||||||
echo "gh $@" >> "${ghCallLog}"
|
echo "gh $@" >> "${ghCallLog}"
|
||||||
case "$1" in
|
case "$1" in
|
||||||
auth) ${authStatus === 'ok' ? 'exit 0' : 'exit 1'} ;;
|
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)
|
repo)
|
||||||
shift
|
shift
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -65,14 +76,25 @@ exit 0
|
||||||
fs.writeFileSync(path.join(fakeBinDir, 'gh'), script, { mode: 0o755 });
|
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 authStatus = opts.authStatus ?? 'ok';
|
||||||
const repoCreate = opts.repoCreate ?? 'success';
|
const repoCreate = opts.repoCreate ?? 'success';
|
||||||
const webUrl = opts.webUrl ?? 'https://gitlab.com/testuser/gstack-artifacts-testuser';
|
const webUrl = opts.webUrl ?? 'https://gitlab.com/testuser/gstack-artifacts-testuser';
|
||||||
|
const gitProtocol = opts.gitProtocol ?? 'https';
|
||||||
const script = `#!/bin/bash
|
const script = `#!/bin/bash
|
||||||
echo "glab $@" >> "${glabCallLog}"
|
echo "glab $@" >> "${glabCallLog}"
|
||||||
case "$1" in
|
case "$1" in
|
||||||
auth) ${authStatus === 'ok' ? 'exit 0' : 'exit 1'} ;;
|
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)
|
repo)
|
||||||
shift
|
shift
|
||||||
case "$1" in
|
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');
|
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' });
|
makeFakeGh({ webUrl: 'https://github.com/testuser/gstack-artifacts-testuser' });
|
||||||
const r = run(['--host', 'github']);
|
const r = run(['--host', 'github']);
|
||||||
expect(r.status).toBe(0);
|
expect(r.status).toBe(0);
|
||||||
const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' });
|
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');
|
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)', () => {
|
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).toBeDefined();
|
||||||
expect(gbrainLine).toContain('https://github.com/testuser/gstack-artifacts-testuser');
|
expect(gbrainLine).toContain('https://github.com/testuser/gstack-artifacts-testuser');
|
||||||
expect(gbrainLine).not.toContain('git@github.com');
|
expect(gbrainLine).not.toContain('git@github.com');
|
||||||
// Note: the SSH form does appear in the printout as informational
|
// The Push line follows the provider CLI preference independently.
|
||||||
// (the "Push: ..." line), which is intentional — that's the URL git
|
|
||||||
// actually uses for push.
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -308,6 +355,34 @@ describe('gstack-artifacts-init idempotency', () => {
|
||||||
expect(readCalls(ghCallLog).some((c) => c.startsWith('gh repo create'))).toBe(false);
|
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)', () => {
|
test('re-run with same --remote is safe (no conflict error)', () => {
|
||||||
makeFakeGh({});
|
makeFakeGh({});
|
||||||
const url = 'https://github.com/testuser/gstack-artifacts-testuser';
|
const url = 'https://github.com/testuser/gstack-artifacts-testuser';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue