Init files
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
|
GITHUB_TOKEN=
|
||||||
|
SOCKS5_PROXY=socks5h://127.0.0.1:1080
|
||||||
|
REQUEST_TIMEOUT=30
|
||||||
|
GITHUB_PER_PAGE=100
|
||||||
|
GITHUB_MAX_PAGES=3
|
||||||
|
REPOS_FILE=repos.json
|
||||||
|
STATE_FILE=state.json
|
||||||
|
INCLUDE_PRERELEASES=false
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
state.json
|
||||||
|
state.json.lock
|
||||||
|
state.json.tmp.*
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# GitHub Release Monitor
|
||||||
|
|
||||||
|
Bash monitor for GitHub releases, with Telegram notifications and SOCKS5 proxy support.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Bash 4+
|
||||||
|
- curl with SOCKS5 support
|
||||||
|
- jq
|
||||||
|
- flock (util-linux)
|
||||||
|
- mktemp (coreutils)
|
||||||
|
|
||||||
|
Debian:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install curl jq ca-certificates util-linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
chmod 600 .env
|
||||||
|
chmod +x monitor.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` in `.env`. `GITHUB_TOKEN` is optional for public repositories. `SOCKS5_PROXY` can be set to `socks5h://127.0.0.1:1080`; `socks5h` resolves DNS through the proxy. The `.env` file is sourced as Bash: use only trusted local configuration.
|
||||||
|
|
||||||
|
Edit `repos.json` to select GitHub repositories. Initial test set: Lychee, Firefly III and Memos.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./monitor.sh --test-telegram
|
||||||
|
./monitor.sh --dry-run --verbose
|
||||||
|
./monitor.sh --init
|
||||||
|
./monitor.sh
|
||||||
|
./monitor.sh --include-prereleases
|
||||||
|
```
|
||||||
|
|
||||||
|
`--test-telegram` sends one Telegram test message without querying GitHub or accessing `state.json`/`repos.json`.
|
||||||
|
|
||||||
|
`--init` records current releases without notifications; it **replaces** existing baselines. Use only for first initialization or an intentional reset. Normal first run also initializes missing repositories silently.
|
||||||
|
|
||||||
|
`--dry-run` queries GitHub and displays pending notifications without sending or changing state. On a repository without baseline it prints its current release tags.
|
||||||
|
|
||||||
|
`--include-prereleases` overrides `INCLUDE_PRERELEASES=false`. To track prereleases continuously, set `INCLUDE_PRERELEASES=true` in `.env`.
|
||||||
|
|
||||||
|
State is written atomically after each successful Telegram notification. If Telegram accepted a message but its response was lost, a duplicate may be sent next time. The state contains release IDs and grows over time. A bounded GitHub pagination window (`GITHUB_PER_PAGE * GITHUB_MAX_PAGES`) may miss releases if more are published between checks. For small projects and daily checks this is generally sufficient.
|
||||||
|
|
||||||
|
## Daily cron
|
||||||
|
|
||||||
|
At 09:00 server-local time:
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 9 * * * cd /opt/github-release-monitor && ./monitor.sh >> monitor.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Usage:
|
||||||
|
# ./monitor.sh --init
|
||||||
|
# ./monitor.sh --dry-run [--verbose]
|
||||||
|
# ./monitor.sh --test-telegram
|
||||||
|
# ./monitor.sh [--include-prereleases]
|
||||||
|
set -Eeuo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||||
|
DRY_RUN=false
|
||||||
|
INIT=false
|
||||||
|
VERBOSE=false
|
||||||
|
TEST_TELEGRAM=false
|
||||||
|
CLI_PRERELEASES=false
|
||||||
|
GITHUB_API="https://api.github.com"
|
||||||
|
TELEGRAM_API="https://api.telegram.org"
|
||||||
|
log() { printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >&2; }
|
||||||
|
die() { log ERROR "$1"; exit 1; }
|
||||||
|
usage() {
|
||||||
|
cat <<'HELP'
|
||||||
|
Usage: ./monitor.sh [OPTIONS]
|
||||||
|
--init Set baseline without notifications
|
||||||
|
--dry-run Show changes without sending or saving
|
||||||
|
--test-telegram Send one test message; do not query GitHub
|
||||||
|
--include-prereleases Include prereleases
|
||||||
|
--verbose Enable debug logging
|
||||||
|
--help Show help
|
||||||
|
HELP
|
||||||
|
}
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--init) INIT=true ;;
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--test-telegram) TEST_TELEGRAM=true ;;
|
||||||
|
--include-prereleases) CLI_PRERELEASES=true ;;
|
||||||
|
--verbose) VERBOSE=true ;;
|
||||||
|
--help) usage; exit 0 ;;
|
||||||
|
*) die "Unknown argument: $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ "$INIT" == true && "$DRY_RUN" == true ]]; then die "--init and --dry-run cannot be combined"; fi
|
||||||
|
if [[ "$TEST_TELEGRAM" == true && ( "$INIT" == true || "$DRY_RUN" == true ) ]]; then
|
||||||
|
die "--test-telegram cannot be combined with --init or --dry-run"
|
||||||
|
fi
|
||||||
|
[[ -f "$ENV_FILE" ]] || die "Missing configuration: $ENV_FILE"
|
||||||
|
set -a
|
||||||
|
# Trusted local shell configuration. Never source untrusted .env files.
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
: "${TELEGRAM_BOT_TOKEN:=}"
|
||||||
|
: "${TELEGRAM_CHAT_ID:=}"
|
||||||
|
: "${GITHUB_TOKEN:=}"
|
||||||
|
: "${SOCKS5_PROXY:=}"
|
||||||
|
: "${REQUEST_TIMEOUT:=30}"
|
||||||
|
: "${GITHUB_PER_PAGE:=100}"
|
||||||
|
: "${GITHUB_MAX_PAGES:=3}"
|
||||||
|
: "${REPOS_FILE:=repos.json}"
|
||||||
|
: "${STATE_FILE:=state.json}"
|
||||||
|
: "${INCLUDE_PRERELEASES:=false}"
|
||||||
|
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; }
|
||||||
|
REPOS_FILE="$(resolve_path "$REPOS_FILE")"
|
||||||
|
STATE_FILE="$(resolve_path "$STATE_FILE")"
|
||||||
|
for cmd in curl jq flock mktemp; do command -v "$cmd" >/dev/null 2>&1 || die "Missing dependency: $cmd"; done
|
||||||
|
[[ "$REQUEST_TIMEOUT" =~ ^[1-9][0-9]*$ ]] || die "Invalid REQUEST_TIMEOUT"
|
||||||
|
[[ "$GITHUB_PER_PAGE" =~ ^[1-9][0-9]*$ ]] || die "Invalid GITHUB_PER_PAGE"
|
||||||
|
(( GITHUB_PER_PAGE <= 100 )) || die "GITHUB_PER_PAGE must be <= 100"
|
||||||
|
[[ "$GITHUB_MAX_PAGES" =~ ^[1-9][0-9]*$ ]] || die "Invalid GITHUB_MAX_PAGES"
|
||||||
|
[[ "$INCLUDE_PRERELEASES" == true || "$INCLUDE_PRERELEASES" == false ]] || die "Invalid INCLUDE_PRERELEASES"
|
||||||
|
if [[ "$TEST_TELEGRAM" == true || ( "$INIT" == false && "$DRY_RUN" == false ) ]]; then
|
||||||
|
[[ -n "$TELEGRAM_BOT_TOKEN" && -n "$TELEGRAM_CHAT_ID" ]] || die "Telegram credentials are missing"
|
||||||
|
fi
|
||||||
|
umask 077
|
||||||
|
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
|
||||||
|
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
|
||||||
|
debug() { if [[ "$VERBOSE" == true ]]; then log DEBUG "$1"; fi; }
|
||||||
|
send_telegram() {
|
||||||
|
local message="$1" response
|
||||||
|
# Do not retry POST: retries can create duplicate Telegram messages.
|
||||||
|
response="$(curl "${CURL_OPTIONS[@]}" --request POST \
|
||||||
|
--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
|
--data-urlencode "text=${message}" \
|
||||||
|
--data-urlencode 'disable_web_page_preview=true' \
|
||||||
|
"${TELEGRAM_API}/bot${TELEGRAM_BOT_TOKEN}/sendMessage")" || return 1
|
||||||
|
jq -e '.ok == true' <<< "$response" >/dev/null
|
||||||
|
}
|
||||||
|
test_telegram() {
|
||||||
|
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')")"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
fetch_releases() {
|
||||||
|
local repo="$1" page response count result='[]'
|
||||||
|
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 (( 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"
|
||||||
|
}
|
||||||
|
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 ! mv -f "$temp" "$STATE_FILE"; then rm -f "$temp"; return 1; fi
|
||||||
|
}
|
||||||
|
format_message() {
|
||||||
|
local repo="$1" release="$2" tag name url published kind
|
||||||
|
tag="$(jq -r '.tag // "unknown"' <<< "$release")"
|
||||||
|
name="$(jq -r '.name // empty' <<< "$release")"
|
||||||
|
url="$(jq -r '.url // empty' <<< "$release")"
|
||||||
|
published="$(jq -r '.published // "unknown"' <<< "$release")"
|
||||||
|
[[ -n "$name" ]] || name="$tag"
|
||||||
|
kind=Release
|
||||||
|
if [[ "$(jq -r '.prerelease' <<< "$release")" == true ]]; then kind=Prerelease; fi
|
||||||
|
printf 'GitHub: New %s\nRepository: %s\nVersion: %s\nName: %s\nPublished: %s\nURL: %s' \
|
||||||
|
"$kind" "$repo" "$tag" "$name" "$published" "$url"
|
||||||
|
}
|
||||||
|
process_repo() {
|
||||||
|
local repo="$1" releases ids previous new_releases count release id tag message
|
||||||
|
log INFO "Checking $repo"
|
||||||
|
releases="$(fetch_releases "$repo")" || { log ERROR "GitHub API failed for $repo"; return 1; }
|
||||||
|
ids="$(jq -c '[.[].id] | unique' <<< "$releases")"
|
||||||
|
count="$(jq 'length' <<< "$releases")"
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
previous="$(jq -c --arg repo "$repo" '.[$repo] // null' "$STATE_FILE")"
|
||||||
|
else
|
||||||
|
previous=null
|
||||||
|
fi
|
||||||
|
if [[ "$INIT" == true || "$previous" == null ]]; then
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log INFO "[DRY-RUN] Baseline: $repo ($count releases)"
|
||||||
|
jq -r 'reverse[] | " " + (.tag // "unknown")' <<< "$releases"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
write_state "$repo" "$ids" replace || return 1
|
||||||
|
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
|
||||||
|
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"
|
||||||
|
while IFS= read -r release; do
|
||||||
|
[[ -n "$release" ]] || continue
|
||||||
|
id="$(jq -r '.id' <<< "$release")"
|
||||||
|
tag="$(jq -r '.tag' <<< "$release")"
|
||||||
|
message="$(format_message "$repo" "$release")"
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log INFO "[DRY-RUN] $repo $tag"
|
||||||
|
printf '%s\n\n' "$message"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! send_telegram "$message"; then log ERROR "Telegram send failed: $repo $tag"; return 1; fi
|
||||||
|
write_state "$repo" "[\"$id\"]" append || return 1
|
||||||
|
log INFO "Notification sent: $repo $tag"
|
||||||
|
done < <(jq -c '.[]' <<< "$new_releases")
|
||||||
|
}
|
||||||
|
main() {
|
||||||
|
local url repo failures=0
|
||||||
|
[[ -f "$REPOS_FILE" ]] || die "Missing repos file: $REPOS_FILE"
|
||||||
|
jq -e '.repositories | type == "array" and length > 0' "$REPOS_FILE" >/dev/null || die 'Invalid repositories configuration'
|
||||||
|
jq -e '.repositories | all(.[]; type == "string")' "$REPOS_FILE" >/dev/null || die 'Repository URLs must be strings'
|
||||||
|
mkdir -p "$(dirname "$STATE_FILE")"
|
||||||
|
exec 9>"${STATE_FILE}.lock"
|
||||||
|
flock -n 9 || die 'Another instance is running'
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
jq -e 'type == "object" and all(.[]; type == "array" and all(.[]; type == "string"))' "$STATE_FILE" >/dev/null || die 'Invalid state file'
|
||||||
|
elif [[ "$DRY_RUN" == false ]]; then
|
||||||
|
printf '{}\n' > "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
while IFS= read -r url; do
|
||||||
|
[[ -n "$url" ]] || continue
|
||||||
|
if [[ ! "$url" =~ ^https://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)/?$ ]]; then
|
||||||
|
log ERROR "Invalid GitHub URL: $url"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
repo="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
|
||||||
|
repo="${repo%.git}"
|
||||||
|
if ! process_repo "$repo"; then failures=$((failures + 1)); fi
|
||||||
|
done < <(jq -r '.repositories[]' "$REPOS_FILE" | sort -u)
|
||||||
|
if (( failures > 0 )); then log ERROR "Failed repositories: $failures"; return 1; fi
|
||||||
|
log INFO 'Monitoring completed'
|
||||||
|
}
|
||||||
|
if [[ "$TEST_TELEGRAM" == true ]]; then test_telegram; else main; fi
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"repositories": [
|
||||||
|
"https://github.com/LycheeOrg/Lychee",
|
||||||
|
"https://github.com/firefly-iii/firefly-iii",
|
||||||
|
"https://github.com/usememos/memos"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user