A lightweight HTTP service (written in Rust) that fetches and caches the latest matching GitHub Release for multiple repositories. It implements a stale‑while‑revalidate (SWR) caching strategy, flexible filtering (substring / regex / tag regex / latest), optional inclusion of prereleases & drafts, rate‑limit protection, ETag conditional requests, retry with exponential backoff, simple API key authentication, and Prometheus metrics.
Motivation: Client applications (especially anonymous ones) easily hit GitHub's unauthenticated rate limits when polling for updates. Centralizing update lookups through a small cache layer greatly reduces upstream traffic while keeping clients fast & reliable.
latest, substring, regex, tagregex (force tag only)X-Api-Key header (optional)$VAR / ${VAR})cargo build --release
(or cargo run during development)
Copy the example configuration:
cp config.example.toml config.toml
Export any referenced environment variables (e.g. GitHub token):
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxx
(Optional) Set an API key:
# In [server]
api_key = "change_me_secret"
./target/release/update-checker
Or point to a custom config path:
APP_CONFIG=/path/to/my.toml ./target/release/update-checker
curl -H 'X-Api-Key: change_me_secret' http://localhost:8080/v1/repos
curl -H 'X-Api-Key: change_me_secret' http://localhost:8080/v1/updates/example-latest/latest
curl http://localhost:8080/healthz
See config.example.toml (Chinese annotated). Key sections:
| Section | Field | Description |
|---|---|---|
| server | bind / port | Listen address & port |
| server | github_token | Personal token to raise GitHub rate limits (env placeholder supported) |
| server | api_key | Static API key; if set, protected endpoints require X-Api-Key |
| cache | default_ttl | Fresh lifetime (e.g. 30m, 1h, 45s) |
| cache | default_stale_while_revalidate | Additional stale window enabling SWR |
| cache | max_concurrent_refresh | Semaphore limit for simultaneous upstream fetches |
| repos[] | name | Local alias exposed via API |
| repos[] | repo | owner/project |
| repos[] | include_prerelease | Allow prerelease selection |
| repos[] | include_draft | Allow draft selection |
| repos[].filter | type | latest / substring / regex / tagregex |
| repos[].filter | pattern | Required for non-latest types |
| repos[].filter | match_target | tag or title (ignored for tagregex) |
| repos[].cache | ttl | Override default TTL |
| repos[].cache | stale_while_revalidate | Override default SWR window |
Environment variable expansion errors are fatal (missing variables referenced in the file).
latest: first acceptable release from GitHub's chronological list (after applying prerelease/draft flags)substring: pattern must be a substring of the selected match_targetregex: regex applied to chosen match_target (tag or title)tagregex: regex forced on tag regardless of match_targetTimeline for one repo:
stale_while_revalidate → return stale response (cache.stale=true) and trigger async refresh (once).stale=true & upstream_status=error.upstream_status=rate_limited.Protected endpoints (require X-Api-Key if configured): /v1/repos, /v1/updates/{name}/latest, /v1/status, /v1/metrics.
| Method | Path | Description |
|---|---|---|
| GET | /healthz | Liveness probe |
| GET | /v1/repos | List configured repositories (alias + owner/project) |
| GET | /v1/updates/{name}/latest | Latest matching release with cache metadata |
| GET | /v1/status | Per‑repo operational status (last fetch, counts, cache state) |
| GET | /v1/metrics | Prometheus metrics text exposition |
{ "name": "alias", "repo": "owner/project", "version": "v1.2.3", // same as tag "tag": "v1.2.3", "title": "v1.2.3", "published_at": "RFC3339", "body_markdown": "...", "body_excerpt": "...", // first line truncated to 120 chars "url": "https://github.com/...", "assets": [ {"name": "bin", "size": 123, "download_url": "...", "content_type": "...", "updated_at": "..."} ], "cache": { "fetched_at": "...", "expires_at": "...", "swr_expiry": "...", "stale": false }, "upstream_status": "ok | rate_limited | error" }
| HTTP | body.error | Meaning |
|---|---|---|
| 401 | unauthorized | Missing / wrong API key |
| 404 | unknown_repo | Alias not configured |
| 404 | no_matching_release | Repo exists but nothing matched filters |
| 502 | upstream_not_found | Upstream reported 404 (repo or releases) |
| 503 | upstream_unavailable | Upstream unavailable & no cache fallback |
When cache exists but refresh failed: 200 with upstream_status=error and cache.stale=true.
Collected per repository:
update_checker_cache_hits_total{repo}update_checker_cache_stale_returns_total{repo}update_checker_fetch_total{repo,result="ok|error"}update_checker_last_fetch_duration_ms{repo}update_checker_last_fetch_timestamp_seconds{repo}update_checker_fetch_duration_seconds_bucket{repo,le="0.1|0.3|1|3|10|+Inf"} + ..._countUse these to derive success ratios, error rates, and latency percentiles (approx via buckets).
If X-RateLimit-Remaining from GitHub <= internal threshold (currently hardcoded 2), the service stores reset time & switches that repo into a "cache only" mode until reset. Requests still succeed using cached content, marked with upstream_status=rate_limited. If no cache exists, a 503 upstream_unavailable is returned.
Uses tracing + tracing-subscriber. By default emits plain text (JSON mode can be enabled in future). Typical fields: repo, event=fetch, result=ok|error, ms, etag_hit / etag_miss.
Set RUST_LOG for filtering, e.g.:
RUST_LOG=info,update-checker=debug ./target/release/update-checker
DashMap for concurrent in-memory caches & metrics/v1/statusSee todo.md for implementation status (Chinese) and prd.md for the original product requirements document.
Run tests (add when available):
cargo test
Format & lint:
cargo fmt --all
cargo clippy --all-targets -- -D warnings
Contributions welcome!
feat/xyz)Please update README.md / api.md for externally visible changes.
Do not commit real GitHub tokens. If you discover a security issue (e.g., authentication bypass or data confusion), please open a private issue or contact the maintainer directly before public disclosure.
(Choose a license, e.g. MIT / Apache-2.0. Add the license file and update this section.)
Q: Why not just query GitHub directly from clients?
Anonymous rate limits are low; consolidating reduces quota pressure and adds resilience when GitHub is briefly unavailable.
Q: Does it cache assets?
No, only metadata & asset URLs.
Q: Can I force a refresh?
Currently no explicit endpoint; forcing happens automatically once TTL + SWR expire, or synchronously if fully expired.
Q: Draft releases not appearing?
Set include_draft = true for that repo (ensure the token has access if the repo is private – private repos not yet supported if token scope insufficient).
Early stage (v0.1.0). Interfaces may evolve; pin versions when packaging.
Happy caching! 🚀