Setting the file. One moment.
Gather gh Events · Daily Recap · bitwarden/ai-plugins · Skills Docs
ContentsBack to the top of the page 46.10
README
(opens in a new tab)
scripts/ gather-gh-events.sh
Shell · 163 lines · 6 KB
15 # TZ Standard timezone override, e.g. America/Los_Angeles.
16 #
17 # Output (stdout): a JSON object with "login", "window", "feed_complete", "event_count",
18 # and "events". The window line and any coverage warning also go to stderr.
19 #
20 # Requires: gh (authenticated), jq, and GNU or BSD date.
21
22 set -euo pipefail
23
24 SCRIPT_DIR = "$( cd "$( dirname "${ BASH_SOURCE [0]}")" && pwd )"
25 # shellcheck source=SCRIPTDIR/date-utils.sh
26 source "${ SCRIPT_DIR }/date-utils.sh"
27
28 die () {
29 echo "gather-gh-events: $* " >&2
30 exit 1
31 }
32
33 command -v jq > /dev/null 2>&1 || die "'jq' is required but not installed"
34
35 # GitHub's user events feed holds at most 300 events (3 pages of 100) from the last 30 days.
36 readonly PER_PAGE = 100
37 readonly MAX_PAGES = 3
38 readonly FEED_MAX_AGE_DAYS = 30
39
40 cutoff_raw = "${ DAILY_RECAP_CUTOFF_HOUR :- 7 }"
41 if ! [[ " $cutoff_raw " =~ ^[0-9]{1,2}$ ]] || (( 10 #$cutoff_raw > 23 )); then
42 die "DAILY_RECAP_CUTOFF_HOUR must be a whole hour from 0 to 23 (got '${ cutoff_raw }')"
43 fi
44 CUTOFF_HOUR = $(( 10#$cutoff_raw ))
45 CUTOFF_HH = "$( printf '%02d' " $CUTOFF_HOUR ")"
46
47 TODAY = "$( date +%Y-%m-%d)"
48 if [[ $# -ge 1 ]]; then
49 DATE = " $1 "
50 is_valid_date " $DATE " || die "date must be a real calendar date in YYYY-MM-DD form (got '${ DATE }')"
51 elif (( 10 #$(date +% H) >= CUTOFF_HOUR)); then
52 DATE = "$( add_days " $TODAY " -1 )"
53 else
54 DATE = "$( add_days " $TODAY " -2 )"
55 fi
56
57 NEXT_DATE = "$( add_days " $DATE " 1 )"
58 START_EPOCH = "$( local_to_epoch "${ DATE } ${ CUTOFF_HH }:00:00")" \
59 || die "could not compute the window start for ${ DATE } ${ CUTOFF_HH }:00 local time"
60 END_EPOCH = "$( local_to_epoch "${ NEXT_DATE } ${ CUTOFF_HH }:00:00")" \
61 || die "could not compute the window end for ${ NEXT_DATE } ${ CUTOFF_HH }:00 local time"
62
63 START_UTC = "$( epoch_fmt_utc " $START_EPOCH " '%Y-%m-%dT%H:%M:%SZ')"
64 END_UTC = "$( epoch_fmt_utc " $END_EPOCH " '%Y-%m-%dT%H:%M:%SZ')"
65 START_LOCAL = "$( epoch_fmt " $START_EPOCH " '%Y-%m-%dT%H:%M:%S%z')"
66 END_LOCAL = "$( epoch_fmt " $END_EPOCH " '%Y-%m-%dT%H:%M:%S%z')"
67 TZ_LABEL = "$( epoch_fmt " $START_EPOCH " '%Z')"
68
69 echo "# Window: ${ START_UTC } -> ${ END_UTC } (${ CUTOFF_HH }:00 ${ TZ_LABEL } boundary)" >&2
70
71 # Checked only after the window is printed, so a recap built without GitHub still has it.
72 command -v gh > /dev/null 2>&1 || die "'gh' is required but not installed"
73 LOGIN = "$( gh api user --jq .login 2> /dev/null)" \
74 || die "could not read your GitHub login; run 'gh auth login' and try again"
75
76 WORK_DIR = "$( mktemp -d "${ TMPDIR :-/ tmp }/daily-recap.XXXXXX")"
77 trap 'rm -rf "$WORK_DIR"' EXIT
78
79 # The feed is in delivery order, not created_at order: events from private repositories
80 # often arrive late carrying their original, older timestamps. An old timestamp on a page
81 # says nothing about how far back the feed reaches, and a page can come back a few events
82 # short mid-feed, so only an empty page marks the end.
83 feed_exhausted = false
84 page = 1
85 while ((page <= MAX_PAGES)); do
86 page_file = "${ WORK_DIR }/page-${ page }.json"
87 gh api "users/${ LOGIN }/events?per_page=${ PER_PAGE }&page=${ page }" > " $page_file " \
88 || die "GitHub events request failed on page ${ page }"
89 if (($(jq 'length' " $page_file " ) == 0 )); then
90 feed_exhausted = true
91 break
92 fi
93 page = $(( page + 1 ))
94 done
95
96 RAW = "${ WORK_DIR }/events.json"
97 jq -s 'add // []' " $WORK_DIR "/page- * .json > " $RAW "
98
99 # How far back the feed reaches is read from its tail, taking the newest of the last few
100 # events so one late arrival there cannot overstate the reach. The window is fully covered
101 # when that point is before its start, or when the feed ended naturally and the window is
102 # recent enough to fall inside the feed's retention.
103 reach_back = "$( jq -r '.[-10:] | map(.created_at) | max // ""' " $RAW ")"
104 retention_floor = $(($( date +%s ) - FEED_MAX_AGE_DAYS * 86400))
105 feed_complete = false
106 if [[ -n " $reach_back " && " $reach_back " < " $START_UTC " ]]; then
107 feed_complete = true
108 elif [[ " $feed_exhausted " == true ]] && ((START_EPOCH >= retention_floor)); then
109 feed_complete = true
110 fi
111 if [[ " $feed_complete " != true ]]; then
112 echo "# Warning: GitHub's events feed only reaches back to about ${ reach_back :- nothing } (300 events or ${ FEED_MAX_AGE_DAYS } days at most), so events early in this window may be missing." >&2
113 fi
114
115 jq \
116 --arg login " $LOGIN " \
117 --arg date " $DATE " \
118 --argjson cutoff " $CUTOFF_HOUR " \
119 --arg tz " $TZ_LABEL " \
120 --arg start " $START_UTC " \
121 --arg end " $END_UTC " \
122 --arg start_local " $START_LOCAL " \
123 --arg end_local " $END_LOCAL " \
124 --argjson complete " $feed_complete " '
125 [ .[]
126 | select(.created_at >= $start and .created_at < $end)
127 | {
128 time: .created_at,
129 type,
130 repo: .repo.name,
131 action: (.payload.action // null),
132 pr: (.payload.pull_request.number // .payload.issue.number // null),
133 pr_title: (.payload.pull_request.title // .payload.issue.title // null),
134 pr_author: (.payload.pull_request.user.login // .payload.issue.user.login // null),
135 pr_state: (.payload.pull_request.state // null),
136 pr_merged: (.payload.pull_request.merged // null),
137 pr_draft: (.payload.pull_request.draft // null),
138 url: (.payload.comment.html_url // .payload.review.html_url // .payload.pull_request.html_url // .payload.issue.html_url // null),
139 review_state: (.payload.review.state // null),
140 comment_body: (.payload.comment.body // .payload.review.body // null | if . then .[0:300] else null end),
141 comment_path: (.payload.comment.path // null),
142 ref: (.payload.ref // null),
143 ref_type: (.payload.ref_type // null),
144 head_sha: (.payload.head // null | if . then .[0:9] else null end),
145 commit_msgs: [.payload.commits[]? | .message | split("\n")[0]]
146 }
147 ] | sort_by(.time) as $events
148 | {
149 login: $login,
150 window: {
151 date: $date,
152 cutoff_hour: $cutoff,
153 timezone: $tz,
154 start_utc: $start,
155 end_utc: $end,
156 start_local: $start_local,
157 end_local: $end_local
158 },
159 feed_complete: $complete,
160 event_count: ($events | length),
161 events: $events
162 }
163 ' " $RAW "