README: Written by AI (hate doing this).
Code: Mostly written by hand! I'm using this project to learn Golang,
so the code is probably a bit messy nd definitely not as "perfect" as what an AI would spit out.
Want to contribute? Awesome! But please follow these two rules:
1. No AI-generated PRs: Please write your code by hand.
2. Explain your changes: Clarify exactly what your code does. I need to understand it to learn!
GitHub release aggregator. Polls GitHub Atom feeds, filters the content and publishes a single combined Atom feed for RSS readers (e.g. FreshRSS). Optionally sends Apprise notifications when a new release matches alert keywords.
| Endpoint | Description |
|---|---|
GET /feed |
Combined Atom feed with all releases |
GET /release/{name}/{title} |
Minimal HTML page for one release (e.g. /release/Hyprland/v0.55.4) — feed entry links point here so reader-mode scraping gets the filtered content instead of github.com clutter; includes a "View on GitHub" link, follows the system light/dark theme |
GET /status |
JSON status per feed: last successful fetch, entry count, last error, consecutive failures |
GET /image/{file} |
Serves images from config/image/ |
GET / |
Health check — returns ok |
releasefeed/
├── config/
│ ├── config.yaml # configuration
│ ├── state.json # notification state (auto-created on first run)
│ └── image/ # per-feed images (webp, png, svg)
├── internal/
├── Dockerfile
├── docker-compose.yml
└── main.go
| Field | Description |
|---|---|
port |
HTTP port (default 880) |
base_url |
Public server address — used to build image URLs |
| Field | Description |
|---|---|
url |
Apprise API URL |
tags |
Default notification tags |
| Field | Description |
|---|---|
fetch_interval |
How often to poll feeds (e.g. 30m, 1h, 6h) |
max_entries |
Cap on releases shown per feed in /feed (default 10) — each poll replaces the feed's entries with the N newest, older ones drop off |
alert_keywords |
Keywords that trigger a notification |
exclude_sections |
Sections stripped from every feed |
ignore_titles |
Case-insensitive regexes — matching releases are skipped entirely (e.g. beta, -rc) |
| Field | Required | Description |
|---|---|---|
name |
yes | Display name used in the feed |
url |
yes | GitHub Atom feed URL (/releases.atom) |
fetch_interval |
no | Overrides defaults.fetch_interval |
max_entries |
no | Overrides defaults.max_entries |
image |
no | File name from config/image/ |
exclude_sections |
no | Extra sections to strip — merged with defaults.exclude_sections |
regex_remove |
no | Regular expressions — matching HTML fragments are removed |
alert_keywords |
no | Merged with defaults.alert_keywords |
ignore_titles |
no | Merged with defaults.ignore_titles |
notify_all |
no | true = notify on every new release, not only on keyword match |
apprise_tags |
no | Overrides global apprise.tags |
alert_keywords and exclude_sections are always merged — per-feed values add to the defaults instead of replacing them. Duplicates are removed.
defaults:
alert_keywords:
- "breaking"
- "security"
exclude_sections:
- "sponsors"
- "donators"
feeds:
- name: "Hyprland"
alert_keywords:
- "critical" # effective: breaking, security, critical
exclude_sections:
- "one-time donators" # effective: sponsors, donators, one-time donatorsSections are detected from <h1>–<h6> tags and bold-paragraph pseudo-headings (<p><strong>Sponsors</strong></p>) in the HTML GitHub ships. Matching is case-insensitive and partial — "sponsors" also removes "Diamond Sponsors".
Removing a section also removes its subsections: cutting an <h2> drops everything up to the next heading of the same or higher level, including any <h3>/<h4> inside.
Text before the first section (the preamble) is always kept.
GitHub ships release-note images as private-user-images.githubusercontent.com URLs signed with a JWT that expires after a few minutes, so they always show up broken in RSS readers. releasefeed rewrites them to the stable github.com/user-attachments/assets/... form, which GitHub redirects to a freshly signed URL on every request.
Editing a release on GitHub bumps its <updated> timestamp, which would resurface old releases as new in RSS readers. releasefeed pins the timestamp it first saw for each release, so edits upstream don't reorder the feed.
A notification is sent when a release is new (not present in state.json) and its title or content contains one of the alert_keywords, or the feed has notify_all: true.
Notification content: [FeedName] version title plus a link to the release.
On a fresh (empty) state.json the first poll only records existing releases without notifying, so a redeploy never floods you with alerts about old releases.
When a feed fails to fetch 5 times in a row, a single Apprise notification is sent so a silently broken feed doesn't go unnoticed. The failure counter resets on the next successful fetch and is visible under /status.
docker compose up -d
docker logs releasefeed -f