Improve repository config, changelog notifications and API resilience
This commit is contained in:
@@ -8,3 +8,7 @@ GITHUB_MAX_PAGES=3
|
|||||||
REPOS_FILE=repos.json
|
REPOS_FILE=repos.json
|
||||||
STATE_FILE=state.json
|
STATE_FILE=state.json
|
||||||
INCLUDE_PRERELEASES=false
|
INCLUDE_PRERELEASES=false
|
||||||
|
|
||||||
|
TELEGRAM_INCLUDE_CHANGELOG=true
|
||||||
|
TELEGRAM_CHANGELOG_MAX_LENGTH=1800
|
||||||
|
STATE_MAX_IDS=500
|
||||||
|
|||||||
@@ -65,3 +65,12 @@ through command-line arguments. The state file is read directly by `jq`.
|
|||||||
This prevents `Argument list too long` for repositories with large release notes.
|
This prevents `Argument list too long` for repositories with large release notes.
|
||||||
|
|
||||||
Run `bash -n monitor.sh` and `./monitor.sh --dry-run --verbose` to verify.
|
Run `bash -n monitor.sh` and `./monitor.sh --dry-run --verbose` to verify.
|
||||||
|
|
||||||
|
### Release notes and retention
|
||||||
|
|
||||||
|
`repos.json` uses `owner/repo` names (legacy full GitHub URLs remain accepted).
|
||||||
|
`TELEGRAM_INCLUDE_CHANGELOG=true` includes a shortened release body in Telegram messages.
|
||||||
|
`TELEGRAM_CHANGELOG_MAX_LENGTH=1800` limits the changelog; Telegram messages are capped at 4096 characters.
|
||||||
|
`STATE_MAX_IDS=500` bounds stored release IDs per repository. Keep this larger than the number of releases that may appear in the fetched pages; old releases returning after pruning may otherwise be notified again.
|
||||||
|
|
||||||
|
GitHub HTTP 403/429 responses log rate-limit headers and fail without changing the repository state. Mid-pagination errors also fail the repository without partially processing it. Tags without GitHub releases are not tracked.
|
||||||
|
|||||||
+57
-18
@@ -57,6 +57,9 @@ set +a
|
|||||||
: "${REPOS_FILE:=repos.json}"
|
: "${REPOS_FILE:=repos.json}"
|
||||||
: "${STATE_FILE:=state.json}"
|
: "${STATE_FILE:=state.json}"
|
||||||
: "${INCLUDE_PRERELEASES:=false}"
|
: "${INCLUDE_PRERELEASES:=false}"
|
||||||
|
: "${TELEGRAM_INCLUDE_CHANGELOG:=true}"
|
||||||
|
: "${TELEGRAM_CHANGELOG_MAX_LENGTH:=1800}"
|
||||||
|
: "${STATE_MAX_IDS:=500}"
|
||||||
if [[ "$CLI_PRERELEASES" == true ]]; then INCLUDE_PRERELEASES=true; fi
|
if [[ "$CLI_PRERELEASES" == true ]]; then INCLUDE_PRERELEASES=true; fi
|
||||||
resolve_path() { if [[ "$1" == /* ]]; then printf '%s\n' "$1"; else printf '%s/%s\n' "$SCRIPT_DIR" "$1"; fi; }
|
resolve_path() { if [[ "$1" == /* ]]; then printf '%s\n' "$1"; else printf '%s/%s\n' "$SCRIPT_DIR" "$1"; fi; }
|
||||||
REPOS_FILE="$(resolve_path "$REPOS_FILE")"
|
REPOS_FILE="$(resolve_path "$REPOS_FILE")"
|
||||||
@@ -67,12 +70,18 @@ for cmd in curl jq flock mktemp; do command -v "$cmd" >/dev/null 2>&1 || die "Mi
|
|||||||
(( GITHUB_PER_PAGE <= 100 )) || die "GITHUB_PER_PAGE must be <= 100"
|
(( GITHUB_PER_PAGE <= 100 )) || die "GITHUB_PER_PAGE must be <= 100"
|
||||||
[[ "$GITHUB_MAX_PAGES" =~ ^[1-9][0-9]*$ ]] || die "Invalid GITHUB_MAX_PAGES"
|
[[ "$GITHUB_MAX_PAGES" =~ ^[1-9][0-9]*$ ]] || die "Invalid GITHUB_MAX_PAGES"
|
||||||
[[ "$INCLUDE_PRERELEASES" == true || "$INCLUDE_PRERELEASES" == false ]] || die "Invalid INCLUDE_PRERELEASES"
|
[[ "$INCLUDE_PRERELEASES" == true || "$INCLUDE_PRERELEASES" == false ]] || die "Invalid INCLUDE_PRERELEASES"
|
||||||
|
for var in TELEGRAM_CHANGELOG_MAX_LENGTH STATE_MAX_IDS; do
|
||||||
|
[[ "${!var}" =~ ^[1-9][0-9]*$ ]] || die "Invalid $var"
|
||||||
|
done
|
||||||
|
[[ "$TELEGRAM_INCLUDE_CHANGELOG" == true || "$TELEGRAM_INCLUDE_CHANGELOG" == false ]] || die 'Invalid TELEGRAM_INCLUDE_CHANGELOG'
|
||||||
if [[ "$TEST_TELEGRAM" == true || ( "$INIT" == false && "$DRY_RUN" == false ) ]]; then
|
if [[ "$TEST_TELEGRAM" == true || ( "$INIT" == false && "$DRY_RUN" == false ) ]]; then
|
||||||
[[ -n "$TELEGRAM_BOT_TOKEN" && -n "$TELEGRAM_CHAT_ID" ]] || die "Telegram credentials are missing"
|
[[ -n "$TELEGRAM_BOT_TOKEN" && -n "$TELEGRAM_CHAT_ID" ]] || die "Telegram credentials are missing"
|
||||||
fi
|
fi
|
||||||
umask 077
|
umask 077
|
||||||
CURL_OPTIONS=(--silent --show-error --fail --location --connect-timeout 10 --max-time "$REQUEST_TIMEOUT")
|
CURL_OPTIONS=(--silent --show-error --fail --location --connect-timeout 10 --max-time "$REQUEST_TIMEOUT")
|
||||||
if [[ -n "$SOCKS5_PROXY" ]]; then CURL_OPTIONS+=(--proxy "$SOCKS5_PROXY"); fi
|
if [[ -n "$SOCKS5_PROXY" ]]; then CURL_OPTIONS+=(--proxy "$SOCKS5_PROXY"); fi
|
||||||
|
GITHUB_CURL_OPTIONS=(--silent --show-error --location --connect-timeout 10 --max-time "$REQUEST_TIMEOUT")
|
||||||
|
if [[ -n "$SOCKS5_PROXY" ]]; then GITHUB_CURL_OPTIONS+=(--proxy "$SOCKS5_PROXY"); fi
|
||||||
GITHUB_HEADERS=(-H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' -H 'User-Agent: github-release-monitor')
|
GITHUB_HEADERS=(-H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' -H 'User-Agent: github-release-monitor')
|
||||||
if [[ -n "$GITHUB_TOKEN" ]]; then GITHUB_HEADERS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}"); fi
|
if [[ -n "$GITHUB_TOKEN" ]]; then GITHUB_HEADERS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}"); fi
|
||||||
debug() { if [[ "$VERBOSE" == true ]]; then log DEBUG "$1"; fi; }
|
debug() { if [[ "$VERBOSE" == true ]]; then log DEBUG "$1"; fi; }
|
||||||
@@ -89,33 +98,47 @@ send_telegram() {
|
|||||||
test_telegram() {
|
test_telegram() {
|
||||||
local message
|
local message
|
||||||
message="$(printf 'GitHub Release Monitor: Test notification\nHost: %s\nTime: %s\nStatus: Telegram connection successful' "$(hostname)" "$(date '+%Y-%m-%d %H:%M:%S %Z')")"
|
message="$(printf 'GitHub Release Monitor: Test notification\nHost: %s\nTime: %s\nStatus: Telegram connection successful' "$(hostname)" "$(date '+%Y-%m-%d %H:%M:%S %Z')")"
|
||||||
|
if [[ "$TELEGRAM_INCLUDE_CHANGELOG" == true ]]; then
|
||||||
|
message+=$'\n\nChangelog:\nTest changelog rendering enabled.'
|
||||||
|
fi
|
||||||
log INFO 'Sending test Telegram notification'
|
log INFO 'Sending test Telegram notification'
|
||||||
if send_telegram "$message"; then log INFO 'Test notification sent'; else log ERROR 'Test notification failed'; return 1; fi
|
if send_telegram "$message"; then log INFO 'Test notification sent'; else log ERROR 'Test notification failed'; return 1; fi
|
||||||
}
|
}
|
||||||
fetch_releases() {
|
fetch_releases() {
|
||||||
local repo="$1" page count response_file releases_file
|
local repo="$1" page count response_file releases_file headers_file http_code retry_after remaining reset
|
||||||
response_file="$(mktemp)" || return 1
|
response_file="$(mktemp)" || return 1
|
||||||
releases_file="$(mktemp)" || { rm -f "$response_file"; return 1; }
|
releases_file="$(mktemp)" || { rm -f "$response_file"; return 1; }
|
||||||
|
headers_file="$(mktemp)" || { rm -f "$response_file" "$releases_file"; return 1; }
|
||||||
|
|
||||||
for ((page=1; page<=GITHUB_MAX_PAGES; page++)); do
|
for ((page=1; page<=GITHUB_MAX_PAGES; page++)); do
|
||||||
debug "Fetching $repo page=$page"
|
debug "Fetching $repo page=$page"
|
||||||
if ! curl "${CURL_OPTIONS[@]}" "${GITHUB_HEADERS[@]}" \
|
if ! http_code="$(curl "${GITHUB_CURL_OPTIONS[@]}" "${GITHUB_HEADERS[@]}" \
|
||||||
|
--dump-header "$headers_file" --write-out '%{http_code}' \
|
||||||
--output "$response_file" \
|
--output "$response_file" \
|
||||||
"${GITHUB_API}/repos/${repo}/releases?per_page=${GITHUB_PER_PAGE}&page=${page}"; then
|
"${GITHUB_API}/repos/${repo}/releases?per_page=${GITHUB_PER_PAGE}&page=${page}")"; then
|
||||||
rm -f "$response_file" "$releases_file"
|
rm -f "$response_file" "$releases_file" "$headers_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$http_code" != 200 ]]; then
|
||||||
|
retry_after="$(awk 'BEGIN{IGNORECASE=1} tolower($1)=="retry-after:" {gsub("\r", "", $2); print $2}' "$headers_file" | tail -n 1)"
|
||||||
|
remaining="$(awk 'tolower($1)=="x-ratelimit-remaining:" {gsub("\r", "", $2); print $2}' "$headers_file" | tail -n 1)"
|
||||||
|
reset="$(awk 'tolower($1)=="x-ratelimit-reset:" {gsub("\r", "", $2); print $2}' "$headers_file" | tail -n 1)"
|
||||||
|
log ERROR "GitHub HTTP $http_code for $repo; rate_remaining=${remaining:-unknown}; rate_reset=${reset:-unknown}; retry_after=${retry_after:-unknown}"
|
||||||
|
rm -f "$response_file" "$releases_file" "$headers_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! count="$(jq -er 'if type == "array" then length else error("Invalid releases response") end' "$response_file")"; then
|
if ! count="$(jq -er 'if type == "array" then length else error("Invalid releases response") end' "$response_file")"; then
|
||||||
rm -f "$response_file" "$releases_file"
|
rm -f "$response_file" "$releases_file" "$headers_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! jq -c --argjson prereleases "$INCLUDE_PRERELEASES" '
|
if ! jq -c --argjson prereleases "$INCLUDE_PRERELEASES" '
|
||||||
.[] | select(.draft == false) |
|
.[] | select(.draft == false) |
|
||||||
select($prereleases or (.prerelease == false)) |
|
select($prereleases or (.prerelease == false)) |
|
||||||
{id: (.id | tostring), tag: .tag_name, name: .name,
|
{id: (.id | tostring), tag: .tag_name, name: .name,
|
||||||
url: .html_url, published: .published_at, prerelease: .prerelease}
|
url: .html_url, published: .published_at, prerelease: .prerelease,
|
||||||
|
body: (.body // "")}
|
||||||
' "$response_file" >> "$releases_file"; then
|
' "$response_file" >> "$releases_file"; then
|
||||||
rm -f "$response_file" "$releases_file"
|
rm -f "$response_file" "$releases_file" "$headers_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if (( count < GITHUB_PER_PAGE )); then break; fi
|
if (( count < GITHUB_PER_PAGE )); then break; fi
|
||||||
@@ -123,22 +146,22 @@ fetch_releases() {
|
|||||||
|
|
||||||
local status=0
|
local status=0
|
||||||
jq -sc '.' "$releases_file" || status=$?
|
jq -sc '.' "$releases_file" || status=$?
|
||||||
rm -f "$response_file" "$releases_file"
|
rm -f "$response_file" "$releases_file" "$headers_file"
|
||||||
return "$status"
|
return "$status"
|
||||||
}
|
}
|
||||||
|
|
||||||
write_state() {
|
write_state() {
|
||||||
local repo="$1" ids="$2" mode="$3" temp
|
local repo="$1" ids="$2" mode="$3" temp
|
||||||
temp="$(mktemp "${STATE_FILE}.tmp.XXXXXX")" || return 1
|
temp="$(mktemp "${STATE_FILE}.tmp.XXXXXX")" || return 1
|
||||||
if ! jq --arg repo "$repo" --arg mode "$mode" --slurpfile ids /dev/stdin '
|
if ! jq --arg repo "$repo" --arg mode "$mode" --argjson limit "$STATE_MAX_IDS" --slurpfile ids /dev/stdin '
|
||||||
($ids[0] // []) as $new_ids |
|
($ids[0] // []) as $new_ids |
|
||||||
if $mode == "replace" then .[$repo] = $new_ids
|
if $mode == "replace" then .[$repo] = ($new_ids | unique | .[-$limit:])
|
||||||
else .[$repo] = ((.[$repo] // []) + $new_ids | unique) end
|
else .[$repo] = ((.[$repo] // []) + $new_ids | unique | .[-$limit:]) end
|
||||||
' "$STATE_FILE" <<< "$ids" > "$temp"; then rm -f "$temp"; return 1; fi
|
' "$STATE_FILE" <<< "$ids" > "$temp"; then rm -f "$temp"; return 1; fi
|
||||||
if ! mv -f "$temp" "$STATE_FILE"; then rm -f "$temp"; return 1; fi
|
if ! mv -f "$temp" "$STATE_FILE"; then rm -f "$temp"; return 1; fi
|
||||||
}
|
}
|
||||||
format_message() {
|
format_message() {
|
||||||
local repo="$1" release="$2" tag name url published kind
|
local repo="$1" release="$2" tag name url published kind body message allowance
|
||||||
tag="$(jq -r '.tag // "unknown"' <<< "$release")"
|
tag="$(jq -r '.tag // "unknown"' <<< "$release")"
|
||||||
name="$(jq -r '.name // empty' <<< "$release")"
|
name="$(jq -r '.name // empty' <<< "$release")"
|
||||||
url="$(jq -r '.url // empty' <<< "$release")"
|
url="$(jq -r '.url // empty' <<< "$release")"
|
||||||
@@ -146,8 +169,20 @@ format_message() {
|
|||||||
[[ -n "$name" ]] || name="$tag"
|
[[ -n "$name" ]] || name="$tag"
|
||||||
kind=Release
|
kind=Release
|
||||||
if [[ "$(jq -r '.prerelease' <<< "$release")" == true ]]; then kind=Prerelease; fi
|
if [[ "$(jq -r '.prerelease' <<< "$release")" == true ]]; then kind=Prerelease; fi
|
||||||
printf 'GitHub: New %s\nRepository: %s\nVersion: %s\nName: %s\nPublished: %s\nURL: %s' \
|
message="$(printf 'GitHub: New %s\nRepository: %s\nVersion: %s\nName: %s\nPublished: %s' \
|
||||||
"$kind" "$repo" "$tag" "$name" "$published" "$url"
|
"$kind" "$repo" "$tag" "$name" "$published")"
|
||||||
|
if [[ "$TELEGRAM_INCLUDE_CHANGELOG" == true ]]; then
|
||||||
|
body="$(jq -r '.body // ""' <<< "$release" | sed -E 's/<[^>]*>/ /g; s/!\[[^]]*\]\([^)]*\)//g')"
|
||||||
|
allowance=$((4096 - ${#message} - ${#url} - 24))
|
||||||
|
if (( allowance > TELEGRAM_CHANGELOG_MAX_LENGTH )); then allowance="$TELEGRAM_CHANGELOG_MAX_LENGTH"; fi
|
||||||
|
if (( allowance > 0 )) && [[ -n "$body" ]]; then
|
||||||
|
body="$(printf '%s' "$body" | jq -Rs --argjson max "$allowance" '.[:$max]' -r)"
|
||||||
|
message+="$(printf '\n\nChangelog:\n%s' "$body")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
message+="$(printf '\n\n%s' "$url")"
|
||||||
|
# Telegram limit is 4096 Unicode characters, not bytes.
|
||||||
|
printf '%s' "$message" | jq -Rs '.[:4096]' -r
|
||||||
}
|
}
|
||||||
process_repo() {
|
process_repo() {
|
||||||
local repo="$1" releases ids previous new_releases count release id tag message
|
local repo="$1" releases ids previous new_releases count release id tag message
|
||||||
@@ -210,13 +245,17 @@ main() {
|
|||||||
fi
|
fi
|
||||||
while IFS= read -r url; do
|
while IFS= read -r url; do
|
||||||
[[ -n "$url" ]] || continue
|
[[ -n "$url" ]] || continue
|
||||||
if [[ ! "$url" =~ ^https://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)/?$ ]]; then
|
if [[ "$url" =~ ^https://(www\.)?github\.com/ ]]; then
|
||||||
log ERROR "Invalid GitHub URL: $url"
|
url="${url#*github.com/}"
|
||||||
|
fi
|
||||||
|
url="${url%/}"
|
||||||
|
url="${url%.git}"
|
||||||
|
if [[ ! "$url" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
||||||
|
log ERROR "Invalid repository: $url"
|
||||||
failures=$((failures + 1))
|
failures=$((failures + 1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
repo="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
|
repo="$url"
|
||||||
repo="${repo%.git}"
|
|
||||||
if ! process_repo "$repo"; then failures=$((failures + 1)); fi
|
if ! process_repo "$repo"; then failures=$((failures + 1)); fi
|
||||||
done < <(jq -r '.repositories[]' "$REPOS_FILE" | sort -u)
|
done < <(jq -r '.repositories[]' "$REPOS_FILE" | sort -u)
|
||||||
if (( failures > 0 )); then log ERROR "Failed repositories: $failures"; return 1; fi
|
if (( failures > 0 )); then log ERROR "Failed repositories: $failures"; return 1; fi
|
||||||
|
|||||||
+3
-4
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"repositories": [
|
"repositories": [
|
||||||
"https://github.com/LycheeOrg/Lychee",
|
"LycheeOrg/Lychee",
|
||||||
"https://github.com/firefly-iii/firefly-iii",
|
"firefly-iii/firefly-iii",
|
||||||
"https://github.com/usememos/memos"
|
"usememos/memos"
|
||||||
]
|
]
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user