Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/patch-clean-git-credentials-recursive.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/smoke-test-tools.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .smoke-test-22211157844
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Smoke test run 22211157844
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file appears to be an ephemeral artifact from a specific smoke test run and isn’t referenced elsewhere in the repo. It will accumulate over time and add noise to the repository history; please remove it from the PR (or add an explicit convention + .gitignore entry if these files are intended to be tracked).

Copilot uses AI. Check for mistakes.
116 changes: 63 additions & 53 deletions actions/setup/sh/clean_git_credentials.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
#
# clean_git_credentials.sh - Remove git credentials from .git/config
# clean_git_credentials.sh - Remove git credentials from all git checkouts
#
# This script removes any git credentials that may have been left on disk
# accidentally by an injected step. It specifically targets the credentials
# in $GITHUB_WORKSPACE/.git/config to prevent credential leakage.
# accidentally by an injected step. It recursively finds all .git/config
# files in $GITHUB_WORKSPACE and /tmp/ and cleans credentials from each.
#
# This is a security measure to ensure that git credentials configured by
# custom steps or other workflow steps are removed before the agentic engine
Expand All @@ -16,65 +16,75 @@

set -euo pipefail

# Get the workspace directory (defaults to current GITHUB_WORKSPACE)
WORKSPACE="${GITHUB_WORKSPACE:-.}"
GIT_CONFIG_PATH="${WORKSPACE}/.git/config"
# clean_git_config removes credentials from a single .git/config file
clean_git_config() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactor extracting this into a reusable clean_git_config() function β€” makes the recursive logic much cleaner to follow.

local GIT_CONFIG_PATH="$1"

echo "Cleaning git credentials from ${GIT_CONFIG_PATH@Q}"
echo "Cleaning git credentials from ${GIT_CONFIG_PATH@Q}"

# Check if .git/config exists
if [ ! -f "${GIT_CONFIG_PATH}" ]; then
echo "No .git/config found at ${GIT_CONFIG_PATH@Q}, nothing to clean"
exit 0
fi
# Remove credential helper configuration
# This removes lines like:
# [credential]
# helper = ...
# And any credential URL-specific configs like:
# [credential "https://github.com"]
# helper = ...
if git config --file "${GIT_CONFIG_PATH}" --remove-section credential 2>/dev/null; then
echo "Removed [credential] section from git config"
fi

# Remove credential helper configuration
# This removes lines like:
# [credential]
# helper = ...
# And any credential URL-specific configs like:
# [credential "https://github.com"]
# helper = ...
if git config --file "${GIT_CONFIG_PATH}" --remove-section credential 2>/dev/null; then
echo "Removed [credential] section from git config"
fi
# Remove credential URL-specific sections using sed
# Pattern: match lines from "[credential ..." to the next section header,
# deleting the credential header line and all lines until the next "[" section.
sed -i '/^\[credential /,/^\[/{ /^\[credential /d; /^\[/!d; }' "${GIT_CONFIG_PATH}" 2>/dev/null || true

# Remove credential URL-specific sections using grep
# This handles multi-line credential sections with URLs
sed -i '/^\[credential /,/^\[/{ /^\[credential /d; /^\[/!d; }' "${GIT_CONFIG_PATH}" 2>/dev/null || true
# Remove http extraheader (used by GitHub Actions for authentication)
# This is used by actions/checkout to authenticate
if git config --file "${GIT_CONFIG_PATH}" --unset-all http.extraheader 2>/dev/null; then
echo "Removed http.extraheader from git config"
fi

# Remove http extraheader (used by GitHub Actions for authentication)
# This is used by actions/checkout to authenticate
if git config --file "${GIT_CONFIG_PATH}" --unset-all http.extraheader 2>/dev/null; then
echo "Removed http.extraheader from git config"
fi
# Remove any http.<url>.extraheader configurations
git config --file "${GIT_CONFIG_PATH}" --get-regexp '^http\..*\.extraheader$' 2>/dev/null | while read -r key _; do
git config --file "${GIT_CONFIG_PATH}" --unset-all "$key" || true
echo "Removed $key from git config"
done || true

# Remove any http.<url>.extraheader configurations
git config --file "${GIT_CONFIG_PATH}" --get-regexp '^http\..*\.extraheader$' 2>/dev/null | while read -r key _; do
git config --file "${GIT_CONFIG_PATH}" --unset-all "$key" || true
echo "Removed $key from git config"
done || true
# Remove any credentials from remote URLs (https://username:password@github.com format)
# Replace authenticated URLs with unauthenticated ones
if git config --file "${GIT_CONFIG_PATH}" --get-regexp '^remote\..*\.url$' 2>/dev/null | grep -q '@'; then
echo "Found authenticated remote URLs, cleaning..."
git config --file "${GIT_CONFIG_PATH}" --get-regexp '^remote\..*\.url$' 2>/dev/null | while read -r key url; do
# Remove credentials from URL: https://user:pass@host -> https://host
clean_url=$(echo "$url" | sed -E 's|(https?://)([^@]+@)?(.*)|\1\3|')
if [ "$url" != "$clean_url" ]; then
git config --file "${GIT_CONFIG_PATH}" "$key" "$clean_url"
echo "Cleaned credentials from $key"
fi
done || true
fi

# Remove any credentials from remote URLs (https://username:password@github.com format)
# Replace authenticated URLs with unauthenticated ones
if git config --file "${GIT_CONFIG_PATH}" --get-regexp '^remote\..*\.url$' 2>/dev/null | grep -q '@'; then
echo "Found authenticated remote URLs, cleaning..."
git config --file "${GIT_CONFIG_PATH}" --get-regexp '^remote\..*\.url$' 2>/dev/null | while read -r key url; do
# Remove credentials from URL: https://user:pass@host -> https://host
clean_url=$(echo "$url" | sed -E 's|(https?://)([^@]+@)?(.*)|\1\3|')
if [ "$url" != "$clean_url" ]; then
git config --file "${GIT_CONFIG_PATH}" "$key" "$clean_url"
echo "Cleaned credentials from $key"
fi
done || true
fi
echo "βœ“ Git credentials cleaned from ${GIT_CONFIG_PATH@Q}"

# Verify the file is still valid git config
if ! git config --file "${GIT_CONFIG_PATH}" --list >/dev/null 2>&1; then
echo "ERROR: Git config file is corrupted after cleaning: ${GIT_CONFIG_PATH@Q}"
exit 1
fi
}

# Get the workspace directory (defaults to current GITHUB_WORKSPACE)
WORKSPACE="${GITHUB_WORKSPACE:-.}"

echo "βœ“ Git credentials cleaned successfully"
# Collect all .git/config files to clean from workspace and /tmp/
CLEANED=0
while IFS= read -r git_config; do
clean_git_config "${git_config}"
CLEANED=$((CLEANED + 1))
done < <(find "${WORKSPACE}" /tmp -maxdepth 10 -name "config" -path "*/.git/config" 2>/dev/null | sort -u)
Comment on lines +79 to +84
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $GITHUB_WORKSPACE is under /tmp, find "${WORKSPACE}" /tmp ... will traverse the workspace twice (once via ${WORKSPACE}, once as a subtree of /tmp). sort -u dedupes results but doesn’t avoid the extra traversal. Consider conditionally pruning ${WORKSPACE} from the /tmp walk (or only scanning /tmp when it’s not a parent of ${WORKSPACE}), and also add -type f so only regular files are passed to git config.

See below for a potential fix:

WORKSPACE_ABS="$(cd "${WORKSPACE}" && pwd -P)"

# Collect all .git/config files to clean from workspace and /tmp/
CLEANED=0
while IFS= read -r git_config; do
  clean_git_config "${git_config}"
  CLEANED=$((CLEANED + 1))
