|
#!/bin/bash |
|
# gh-subscribe.sh — subscribe/unsubscribe/list GitHub issue (or PR) notifications. |
|
# |
|
# WHY: the GitHub REST API can't subscribe you to an arbitrary issue; the GraphQL |
|
# `updateSubscription` mutation can (an Issue is a Subscribable). This wraps it so |
|
# you don't re-derive the mutation each time. Handy for tracking upstream bug |
|
# reports you care about across the tools you depend on. |
|
# |
|
# IDENTITY: uses your *active* gh account (whatever `gh auth status` shows / your |
|
# GH_CONFIG_DIR). Portable, repo-less — drop it on any machine with gh. |
|
# |
|
# REQUIRES: the `notifications` OAuth scope. Add it once with: |
|
# gh auth refresh -h github.com -s notifications |
|
# Without it you'll get INSUFFICIENT_SCOPES. Subscriptions are PRIVATE |
|
# (viewerSubscription is per-user, invisible to others). |
|
# |
|
# Usage: |
|
# ./gh-subscribe.sh <owner/repo> <issue#> [issue# ...] # subscribe |
|
# ./gh-subscribe.sh --list <owner/repo> <issue#> [...] # show state |
|
# ./gh-subscribe.sh --unsub <owner/repo> <issue#> [...] # unsubscribe |
|
# |
|
# Examples: |
|
# ./gh-subscribe.sh cli/cli 1234 |
|
# ./gh-subscribe.sh --list cli/cli 1234 5678 |
|
|
|
set -uo pipefail |
|
|
|
STATE="SUBSCRIBED" |
|
MODE="set" |
|
case "${1:-}" in |
|
--list) MODE="list"; shift ;; |
|
--unsub) STATE="UNSUBSCRIBED"; shift ;; |
|
-h|--help|"") grep -E '^#( |$)' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; |
|
esac |
|
|
|
REPO="${1:-}"; shift || true |
|
OWNER="${REPO%%/*}"; NAME="${REPO##*/}" |
|
# Require exactly one '/', both sides non-empty (rejects bareword, owner/, /repo, |
|
# and 3-segment owner/x/repo which would silently drop the middle). |
|
if [[ "$REPO" != */* || "$REPO" == */*/* ]] || [ -z "$OWNER" ] || [ -z "$NAME" ] || [ $# -eq 0 ]; then |
|
echo "usage: $0 [--list|--unsub] <owner/repo> <issue#> [issue# ...]" >&2 |
|
exit 2 |
|
fi |
|
|
|
# Resolve node id + current state for one issue number. Echoes |
|
# "id<TAB>title<TAB>state" on a real hit; empty on a non-existent issue (jq |
|
# `select(.)` drops a null issue); tabs/newlines in the title are squashed so |
|
# the field split stays aligned. Returns gh's exit status so the caller can |
|
# tell a genuine API/auth/network failure apart from a clean "not found". |
|
resolve() { # arg: issue number |
|
gh api graphql \ |
|
-f query='query($o:String!,$n:String!,$num:Int!){repository(owner:$o,name:$n){issue(number:$num){id title viewerSubscription}}}' \ |
|
-f o="$OWNER" -f n="$NAME" -F num="$1" \ |
|
--jq '.data.repository.issue | select(.) | "\(.id)\t\((.title // "")[0:60] | gsub("[\t\n]";" "))\t\(.viewerSubscription)"' |
|
} |
|
|
|
ERRF=$(mktemp) |
|
trap 'rm -f "$ERRF"' EXIT |
|
|
|
rc=0 |
|
for num in "$@"; do |
|
if ! [[ "$num" =~ ^[0-9]+$ ]]; then |
|
printf '#%-6s ✗ not a positive issue number\n' "$num"; rc=1; continue |
|
fi |
|
|
|
if ! row=$(resolve "$num" 2>"$ERRF"); then |
|
printf '#%-6s ✗ gh error: %s\n' "$num" "$(head -1 "$ERRF")"; rc=1; continue |
|
fi |
|
if [ -z "$row" ]; then |
|
printf '#%-6s ✗ not found in %s\n' "$num" "$REPO"; rc=1; continue |
|
fi |
|
id="${row%%$'\t'*}"; rest="${row#*$'\t'}"; title="${rest%%$'\t'*}"; cur="${rest##*$'\t'}" |
|
|
|
if [ "$MODE" = "list" ]; then |
|
printf '#%-6s [%s] %s\n' "$num" "$cur" "$title" |
|
continue |
|
fi |
|
|
|
new=$(gh api graphql \ |
|
-f query='mutation($id:ID!,$s:SubscriptionState!){updateSubscription(input:{subscribableId:$id,state:$s}){subscribable{viewerSubscription}}}' \ |
|
-f id="$id" -f s="$STATE" \ |
|
--jq '.data.updateSubscription.subscribable.viewerSubscription' 2>&1) |
|
if [ "$new" = "$STATE" ]; then |
|
printf '#%-6s ✓ %s %s\n' "$num" "$new" "$title" |
|
else |
|
printf '#%-6s ✗ failed: %s\n' "$num" "$(printf '%s' "$new" | head -1)"; rc=1 |
|
fi |
|
done |
|
exit "$rc" |