diff --git a/README.md b/README.md index e695b1a..932133f 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,11 @@ At 09:00 server-local time: ``` Run the monitor under a dedicated user where possible. Keep `.env` private and do not commit it. Use `./monitor.sh --test-telegram` to verify Telegram and proxy connectivity. + +### Large release histories + +GitHub response pages are processed through temporary files, rather than passed +through command-line arguments. The state file is read directly by `jq`. +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. diff --git a/monitor.sh b/monitor.sh old mode 100644 new mode 100755 index cfc5f4e..7e30030 --- a/monitor.sh +++ b/monitor.sh @@ -93,29 +93,48 @@ test_telegram() { if send_telegram "$message"; then log INFO 'Test notification sent'; else log ERROR 'Test notification failed'; return 1; fi } fetch_releases() { - local repo="$1" page response count result='[]' + local repo="$1" page count response_file releases_file + response_file="$(mktemp)" || return 1 + releases_file="$(mktemp)" || { rm -f "$response_file"; return 1; } + for ((page=1; page<=GITHUB_MAX_PAGES; page++)); do debug "Fetching $repo page=$page" - response="$(curl "${CURL_OPTIONS[@]}" "${GITHUB_HEADERS[@]}" \ - "${GITHUB_API}/repos/${repo}/releases?per_page=${GITHUB_PER_PAGE}&page=${page}")" || return 1 - jq -e 'type == "array"' <<< "$response" >/dev/null || return 1 - count="$(jq 'length' <<< "$response")" - result="$(jq -cn --argjson old "$result" --argjson new "$response" '$old + $new')" || return 1 + if ! curl "${CURL_OPTIONS[@]}" "${GITHUB_HEADERS[@]}" \ + --output "$response_file" \ + "${GITHUB_API}/repos/${repo}/releases?per_page=${GITHUB_PER_PAGE}&page=${page}"; then + rm -f "$response_file" "$releases_file" + return 1 + fi + if ! count="$(jq -er 'if type == "array" then length else error("Invalid releases response") end' "$response_file")"; then + rm -f "$response_file" "$releases_file" + return 1 + fi + if ! jq -c --argjson prereleases "$INCLUDE_PRERELEASES" ' + .[] | select(.draft == false) | + select($prereleases or (.prerelease == false)) | + {id: (.id | tostring), tag: .tag_name, name: .name, + url: .html_url, published: .published_at, prerelease: .prerelease} + ' "$response_file" >> "$releases_file"; then + rm -f "$response_file" "$releases_file" + return 1 + fi if (( count < GITHUB_PER_PAGE )); then break; fi done - jq -c --argjson prereleases "$INCLUDE_PRERELEASES" ' - [.[] | select(.draft == false) | select($prereleases or (.prerelease == false)) | - {id: (.id | tostring), tag: .tag_name, name: .name, url: .html_url, - published: .published_at, prerelease: .prerelease}] - ' <<< "$result" + + local status=0 + jq -sc '.' "$releases_file" || status=$? + rm -f "$response_file" "$releases_file" + return "$status" } + write_state() { local repo="$1" ids="$2" mode="$3" temp temp="$(mktemp "${STATE_FILE}.tmp.XXXXXX")" || return 1 - if ! jq --arg repo "$repo" --argjson ids "$ids" --arg mode "$mode" ' - if $mode == "replace" then .[$repo] = $ids - else .[$repo] = ((.[$repo] // []) + $ids | unique) end - ' "$STATE_FILE" > "$temp"; then rm -f "$temp"; return 1; fi + if ! jq --arg repo "$repo" --arg mode "$mode" --slurpfile ids /dev/stdin ' + ($ids[0] // []) as $new_ids | + if $mode == "replace" then .[$repo] = $new_ids + else .[$repo] = ((.[$repo] // []) + $new_ids | unique) end + ' "$STATE_FILE" <<< "$ids" > "$temp"; then rm -f "$temp"; return 1; fi if ! mv -f "$temp" "$STATE_FILE"; then rm -f "$temp"; return 1; fi } format_message() { @@ -151,7 +170,13 @@ process_repo() { log INFO "Baseline initialized: $repo ($count releases)" return 0 fi - new_releases="$(jq -c --argjson seen "$previous" '[.[] | select(.id as $id | $seen | index($id) | not)] | reverse' <<< "$releases")" || return 1 + # Read the existing state from a file, not from argv (ARG_MAX). + local state_source="$STATE_FILE" + [[ -f "$state_source" ]] || state_source=/dev/null + new_releases="$(jq -c --slurpfile state "$state_source" --arg repo "$repo" ' + ($state[0][$repo] // []) as $seen | + [.[] | select(.id as $id | $seen | index($id) | not)] | reverse + ' <<< "$releases")" || return 1 count="$(jq 'length' <<< "$new_releases")" if (( count == 0 )); then log INFO "No new releases: $repo"; return 0; fi log INFO "Found $count new releases: $repo"