|
|
@@ -0,0 +1,109 @@
|
|
|
+#!/usr/bin/env bash
|
|
|
+# Surface open GitHub issues you may not have seen — externally-authored and stale.
|
|
|
+#
|
|
|
+# Read-only (gh issue list). Built to flag the blind spot: issues filed by someone
|
|
|
+# other than the repo owner, and issues left untouched for a while. Designed to run
|
|
|
+# advisory at push-time without ever gating the push.
|
|
|
+#
|
|
|
+# Usage: check-issues.sh [--repo OWNER/REPO | --remote NAME] [--stale-days N]
|
|
|
+# [--limit N] [--advisory] [--json] [-h|--help]
|
|
|
+# Input: argv only. Default repo = derived from the 'origin' remote of the cwd.
|
|
|
+# Output: stdout = data (human summary, or --json envelope). Framing on stderr.
|
|
|
+# Stderr: headers, the advisory banner, skip notices, errors.
|
|
|
+# Exit: 0 nothing you're missing (no open issues, or all are yours and fresh)
|
|
|
+# 2 usage
|
|
|
+# 5 gh not installed (standalone mode; --advisory downgrades this to a skip)
|
|
|
+# 7 unavailable — not a GitHub remote, gh not authed, offline, rate-limited,
|
|
|
+# or the lookup timed out (ADVISORY signal; never a real failure)
|
|
|
+# 10 open external and/or stale issues present (the thing to look at)
|
|
|
+#
|
|
|
+# Examples:
|
|
|
+# check-issues.sh # origin of the cwd
|
|
|
+# check-issues.sh --repo 0xDarkMatter/flarecrawl
|
|
|
+# check-issues.sh --remote origin --stale-days 14
|
|
|
+# check-issues.sh --json | jq '.data[] | select(.external)'
|
|
|
+# check-issues.sh --advisory --remote origin # compact, silent when clean
|
|
|
+set -uo pipefail
|
|
|
+
|
|
|
+EX_OK=0; EX_USAGE=2; EX_MISSING_DEP=5; EX_UNAVAILABLE=7; EX_FINDINGS=10
|
|
|
+GH_TIMEOUT="${GH_TIMEOUT:-15}" # seconds; bounds the network call
|
|
|
+
|
|
|
+REPO=""; REMOTE="origin"; STALE_DAYS=30; LIMIT=50; ADVISORY=0; JSON=0
|
|
|
+while [ $# -gt 0 ]; do
|
|
|
+ case "$1" in
|
|
|
+ --repo) REPO="${2:?--repo needs OWNER/REPO}"; shift 2 ;;
|
|
|
+ --remote) REMOTE="${2:?--remote needs a name}"; shift 2 ;;
|
|
|
+ --stale-days) STALE_DAYS="${2:?--stale-days needs N}"; shift 2 ;;
|
|
|
+ --limit) LIMIT="${2:?--limit needs N}"; shift 2 ;;
|
|
|
+ --advisory) ADVISORY=1; shift ;;
|
|
|
+ --json) JSON=1; shift ;;
|
|
|
+ -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit "$EX_OK" ;;
|
|
|
+ *) echo "check-issues: unknown argument: $1" >&2; exit "$EX_USAGE" ;;
|
|
|
+ esac
|
|
|
+done
|
|
|
+
|
|
|
+# In advisory mode, ANY inability to check is a silent skip (never disturb a push).
|
|
|
+skip() { # message
|
|
|
+ [ "$ADVISORY" -eq 1 ] || echo "check-issues: $1" >&2
|
|
|
+ exit "$EX_UNAVAILABLE"
|
|
|
+}
|
|
|
+
|
|
|
+command -v gh >/dev/null 2>&1 || {
|
|
|
+ [ "$ADVISORY" -eq 1 ] && exit "$EX_UNAVAILABLE"
|
|
|
+ echo "check-issues: gh not installed (https://cli.github.com)" >&2
|
|
|
+ exit "$EX_MISSING_DEP"
|
|
|
+}
|
|
|
+
|
|
|
+# Resolve OWNER/REPO from the remote if not given explicitly.
|
|
|
+if [ -z "$REPO" ]; then
|
|
|
+ url="$(git remote get-url "$REMOTE" 2>/dev/null)" || skip "no '$REMOTE' remote here"
|
|
|
+ case "$url" in
|
|
|
+ *github.com[:/]*)
|
|
|
+ # strip everything up to github.com<sep>, then a trailing .git and/or slash
|
|
|
+ REPO="$(printf '%s' "$url" | sed -E 's#^.*github\.com[:/]+##; s#\.git$##; s#/$##')" ;;
|
|
|
+ *) skip "remote '$REMOTE' is not a github.com repo" ;;
|
|
|
+ esac
|
|
|
+fi
|
|
|
+OWNER="${REPO%%/*}"
|
|
|
+
|
|
|
+# Bounded, read-only lookup. Any failure (auth/offline/rate-limit/timeout) -> skip/7.
|
|
|
+runner() { if command -v timeout >/dev/null 2>&1; then timeout "$GH_TIMEOUT" "$@"; else "$@"; fi; }
|
|
|
+raw="$(runner gh issue list --repo "$REPO" --state open --limit "$LIMIT" \
|
|
|
+ --json number,title,author,createdAt,updatedAt,labels 2>/dev/null)" \
|
|
|
+ || skip "gh issue list failed for $REPO (not authed / offline / rate-limited?)"
|
|
|
+[ -n "$raw" ] || skip "empty response from gh for $REPO"
|
|
|
+
|
|
|
+# Classify with jq: external = author.login != owner; stale = updatedAt older than N days.
|
|
|
+command -v jq >/dev/null 2>&1 || skip "jq not installed"
|
|
|
+analysis="$(printf '%s' "$raw" | jq -c --arg owner "$OWNER" --argjson stale "$STALE_DAYS" '
|
|
|
+ (now - ($stale * 86400)) as $cutoff
|
|
|
+ | map(. + {
|
|
|
+ external: (.author.login != $owner),
|
|
|
+ stale: ((.updatedAt | sub("\\.[0-9]+";"") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) < $cutoff)
|
|
|
+ })
|
|
|
+ | { total: length,
|
|
|
+ flagged: map(select(.external or .stale)),
|
|
|
+ }' 2>/dev/null)" || skip "could not parse gh output"
|
|
|
+
|
|
|
+total="$(printf '%s' "$analysis" | jq -r '.total')"
|
|
|
+flagged_n="$(printf '%s' "$analysis" | jq -r '.flagged | length')"
|
|
|
+
|
|
|
+if [ "$JSON" -eq 1 ]; then
|
|
|
+ printf '%s' "$analysis" | jq -c --arg repo "$REPO" \
|
|
|
+ '{data: .flagged, meta: {repo: $repo, total_open: .total, flagged: (.flagged|length), schema: "claude-mods.github-ops.check-issues/v1"}}'
|
|
|
+fi
|
|
|
+
|
|
|
+# Human / advisory output (stderr framing; the data above is the stdout product).
|
|
|
+if [ "$flagged_n" -eq 0 ]; then
|
|
|
+ [ "$ADVISORY" -eq 1 ] || echo "check-issues: $REPO — $total open, none external or stale." >&2
|
|
|
+ exit "$EX_OK"
|
|
|
+fi
|
|
|
+
|
|
|
+{
|
|
|
+ echo "OPEN ISSUES worth a look — $REPO ($flagged_n of $total open flagged):"
|
|
|
+ printf '%s' "$analysis" | jq -r '.flagged[]
|
|
|
+ | " #\(.number) [\(if .external then "external" else "yours" end)\(if .stale then ",stale" else "" end)] by \(.author.login) \(.title)"'
|
|
|
+ echo " → gh issue view <n> --repo $REPO (read-only; this never blocks a push)"
|
|
|
+} >&2
|
|
|
+
|
|
|
+exit "$EX_FINDINGS"
|