done < <(find "${WORKSPACE_ABS}" /tmp -maxdepth 10 \( -path "${WORKSPACE_ABS}" -prune -o -type f -name "config" -path "*/.git/config" -print \) 2>/dev/null | sort -u)

Copilot uses AI. Check for mistakes.

# Verify the file is still valid git config
if ! git config --file "${GIT_CONFIG_PATH}" --list >/dev/null 2>&1; then
echo "ERROR: Git config file is corrupted after cleaning"
exit 1
if [ "${CLEANED}" -eq 0 ]; then
echo "No .git/config files found, nothing to clean"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-maxdepth 10 is a reasonable guard against deep nesting, but consider documenting why 10 was chosen β€” or using a named constant like MAX_DEPTH=10 for clarity.

fi

exit 0
153 changes: 153 additions & 0 deletions actions/setup/sh/clean_git_credentials_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# Test script for clean_git_credentials.sh
# Run: bash clean_git_credentials_test.sh

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLEAN_SCRIPT="${SCRIPT_DIR}/clean_git_credentials.sh"

# Test counters
TESTS_PASSED=0
TESTS_FAILED=0

# Temporary workspace for tests
TEST_WORKSPACE=$(mktemp -d)

cleanup() {
rm -rf "${TEST_WORKSPACE}"
}
trap cleanup EXIT

# Helper: create a minimal git repo with a .git/config file
make_git_config() {
local dir="$1"
local config="$2"
mkdir -p "${dir}/.git"
echo "${config}" >"${dir}/.git/config"
}

# Helper: assert a condition
assert() {
local name="$1"
local condition="$2"
if eval "${condition}"; then
echo "βœ“ ${name}"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "βœ— ${name}"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
}

echo "Testing clean_git_credentials.sh..."
echo ""

# ── Test 1: No .git/config anywhere (no-op, exit 0) ─────────────────────────
echo "Test 1: No .git/config files β†’ exit 0 with informational message"
EMPTY_WS=$(mktemp -d)
OUTPUT=$(GITHUB_WORKSPACE="${EMPTY_WS}" bash "${CLEAN_SCRIPT}" 2>&1)
EXIT_CODE=$?
rmdir "${EMPTY_WS}"
assert "exits 0 when no .git/config found" "[ ${EXIT_CODE} -eq 0 ]"
assert "prints informational message" "echo '${OUTPUT}' | grep -q 'No .git/config'"
echo ""

