Last active
March 13, 2026 21:27
-
-
Save RichardFevrier/dcfcc55ed38e16be4e6f1288c22399fe to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # check_forks_upstream.sh | |
| # Lists forks on your GitHub account where you have never contributed: | |
| # 1. No commits on default branch authored by you | |
| # 2. No orphan branches (branches not present in upstream) whose first commit is yours | |
| # | |
| # Usage (GitHub Actions): | |
| # curl -fsSL https://gist.github.com/RichardFevrier/dcfcc55ed38e16be4e6f1288c22399fe/raw/check_forks_upstream.sh \ | |
| # | GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} sh | |
| # | |
| # Usage (local): | |
| # curl -fsSL <url> | GITHUB_TOKEN=ghp_xxx sh | |
| # ── Helpers ────────────────────────────────────────────────────────────────── | |
| die() { printf 'Error: %s\n' "$1" >&2; exit 1; } | |
| _api() ( | |
| curl -sL \ | |
| -H "Authorization: Bearer ${GITHUB_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com${1}" | |
| ) | |
| _api_ok() ( | |
| resp="$1" | |
| required_key="$2" | |
| [ -n "$resp" ] || return 1 | |
| if printf '%s' "$resp" | grep -q '"message"' && | |
| ! printf '%s' "$resp" | grep -q "\"${required_key}\""; then | |
| msg=$(printf '%s' "$resp" | tr -d '\n' \ | |
| | sed 's/.*"message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') | |
| case "$msg" in | |
| 'Not Found') return 1 ;; | |
| esac | |
| printf 'Warning: GitHub API error: %s\n' "$msg" >&2 | |
| return 1 | |
| fi | |
| return 0 | |
| ) | |
| _branch_names() ( | |
| printf '%s' "$1" \ | |
| | grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' \ | |
| | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/' | |
| ) | |
| # ── Preflight ───────────────────────────────────────────────────────────────── | |
| [ -n "$GITHUB_TOKEN" ] || die "GITHUB_TOKEN is not set" | |
| command -v curl >/dev/null 2>&1 || die "curl is required" | |
| user_resp=$(_api "/user") | |
| _api_ok "$user_resp" "login" || die "Could not authenticate with GitHub API" | |
| LOGIN=$(printf '%s' "$user_resp" \ | |
| | grep -o '"login"[[:space:]]*:[[:space:]]*"[^"]*"' \ | |
| | head -1 \ | |
| | sed 's/"login"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') | |
| [ -n "$LOGIN" ] || die "Could not determine GitHub login" | |
| printf 'Checking forks for: %s\n' "$LOGIN" >&2 | |
| # ── Main ────────────────────────────────────────────────────────────────────── | |
| never_contributed="" | |
| page=1 | |
| while true; do | |
| resp=$(_api "/user/repos?type=fork&per_page=100&page=${page}") | |
| _api_ok "$resp" "full_name" || break | |
| names=$(printf '%s' "$resp" \ | |
| | grep -o '"full_name"[[:space:]]*:[[:space:]]*"[^"]*"' \ | |
| | sed 's/"full_name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') | |
| [ -n "$names" ] || break | |
| for full_name in $names; do | |
| # Fetch repo details to get parent (reused across all checks) | |
| repo_resp=$(_api "/repos/${full_name}") | |
| _api_ok "$repo_resp" "full_name" || continue | |
| parent=$(printf '%s' "$repo_resp" \ | |
| | tr -d '\n' \ | |
| | grep -o '"parent"[[:space:]]*:[[:space:]]*{[^}]*}' \ | |
| | grep -o '"full_name"[[:space:]]*:[[:space:]]*"[^"]*"' \ | |
| | sed 's/"full_name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') | |
| [ -n "$parent" ] || continue | |
| # Verify upstream still exists — if it was deleted, skip the fork entirely | |
| upstream_check=$(_api "/repos/${parent}") | |
| _api_ok "$upstream_check" "full_name" || continue | |
| # 1a. Default branch ahead of upstream? (catches merges, auto-commits) | |
| compare_resp=$(_api "/repos/${parent}/compare/HEAD...${LOGIN}:HEAD") | |
| _api_ok "$compare_resp" "ahead_by" || continue | |
| ahead=$(printf '%s' "$compare_resp" \ | |
| | grep -o '"ahead_by"[[:space:]]*:[[:space:]]*[0-9]*' \ | |
| | sed 's/[^0-9]//g') | |
| [ "${ahead:-0}" -eq 0 ] || continue | |
| # 1b. Any commit on default branch authored by LOGIN? | |
| commits_resp=$(_api "/repos/${full_name}/commits?author=${LOGIN}&per_page=1") | |
| has_commit=$(printf '%s' "$commits_resp" | grep -c '"sha"' || true) | |
| [ "${has_commit:-0}" -eq 0 ] || continue | |
| # 2. Any orphan branches (not present in upstream)? | |
| # Paginate fork branches | |
| fork_branches="" | |
| bp=1 | |
| while true; do | |
| page_resp=$(_api "/repos/${full_name}/branches?per_page=100&page=${bp}") | |
| page_names=$(_branch_names "$page_resp") | |
| [ -n "$page_names" ] || break | |
| fork_branches="${fork_branches}${page_names} | |
| " | |
| bp=$((bp + 1)) | |
| done | |
| # Paginate upstream branches | |
| upstream_branches="" | |
| bp=1 | |
| while true; do | |
| page_resp=$(_api "/repos/${parent}/branches?per_page=100&page=${bp}") | |
| page_names=$(_branch_names "$page_resp") | |
| [ -n "$page_names" ] || break | |
| upstream_branches="${upstream_branches}${page_names} | |
| " | |
| bp=$((bp + 1)) | |
| done | |
| # For each orphan branch, check if its first (oldest) commit was authored by LOGIN | |
| contributed=0 | |
| for branch in $fork_branches; do | |
| # Skip if branch exists in upstream | |
| for ub in $upstream_branches; do | |
| [ "$branch" = "$ub" ] && continue 2 | |
| done | |
| # Orphan branch — get its first commit (oldest = last page, last item) | |
| # We use the since/until trick: walk to the last page | |
| first_commit_author="" | |
| cp=1 | |
| while true; do | |
| cpage_resp=$(_api "/repos/${full_name}/commits?sha=${branch}&per_page=100&page=${cp}") | |
| page_shas=$(printf '%s' "$cpage_resp" | grep -c '"sha"' || true) | |
| [ "${page_shas:-0}" -gt 0 ] || break | |
| last_resp="$cpage_resp" | |
| cp=$((cp + 1)) | |
| done | |
| # Extract the last commit's author login from the last page | |
| first_commit_author=$(printf '%s' "$last_resp" \ | |
| | tr -d '\n' \ | |
| | grep -o '"author"[[:space:]]*:[[:space:]]*{[^}]*}' \ | |
| | tail -1 \ | |
| | grep -o '"login"[[:space:]]*:[[:space:]]*"[^"]*"' \ | |
| | sed 's/"login"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') | |
| if [ "$first_commit_author" = "$LOGIN" ]; then | |
| contributed=1 | |
| break | |
| fi | |
| done | |
| [ "$contributed" -eq 0 ] || continue | |
| never_contributed="${never_contributed}${full_name} | |
| " | |
| printf 'Never contributed: %s\n' "$full_name" >&2 | |
| done | |
| page=$((page + 1)) | |
| done | |
| # ── GitHub Actions step summary ─────────────────────────────────────────────── | |
| if [ -n "$GITHUB_STEP_SUMMARY" ]; then | |
| { | |
| printf '## Forks with no contribution from %s\n\n' "$LOGIN" | |
| if [ -n "$never_contributed" ]; then | |
| printf '%s' "$never_contributed" | while IFS= read -r repo; do | |
| [ -n "$repo" ] && printf '%s\n' "- [${repo}](https://github.com/${repo})" | |
| done | |
| else | |
| printf '_You have authored commits in all your forks._\n' | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment