From 5c4ea7373af3cb59ccdf414ce4f0dc8ad3c06b30 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 17:04:10 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20fix:=20stop=20baking=20credentials=20int?= =?UTF-8?q?o=20git=20remote=20URLs=20=E2=80=94=20use=20clean=20URLs=20+=20?= =?UTF-8?q?existing=20credential=20helper=20everywhere=20(#604)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/agents/entrypoint.sh | 46 +++----- docker/edge/entrypoint-edge.sh | 51 +++++++-- docker/reproduce/entrypoint-reproduce.sh | 9 ++ docker/runner/entrypoint-runner.sh | 9 ++ lib/forge-push.sh | 18 +--- lib/formula-session.sh | 16 +-- lib/generators.sh | 1 + lib/git-creds.sh | 120 +++++++++++++++++++++ lib/ops-setup.sh | 7 +- tests/smoke-credentials.sh | 131 +++++++++++++++++++++++ 10 files changed, 336 insertions(+), 72 deletions(-) create mode 100644 lib/git-creds.sh create mode 100644 tests/smoke-credentials.sh diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh index bc6891f..02e674f 100644 --- a/docker/agents/entrypoint.sh +++ b/docker/agents/entrypoint.sh @@ -42,42 +42,20 @@ init_state_dir() { log "Initialized state directory" } -# Configure git credential helper for password-based HTTP auth. -# Forgejo 11.x rejects API tokens for git push (#361); password auth works. -# This ensures all git operations (clone, fetch, push) from worktrees use -# password auth without needing tokens embedded in remote URLs. -configure_git_creds() { +# Source shared git credential helper library (#604). +# shellcheck source=lib/git-creds.sh +source "${DISINTO_BAKED}/lib/git-creds.sh" + +# Wrapper that calls the shared configure_git_creds with agent-specific paths, +# then repairs any legacy baked-credential URLs in existing clones. +_setup_git_creds() { + configure_git_creds "/home/agent" "gosu agent" if [ -n "${FORGE_PASS:-}" ] && [ -n "${FORGE_URL:-}" ]; then - _forge_host=$(printf '%s' "$FORGE_URL" | sed 's|https\?://||; s|/.*||') - _forge_proto=$(printf '%s' "$FORGE_URL" | sed 's|://.*||') - # Determine the bot username from FORGE_TOKEN identity (or default to dev-bot) - _bot_user=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_URL}/api/v1/user" 2>/dev/null | jq -r '.login // empty') || _bot_user="" - _bot_user="${_bot_user:-dev-bot}" - - # Write a static credential helper script (git credential protocol) - cat > /home/agent/.git-credentials-helper </dev/null -echo "protocol=${_forge_proto}" -echo "host=${_forge_host}" -echo "username=${_bot_user}" -echo "password=${FORGE_PASS}" -CREDEOF - chmod 755 /home/agent/.git-credentials-helper - chown agent:agent /home/agent/.git-credentials-helper - - gosu agent bash -c "git config --global credential.helper '/home/agent/.git-credentials-helper'" - log "Git credential helper configured for ${_bot_user}@${_forge_host} (password auth)" + log "Git credential helper configured (password auth)" fi - # Set safe.directory to work around dubious ownership after container restart - # (https://github.com/disinto-admin/disinto/issues/517) - gosu agent bash -c "git config --global --add safe.directory '*'" + # Repair legacy clones with baked-in stale credentials (#604). + _GIT_CREDS_LOG_FN=log repair_baked_cred_urls /home/agent/repos } # Configure tea CLI login for forge operations (runs as agent user). @@ -272,7 +250,7 @@ pull_factory_repo() { } # Configure git and tea once at startup (as root, then drop to agent) -configure_git_creds +_setup_git_creds configure_tea_login # Bootstrap ops repos from forgejo into container volumes (#586) diff --git a/docker/edge/entrypoint-edge.sh b/docker/edge/entrypoint-edge.sh index 6397639..694d7e6 100755 --- a/docker/edge/entrypoint-edge.sh +++ b/docker/edge/entrypoint-edge.sh @@ -45,17 +45,48 @@ if [ -d /opt/disinto ] && [ ! -d /opt/disinto/.git ] && [ -n "$(ls -A /opt/disin exit 1 fi -# Shallow clone at the pinned version (inject token to support auth-required Forgejo) +# Set HOME early so credential helper and git config land in the right place. +export HOME=/home/agent +mkdir -p "$HOME" + +# Configure git credential helper before cloning (#604). +# /opt/disinto does not exist yet so we cannot source lib/git-creds.sh; +# inline a minimal credential-helper setup here. +if [ -n "${FORGE_PASS:-}" ] && [ -n "${FORGE_URL:-}" ]; then + _forge_host=$(printf '%s' "$FORGE_URL" | sed 's|https\?://||; s|/.*||') + _forge_proto=$(printf '%s' "$FORGE_URL" | sed 's|://.*||') + _bot_user="" + if [ -n "${FORGE_TOKEN:-}" ]; then + _bot_user=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_URL}/api/v1/user" 2>/dev/null | jq -r '.login // empty') || _bot_user="" + fi + _bot_user="${_bot_user:-dev-bot}" + + cat > "${HOME}/.git-credentials-helper" </dev/null +echo "protocol=${_forge_proto}" +echo "host=${_forge_host}" +echo "username=${_bot_user}" +echo "password=${FORGE_PASS}" +CREDEOF + chmod 755 "${HOME}/.git-credentials-helper" + git config --global credential.helper "${HOME}/.git-credentials-helper" + git config --global --add safe.directory '*' +fi + +# Shallow clone at the pinned version — use clean URL, credential helper +# supplies auth (#604). if [ ! -d /opt/disinto/.git ]; then - _auth_url=$(printf '%s' "$FORGE_URL" | sed "s|://|://token:${FORGE_TOKEN}@|") echo "edge: cloning ${FORGE_URL}/${FORGE_REPO} (branch ${DISINTO_VERSION:-main})..." >&2 - if ! git clone --depth 1 --branch "${DISINTO_VERSION:-main}" "${_auth_url}/${FORGE_REPO}.git" /opt/disinto; then + if ! git clone --depth 1 --branch "${DISINTO_VERSION:-main}" "${FORGE_URL}/${FORGE_REPO}.git" /opt/disinto; then echo >&2 echo "FATAL: failed to clone ${FORGE_URL}/${FORGE_REPO}.git (branch ${DISINTO_VERSION:-main})" >&2 echo "Likely causes:" >&2 echo " - Forgejo at ${FORGE_URL} is unreachable from the edge container" >&2 echo " - Repository '${FORGE_REPO}' does not exist on this forge" >&2 - echo " - FORGE_TOKEN is invalid or has no read access to '${FORGE_REPO}'" >&2 + echo " - FORGE_TOKEN/FORGE_PASS is invalid or has no read access to '${FORGE_REPO}'" >&2 echo " - Branch '${DISINTO_VERSION:-main}' does not exist in '${FORGE_REPO}'" >&2 echo "Workaround: bind-mount a local git checkout into /opt/disinto." >&2 echo "Sleeping 60s before exit to throttle the restart loop..." >&2 @@ -64,11 +95,13 @@ if [ ! -d /opt/disinto/.git ]; then fi fi -# Set HOME so that claude OAuth credentials and session.lock are found at the -# same in-container path as in disinto-agents (/home/agent/.claude), which makes -# flock cross-serialize across containers on the same host inode. -export HOME=/home/agent -mkdir -p "$HOME" +# Repair any legacy baked-credential URLs in /opt/disinto (#604). +# Now that /opt/disinto exists, source the shared lib. +if [ -f /opt/disinto/lib/git-creds.sh ]; then + # shellcheck source=/opt/disinto/lib/git-creds.sh + source /opt/disinto/lib/git-creds.sh + _GIT_CREDS_LOG_FN="echo" repair_baked_cred_urls /opt/disinto +fi # Ensure log directory exists mkdir -p /opt/disinto-logs diff --git a/docker/reproduce/entrypoint-reproduce.sh b/docker/reproduce/entrypoint-reproduce.sh index 6b4e469..561df85 100644 --- a/docker/reproduce/entrypoint-reproduce.sh +++ b/docker/reproduce/entrypoint-reproduce.sh @@ -84,6 +84,15 @@ export DISINTO_CONTAINER=1 export HOME="${HOME:-/home/agent}" export USER="${USER:-agent}" +# Configure git credential helper so reproduce/triage agents can clone/push +# without needing tokens embedded in remote URLs (#604). +if [ -f "${DISINTO_DIR}/lib/git-creds.sh" ]; then + # shellcheck source=lib/git-creds.sh + source "${DISINTO_DIR}/lib/git-creds.sh" + # shellcheck disable=SC2119 # no args intended — uses defaults + configure_git_creds +fi + FORGE_API="${FORGE_URL}/api/v1/repos/${FORGE_REPO}" # Load project name from TOML diff --git a/docker/runner/entrypoint-runner.sh b/docker/runner/entrypoint-runner.sh index f13d9f5..9188ecf 100644 --- a/docker/runner/entrypoint-runner.sh +++ b/docker/runner/entrypoint-runner.sh @@ -23,6 +23,15 @@ log() { printf '[%s] runner: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" } +# Configure git credential helper so formulas can clone/push without +# needing tokens embedded in remote URLs (#604). +if [ -f "${FACTORY_ROOT}/lib/git-creds.sh" ]; then + # shellcheck source=lib/git-creds.sh + source "${FACTORY_ROOT}/lib/git-creds.sh" + # shellcheck disable=SC2119 # no args intended — uses defaults + configure_git_creds +fi + # ── Argument parsing ───────────────────────────────────────────────────── action_id="${1:-}" diff --git a/lib/forge-push.sh b/lib/forge-push.sh index 1da61f7..c4403ca 100644 --- a/lib/forge-push.sh +++ b/lib/forge-push.sh @@ -7,7 +7,6 @@ # Globals expected: # FORGE_URL - Forge instance URL (e.g. http://localhost:3000) # FORGE_TOKEN - API token for Forge operations (used for API verification) -# FORGE_PASS - Bot password for git HTTP push (#361: tokens rejected by Forgejo 11.x) # FACTORY_ROOT - Root of the disinto factory # PRIMARY_BRANCH - Primary branch name (e.g. main) # @@ -21,7 +20,6 @@ set -euo pipefail _assert_forge_push_globals() { local missing=() [ -z "${FORGE_URL:-}" ] && missing+=("FORGE_URL") - [ -z "${FORGE_PASS:-}" ] && missing+=("FORGE_PASS") [ -z "${FORGE_TOKEN:-}" ] && missing+=("FORGE_TOKEN") [ -z "${FACTORY_ROOT:-}" ] && missing+=("FACTORY_ROOT") [ -z "${PRIMARY_BRANCH:-}" ] && missing+=("PRIMARY_BRANCH") @@ -35,17 +33,11 @@ _assert_forge_push_globals() { push_to_forge() { local repo_root="$1" forge_url="$2" repo_slug="$3" - # Build authenticated remote URL: http://dev-bot:@host:port/org/repo.git - # Forgejo 11.x rejects API tokens for git HTTP push (#361); password auth works. - if [ -z "${FORGE_PASS:-}" ]; then - echo "Error: FORGE_PASS not set — cannot push to Forgejo (see #361)" >&2 - return 1 - fi - local auth_url - auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_PASS}@|") - local remote_url="${auth_url}/${repo_slug}.git" - # Display URL without token - local display_url="${forge_url}/${repo_slug}.git" + # Use clean URL — credential helper supplies auth (#604). + # Forgejo 11.x rejects API tokens for git HTTP push (#361); password auth works + # via the credential helper configured in configure_git_creds(). + local remote_url="${forge_url}/${repo_slug}.git" + local display_url="$remote_url" # Always set the remote URL to ensure credentials are current if git -C "$repo_root" remote get-url forgejo >/dev/null 2>&1; then diff --git a/lib/formula-session.sh b/lib/formula-session.sh index a39540d..bf44ce7 100644 --- a/lib/formula-session.sh +++ b/lib/formula-session.sh @@ -113,11 +113,9 @@ ensure_profile_repo() { # Define cache directory: /home/agent/data/.profile/{agent-name} PROFILE_REPO_PATH="${HOME:-/home/agent}/data/.profile/${agent_identity}" - # Build clone URL from FORGE_URL and agent identity + # Build clone URL from FORGE_URL — credential helper supplies auth (#604) local forge_url="${FORGE_URL:-http://localhost:3000}" - local auth_url - auth_url=$(printf '%s' "$forge_url" | sed "s|://|://$(whoami):${FORGE_TOKEN}@|") - local clone_url="${auth_url}/${agent_identity}/.profile.git" + local clone_url="${forge_url}/${agent_identity}/.profile.git" # Check if already cached and up-to-date if [ -d "${PROFILE_REPO_PATH}/.git" ]; then @@ -592,14 +590,8 @@ ensure_ops_repo() { local ops_repo="${FORGE_OPS_REPO:-}" [ -n "$ops_repo" ] || return 0 local forge_url="${FORGE_URL:-http://localhost:3000}" - local clone_url - if [ -n "${FORGE_TOKEN:-}" ]; then - local auth_url - auth_url=$(printf '%s' "$forge_url" | sed "s|://|://$(whoami):${FORGE_TOKEN}@|") - clone_url="${auth_url}/${ops_repo}.git" - else - clone_url="${forge_url}/${ops_repo}.git" - fi + # Use clean URL — credential helper supplies auth (#604) + local clone_url="${forge_url}/${ops_repo}.git" log "Cloning ops repo: ${ops_repo} -> ${ops_root}" if git clone --quiet "$clone_url" "$ops_root" 2>/dev/null; then diff --git a/lib/generators.sh b/lib/generators.sh index 4de088e..8cae400 100644 --- a/lib/generators.sh +++ b/lib/generators.sh @@ -391,6 +391,7 @@ services: - FORGE_REPO=${FORGE_REPO:-disinto-admin/disinto} - FORGE_OPS_REPO=${FORGE_OPS_REPO:-disinto-admin/disinto-ops} - FORGE_TOKEN=${FORGE_TOKEN:-} + - FORGE_PASS=${FORGE_PASS:-} - FORGE_ADMIN_USERS=${FORGE_ADMIN_USERS:-disinto-admin} - FORGE_ADMIN_TOKEN=${FORGE_ADMIN_TOKEN:-} - OPS_REPO_ROOT=/opt/disinto-ops diff --git a/lib/git-creds.sh b/lib/git-creds.sh new file mode 100644 index 0000000..9c5c00a --- /dev/null +++ b/lib/git-creds.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# git-creds.sh — Shared git credential helper configuration +# +# Configures a static credential helper for Forgejo password-based HTTP auth. +# Forgejo 11.x rejects API tokens for git push (#361); password auth works. +# This ensures all git operations (clone, fetch, push) use password auth +# without needing tokens embedded in remote URLs (#604). +# +# Usage: +# source "${FACTORY_ROOT}/lib/git-creds.sh" +# configure_git_creds [HOME_DIR] [RUN_AS_CMD] +# repair_baked_cred_urls DIR [DIR ...] +# +# Globals expected: +# FORGE_PASS — bot password for git HTTP auth +# FORGE_URL — Forge instance URL (e.g. http://forgejo:3000) +# FORGE_TOKEN — API token (used to resolve bot username) + +set -euo pipefail + +# configure_git_creds [HOME_DIR] [RUN_AS_CMD] +# HOME_DIR — home directory for the git user (default: $HOME or /home/agent) +# RUN_AS_CMD — command prefix to run as another user (e.g. "gosu agent") +# +# Writes a credential helper script and configures git to use it globally. +configure_git_creds() { + local home_dir="${1:-${HOME:-/home/agent}}" + local run_as="${2:-}" + + if [ -z "${FORGE_PASS:-}" ] || [ -z "${FORGE_URL:-}" ]; then + return 0 + fi + + local forge_host forge_proto + forge_host=$(printf '%s' "$FORGE_URL" | sed 's|https\?://||; s|/.*||') + forge_proto=$(printf '%s' "$FORGE_URL" | sed 's|://.*||') + + # Determine the bot username from FORGE_TOKEN identity (or default to dev-bot) + local bot_user="" + if [ -n "${FORGE_TOKEN:-}" ]; then + bot_user=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_URL}/api/v1/user" 2>/dev/null | jq -r '.login // empty') || bot_user="" + fi + bot_user="${bot_user:-dev-bot}" + + local helper_path="${home_dir}/.git-credentials-helper" + + # Write a static credential helper script (git credential protocol) + cat > "$helper_path" </dev/null +echo "protocol=${forge_proto}" +echo "host=${forge_host}" +echo "username=${bot_user}" +echo "password=${FORGE_PASS}" +CREDEOF + chmod 755 "$helper_path" + + # Set ownership and configure git if running as a different user + if [ -n "$run_as" ]; then + local target_user + target_user=$(echo "$run_as" | awk '{print $NF}') + chown "${target_user}:${target_user}" "$helper_path" 2>/dev/null || true + $run_as bash -c "git config --global credential.helper '${helper_path}'" + else + git config --global credential.helper "$helper_path" + fi + + # Set safe.directory to work around dubious ownership after container restart + if [ -n "$run_as" ]; then + $run_as bash -c "git config --global --add safe.directory '*'" + else + git config --global --add safe.directory '*' + fi +} + +# repair_baked_cred_urls DIR [DIR ...] +# Scans git repos under each DIR and rewrites remote URLs that contain +# embedded credentials (user:pass@host) to clean URLs. +# Logs each repair so operators can see the migration happened. +# +# Set _GIT_CREDS_LOG_FN to a custom log function name (default: echo). +repair_baked_cred_urls() { + local log_fn="${_GIT_CREDS_LOG_FN:-echo}" + + for dir in "$@"; do + [ -d "$dir" ] || continue + + # Find git repos: either dir itself or immediate subdirectories + local -a repos=() + if [ -d "${dir}/.git" ]; then + repos+=("$dir") + else + local sub + for sub in "$dir"/*/; do + [ -d "${sub}.git" ] && repos+=("${sub%/}") + done + fi + + local repo + for repo in "${repos[@]}"; do + local url + url=$(git -C "$repo" config --get remote.origin.url 2>/dev/null || true) + [ -n "$url" ] || continue + + # Check if URL contains embedded credentials: http(s)://user:pass@host + if printf '%s' "$url" | grep -qE '^https?://[^/]+@'; then + # Strip credentials: http(s)://user:pass@host/path -> http(s)://host/path + local clean_url + clean_url=$(printf '%s' "$url" | sed -E 's|(https?://)[^@]+@|\1|') + git -C "$repo" remote set-url origin "$clean_url" + $log_fn "Repaired baked credentials in ${repo} (remote origin -> ${clean_url})" + fi + done + done +} diff --git a/lib/ops-setup.sh b/lib/ops-setup.sh index bf3ec55..635b83c 100644 --- a/lib/ops-setup.sh +++ b/lib/ops-setup.sh @@ -153,11 +153,10 @@ setup_ops_repo() { echo " ! disinto-admin = admin (already set or failed)" fi - # Clone ops repo locally if not present + # Clone ops repo locally if not present — use clean URL, credential helper + # supplies auth (#604). if [ ! -d "${ops_root}/.git" ]; then - local auth_url - auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_TOKEN}@|") - local clone_url="${auth_url}/${actual_ops_slug}.git" + local clone_url="${forge_url}/${actual_ops_slug}.git" echo "Cloning: ops repo -> ${ops_root}" if git clone --quiet "$clone_url" "$ops_root" 2>/dev/null; then echo "Ops repo: ${actual_ops_slug} cloned successfully" diff --git a/tests/smoke-credentials.sh b/tests/smoke-credentials.sh new file mode 100644 index 0000000..3467cb9 --- /dev/null +++ b/tests/smoke-credentials.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# tests/smoke-credentials.sh — Verify no git remote URL contains embedded credentials +# +# Scans all shell scripts that construct git URLs and verifies: +# 1. No source file embeds credentials in remote URLs (static check) +# 2. The repair_baked_cred_urls function correctly strips credentials +# 3. configure_git_creds writes a working credential helper +# +# Required tools: bash, git, grep + +set -euo pipefail + +FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FAILED=0 + +fail() { printf 'FAIL: %s\n' "$*" >&2; FAILED=1; } +pass() { printf 'PASS: %s\n' "$*"; } + +# ── 1. Static check: no credential embedding in URL construction ────────── +echo "=== 1/3 Static check: no credential embedding in URL construction ===" + +# Patterns that embed credentials into git URLs: +# sed "s|://|://user:pass@|" — the classic injection pattern +# ://.*:.*@ — literal user:pass@ in a URL string +# Allowlist: git-creds.sh itself (it writes the credential helper, not URLs), +# and this test file. +cred_embed_pattern='s\|://\|://.*:.*\$\{.*\}@' + +offending_files=() +while IFS= read -r f; do + # Skip allowlisted files: + # git-creds.sh — writes the credential helper, not URLs + # smoke-credentials.sh — this test file + # hire-agent.sh — one-shot setup: clones as newly-created user, clone dir deleted immediately + case "$f" in + */git-creds.sh|*/smoke-credentials.sh|*/hire-agent.sh) continue ;; + esac + if grep -qE "$cred_embed_pattern" "$f" 2>/dev/null; then + offending_files+=("$f") + fi +done < <(git -C "$FACTORY_ROOT" ls-files '*.sh') + +if [ ${#offending_files[@]} -eq 0 ]; then + pass "No shell scripts embed credentials in git remote URLs" +else + for f in "${offending_files[@]}"; do + fail "Credential embedding found in: $f" + grep -nE "$cred_embed_pattern" "$FACTORY_ROOT/$f" 2>/dev/null | head -3 + done +fi + +# ── 2. Unit test: repair_baked_cred_urls strips credentials ─────────────── +echo "=== 2/3 Unit test: repair_baked_cred_urls ===" + +# Source the shared lib +# shellcheck source=lib/git-creds.sh +source "${FACTORY_ROOT}/lib/git-creds.sh" + +# Create a temporary git repo with a baked-credential URL +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +mkdir -p "${test_dir}/repo" +git -C "${test_dir}/repo" init -q +git -C "${test_dir}/repo" config user.email "test@test" +git -C "${test_dir}/repo" config user.name "test" +git -C "${test_dir}/repo" commit --allow-empty -m "init" -q +git -C "${test_dir}/repo" remote add origin "http://dev-bot:secret-token@forgejo:3000/org/repo.git" + +# Run repair +_GIT_CREDS_LOG_FN="echo" repair_baked_cred_urls "${test_dir}" + +# Verify the URL was cleaned +repaired_url=$(git -C "${test_dir}/repo" config --get remote.origin.url) +if [ "$repaired_url" = "http://forgejo:3000/org/repo.git" ]; then + pass "repair_baked_cred_urls correctly stripped credentials" +else + fail "repair_baked_cred_urls result: '${repaired_url}' (expected 'http://forgejo:3000/org/repo.git')" +fi + +# Also test that a clean URL is left untouched +git -C "${test_dir}/repo" remote set-url origin "http://forgejo:3000/org/repo.git" +_GIT_CREDS_LOG_FN="echo" repair_baked_cred_urls "${test_dir}" +clean_url=$(git -C "${test_dir}/repo" config --get remote.origin.url) +if [ "$clean_url" = "http://forgejo:3000/org/repo.git" ]; then + pass "repair_baked_cred_urls leaves clean URLs untouched" +else + fail "repair_baked_cred_urls modified a clean URL: '${clean_url}'" +fi + +# ── 3. Unit test: configure_git_creds writes a credential helper ────────── +echo "=== 3/3 Unit test: configure_git_creds ===" + +cred_home=$(mktemp -d) + +# Export required globals +export FORGE_PASS="test-password-123" +export FORGE_URL="http://forgejo:3000" +export FORGE_TOKEN="" # skip API call in test + +configure_git_creds "$cred_home" + +if [ -x "${cred_home}/.git-credentials-helper" ]; then + pass "Credential helper script created and executable" +else + fail "Credential helper script not found or not executable at ${cred_home}/.git-credentials-helper" +fi + +# Verify the helper outputs correct credentials +helper_output=$(echo "" | "${cred_home}/.git-credentials-helper" get 2>/dev/null) +if printf '%s' "$helper_output" | grep -q "password=test-password-123"; then + pass "Credential helper outputs correct password" +else + fail "Credential helper output missing password: ${helper_output}" +fi + +if printf '%s' "$helper_output" | grep -q "host=forgejo:3000"; then + pass "Credential helper outputs correct host" +else + fail "Credential helper output missing host: ${helper_output}" +fi + +rm -rf "$cred_home" + +# ── Summary ─────────────────────────────────────────────────────────────── +echo "" +if [ "$FAILED" -ne 0 ]; then + echo "=== SMOKE-CREDENTIALS TEST FAILED ===" + exit 1 +fi +echo "=== SMOKE-CREDENTIALS TEST PASSED ==="