# ── Test 2: Removes [credential] section ────────────────────────────────────
echo "Test 2: Removes [credential] section from workspace .git/config"
REPO="${TEST_WORKSPACE}/repo2"
make_git_config "${REPO}" "[core]
repositoryformatversion = 0
[credential]
helper = store
[remote \"origin\"]
url = https://github.com/org/repo.git"
GITHUB_WORKSPACE="${REPO}" bash "${CLEAN_SCRIPT}" >/dev/null 2>&1
assert "credential section removed" "! grep -q '\[credential\]' '${REPO}/.git/config'"
assert "core section preserved" "grep -q '\[core\]' '${REPO}/.git/config'"
assert "remote section preserved" "grep -q '\[remote' '${REPO}/.git/config'"
echo ""

# ── Test 3: Removes http.extraheader ────────────────────────────────────────
echo "Test 3: Removes http.extraheader from git config"
REPO="${TEST_WORKSPACE}/repo3"
make_git_config "${REPO}" "[core]
repositoryformatversion = 0
[http]
extraheader = Authorization: Basic dXNlcjpwYXNz"
GITHUB_WORKSPACE="${REPO}" bash "${CLEAN_SCRIPT}" >/dev/null 2>&1
assert "http.extraheader removed" "! git config --file '${REPO}/.git/config' http.extraheader 2>/dev/null"
assert "config still valid" "git config --file '${REPO}/.git/config' --list >/dev/null 2>&1"
echo ""

# ── Test 4: Strips credentials from remote URL ──────────────────────────────
echo "Test 4: Strips credentials from authenticated remote URL"
REPO="${TEST_WORKSPACE}/repo4"
make_git_config "${REPO}" "[core]
repositoryformatversion = 0
[remote \"origin\"]
url = https://x-access-token:ghs_abc123@github.com/org/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*"
GITHUB_WORKSPACE="${REPO}" bash "${CLEAN_SCRIPT}" >/dev/null 2>&1
CLEANED_URL=$(git config --file "${REPO}/.git/config" remote.origin.url)
assert "credentials stripped from URL" "[ '${CLEANED_URL}' = 'https://github.com/org/repo.git' ]"
echo ""

# ── Test 5: Recursively finds repo nested inside workspace ──────────────────
echo "Test 5: Recursively cleans nested git repo inside workspace"
OUTER="${TEST_WORKSPACE}/outer5"
INNER="${OUTER}/vendor/dep"
make_git_config "${OUTER}" "[core]
repositoryformatversion = 0
[credential]
helper = store"
make_git_config "${INNER}" "[core]
repositoryformatversion = 0
[http]
extraheader = Authorization: Basic dXNlcjpwYXNz"
GITHUB_WORKSPACE="${OUTER}" bash "${CLEAN_SCRIPT}" >/dev/null 2>&1
assert "outer credential section cleaned" "! grep -q '\[credential\]' '${OUTER}/.git/config'"
assert "inner extraheader cleaned" "! git config --file '${INNER}/.git/config' http.extraheader 2>/dev/null"
echo ""

# ── Test 6: Finds git repo in /tmp ──────────────────────────────────────────
echo "Test 6: Cleans git repo located in /tmp"
TMP_REPO=$(mktemp -d)
make_git_config "${TMP_REPO}" "[core]
repositoryformatversion = 0
[credential]
helper = store"
# Use a workspace that does NOT contain the repo so it is found only via /tmp
GITHUB_WORKSPACE="${TEST_WORKSPACE}/workspace6"
mkdir -p "${GITHUB_WORKSPACE}"
GITHUB_WORKSPACE="${GITHUB_WORKSPACE}" bash "${CLEAN_SCRIPT}" >/dev/null 2>&1
assert "tmp repo credential section cleaned" "! grep -q '\[credential\]' '${TMP_REPO}/.git/config'"
rm -rf "${TMP_REPO}"
Comment on lines +113 to +125
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TMP_REPO=$(mktemp -d) is not guaranteed to create the directory under /tmp (e.g., on macOS it often uses $TMPDIR). Since the script explicitly scans /tmp, this test can fail depending on the platform. Create the temp repo explicitly under /tmp (portable pattern: mktemp -d /tmp/gh-aw-clean-creds.XXXXXX).

This issue also appears on line 46 of the same file.

Copilot uses AI. Check for mistakes.
echo ""

# ── Test 7: Config file remains valid after all cleanups ────────────────────
echo "Test 7: Config file is still valid after cleaning"
REPO="${TEST_WORKSPACE}/repo7"
make_git_config "${REPO}" "[core]
repositoryformatversion = 0
[credential]
helper = /usr/lib/git-credential-gnome-keyring
[http]
extraheader = Authorization: Bearer sometoken
[remote \"origin\"]
url = https://oauth2:tok@github.com/org/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*"
GITHUB_WORKSPACE="${REPO}" bash "${CLEAN_SCRIPT}" >/dev/null 2>&1
assert "config still valid" "git config --file '${REPO}/.git/config' --list >/dev/null 2>&1"
assert "core settings intact" "git config --file '${REPO}/.git/config' core.repositoryformatversion >/dev/null 2>&1"
echo ""

# ── Summary ──────────────────────────────────────────────────────────────────
echo "Tests passed: ${TESTS_PASSED}"
echo "Tests failed: ${TESTS_FAILED}"

if [ "${TESTS_FAILED}" -gt 0 ]; then
exit 1
fi

echo "βœ“ All tests passed!"