Skip to content

Commit 9f4cc5d

Browse files
committed
Merge develop into main for release v0.7.8
2 parents 4b7bf11 + 865e470 commit 9f4cc5d

16 files changed

Lines changed: 171 additions & 51 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ GGHSTATS_ENABLED_LOCALES=en,es,de,fr,pt-br
6666
# GGHSTATS_ENABLED_LOCALES=en,es,de
6767

6868
# --- Rate limiting (enabled by default) ---
69+
# Exempt: /metrics, /api/v1/healthz, /api/v1/badge/* (README embeds).
6970
# GGHSTATS_RATE_LIMIT_ENABLED=true
7071
# GGHSTATS_RATE_LIMIT_REQUESTS=120
7172
# GGHSTATS_RATE_LIMIT_PERIOD=1m
7273
# GGHSTATS_RATE_LIMIT_BURST=20
7374

7475
# Optional (0.7.6+): IP whitelist — restrict access to specific IPs/CIDRs
76+
# /api/v1/badge/* stays public for README badges even when /api/ is listed.
7577
# GGHSTATS_WHITELIST=10.0.0.0/8,172.16.0.0/12
7678
# GGHSTATS_WHITELIST_PATHS=/api/,/h2h
7779

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project are documented in this file.
55
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66
This project adheres to [Semantic Versioning](https://semver.org/).
77

8+
## [Unreleased]
9+
10+
## [0.7.8] - 2026-06-15
11+
12+
### Fixed
13+
14+
- **README badge embeds:** exempt `/api/v1/badge/*` from per-IP rate limiting and IP whitelist (with `/metrics` and `/api/v1/healthz`). Fixes broken `![…](badge)` images when `GGHSTATS_WHITELIST_PATHS` includes `/api/` or GitHub/camo proxies share one client IP bucket.
15+
16+
### Changed
17+
18+
- **Docs:** sync README version badge and install examples to **0.7.8**; note badge routes in middleware section; man page and env examples.
19+
- **Docker release image:** Alpine **3.24** in `Dockerfile.release` (aligned with local `Dockerfile`).
20+
821
## [0.7.7] - 2026-06-14
922

1023
### Added
@@ -304,7 +317,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).
304317
- Project naming and module path finalized as `gghstats` (binary, Docker image, `GGHSTATS_*` environment variables).
305318
- Toolchain and build base image aligned to Go **1.26.1**.
306319

307-
[Unreleased]: https://github.com/hrodrig/gghstats/compare/v0.7.4...HEAD
320+
[Unreleased]: https://github.com/hrodrig/gghstats/compare/v0.7.8...HEAD
321+
[0.7.8]: https://github.com/hrodrig/gghstats/compare/v0.7.7...v0.7.8
322+
[0.7.7]: https://github.com/hrodrig/gghstats/compare/v0.7.6...v0.7.7
323+
[0.7.6]: https://github.com/hrodrig/gghstats/compare/v0.7.5...v0.7.6
324+
[0.7.5]: https://github.com/hrodrig/gghstats/compare/v0.7.4...v0.7.5
308325
[0.7.4]: https://github.com/hrodrig/gghstats/compare/v0.7.3...v0.7.4
309326
[0.7.3]: https://github.com/hrodrig/gghstats/compare/v0.7.2...v0.7.3
310327
[0.7.2]: https://github.com/hrodrig/gghstats/compare/v0.7.1...v0.7.2

Dockerfile.release

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# COPY the whole linux/ tree then pick the right binary for single-platform (snapshot) and
33
# multi-platform (release) builds. Local multi-stage builds use Dockerfile (make docker-build).
44
ARG TARGETPLATFORM
5-
FROM alpine:3.22
5+
FROM alpine:3.24
66
LABEL org.opencontainers.image.title="gghstats"
77
LABEL org.opencontainers.image.description="GitHub traffic dashboard and CLI"
88
LABEL org.opencontainers.image.source="https://github.com/hrodrig/gghstats"

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
![gghstats — self-hosted GitHub traffic beyond the 14-day window](assets/gghstats-poster-devto.png)
44

5-
[![Version](https://img.shields.io/badge/version-0.7.4-blue)](https://github.com/hrodrig/gghstats/releases)
5+
[![Version](https://img.shields.io/badge/version-0.7.8-blue)](https://github.com/hrodrig/gghstats/releases)
66
[![Release](https://img.shields.io/github/v/release/hrodrig/gghstats)](https://github.com/hrodrig/gghstats/releases)
77
[![CI](https://github.com/hrodrig/gghstats/actions/workflows/ci.yml/badge.svg)](https://github.com/hrodrig/gghstats/actions)
88
[![codecov](https://codecov.io/gh/hrodrig/gghstats/graph/badge.svg)](https://codecov.io/gh/hrodrig/gghstats)
@@ -123,7 +123,7 @@ export GGHSTATS_GITHUB_TOKEN=ghp_xxx
123123
gghstats run --open
124124
```
125125

126-
Or extract a [Release](https://github.com/hrodrig/gghstats/releases) tarball, or `go install github.com/hrodrig/gghstats/cmd/gghstats@latest`. Pin a version: `VERSION=v0.7.4 curl -fsSL …/install.sh | sh`.
126+
Or extract a [Release](https://github.com/hrodrig/gghstats/releases) tarball, or `go install github.com/hrodrig/gghstats/cmd/gghstats@latest`. Pin a version: `VERSION=v0.7.8 curl -fsSL …/install.sh | sh`.
127127

128128
Open <http://localhost:8080> if you did not use **`--open`**. Data is stored in `./data/gghstats.db` (override with `GGHSTATS_DB`). A first sync may take a while if the default filter includes many repositories — narrow `GGHSTATS_FILTER` in [Configuration](#configuration) when you move beyond this smoke test.
129129

@@ -167,7 +167,7 @@ Open <http://localhost:8080>. The template [`.env.example`](.env.example) docume
167167
curl -fsSL https://raw.githubusercontent.com/hrodrig/gghstats/main/scripts/install.sh | sh
168168
```
169169

170-
Downloads the latest [release](https://github.com/hrodrig/gghstats/releases) archive for your OS/arch into `/usr/local/bin` (override with `BINDIR=~/bin`). Review [`scripts/install.sh`](scripts/install.sh) before piping to `sh`; pin with `VERSION=v0.7.4`.
170+
Downloads the latest [release](https://github.com/hrodrig/gghstats/releases) archive for your OS/arch into `/usr/local/bin` (override with `BINDIR=~/bin`). Review [`scripts/install.sh`](scripts/install.sh) before piping to `sh`; pin with `VERSION=v0.7.8`.
171171

172172
**From source (recommended for developers):**
173173

@@ -182,15 +182,15 @@ This installs the binary to `$GOBIN` (default `$HOME/go/bin`). Ensure `$GOBIN` i
182182
| Platform | Command / path |
183183
|----------|----------------|
184184
| **Homebrew (macOS / Linux)** | `brew install hrodrig/gghstats/gghstats`[tap](https://github.com/hrodrig/homebrew-gghstats) |
185-
| **Debian / Ubuntu** | `wget -q -O /tmp/gghstats.deb https://github.com/hrodrig/gghstats/releases/download/v0.7.4/gghstats_0.7.4_linux_amd64.deb && sudo dpkg -i /tmp/gghstats.deb` |
186-
| **Fedora / RHEL / AlmaLinux / Rocky / Oracle Linux** | `sudo dnf install https://github.com/hrodrig/gghstats/releases/download/v0.7.4/gghstats_0.7.4_linux_amd64.rpm` |
185+
| **Debian / Ubuntu** | `wget -q -O /tmp/gghstats.deb https://github.com/hrodrig/gghstats/releases/download/v0.7.8/gghstats_0.7.8_linux_amd64.deb && sudo dpkg -i /tmp/gghstats.deb` |
186+
| **Fedora / RHEL / AlmaLinux / Rocky / Oracle Linux** | `sudo dnf install https://github.com/hrodrig/gghstats/releases/download/v0.7.8/gghstats_0.7.8_linux_amd64.rpm` |
187187
| **Linux tarball** | `tar -xzf gghstats_*_linux_*.tar.gz` from [Releases](https://github.com/hrodrig/gghstats/releases); verify `checksums.txt` |
188188
| **FreeBSD** | `gghstats_*_freebsd_*.tar.gz` on [Releases](https://github.com/hrodrig/gghstats/releases); port + developer guide [`contrib/freebsd/README.md`](contrib/freebsd/README.md) (`gmake` in repo, `make` in ports tree) |
189189
| **OpenBSD** | `gghstats_*_openbsd_*.tar.gz` on [Releases](https://github.com/hrodrig/gghstats/releases); [`contrib/openbsd/README.md`](contrib/openbsd/README.md) and port [`contrib/openbsd/port/`](contrib/openbsd/port/) |
190190
| **macOS / Windows archives** | `.tar.gz` / `.zip` on [Releases](https://github.com/hrodrig/gghstats/releases) |
191-
| **OCI image** | `ghcr.io/hrodrig/gghstats:v0.7.4` or `:latest` (multi-arch) |
191+
| **OCI image** | `ghcr.io/hrodrig/gghstats:v0.7.8` or `:latest` (multi-arch) |
192192

193-
Replace `v0.7.4` and `amd64` with your [release](https://github.com/hrodrig/gghstats/releases) version and architecture (e.g. `arm64`).
193+
Replace `v0.7.8` and `amd64` with your [release](https://github.com/hrodrig/gghstats/releases) version and architecture (e.g. `arm64`).
194194

195195
After install, validate the UI locally:
196196

@@ -384,6 +384,8 @@ gghstats applies **per-IP token-bucket rate limiting** to all HTTP routes except
384384

385385
**Middleware order (outermost first):** IP whitelist → rate limiting → request logging → Prometheus HTTP metrics → route handlers.
386386

387+
**Always exempt** (no whitelist check, no rate limit): `/metrics`, `/api/v1/healthz`, and **`/api/v1/badge/*`** (README embeds; GitHub and other proxies must fetch SVG without auth or throttling).
388+
387389
**Defaults:** 120 requests per minute, burst of 20. For most dashboard browsing this is generous; adjust for high-traffic or protected deployments.
388390

389391
Behind a **reverse proxy** (nginx, Traefik, Caddy, haproxy), gghstats reads the client IP from `X-Forwarded-For` (preferred) or `X-Real-IP`. Ensure your proxy sets one of these headers, otherwise all requests appear to come from the proxy itself and share a single rate-limit bucket.
@@ -409,7 +411,7 @@ Inactive IPs are evicted from memory after 5 minutes of idle time.
409411

410412
gghstats supports optional IP-based access control via `GGHSTATS_WHITELIST`. When set, only requests from matching IPs or CIDR ranges are allowed; all others receive `403 Forbidden` with `{"error":"ip_not_whitelisted"}`.
411413

412-
By default the whitelist applies to **all routes** except `/metrics` and `/api/v1/healthz`. Scope it to specific paths with `GGHSTATS_WHITELIST_PATHS` (comma-separated prefixes, e.g. `/api/,/h2h`). Paths not listed remain publicly accessible.
414+
By default the whitelist applies to **all routes** except `/metrics`, `/api/v1/healthz`, and **`/api/v1/badge/*`** (README embeds). Scope it to specific paths with `GGHSTATS_WHITELIST_PATHS` (comma-separated prefixes, e.g. `/api/,/h2h`). Paths not listed remain publicly accessible. Badge URLs stay public even when `/api/` is whitelisted.
413415

414416
```bash
415417
# Internal network + VPN only

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.7.7
1+
0.7.8

contrib/freebsd/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PORTNAME= gghstats
2-
PORTVERSION= 0.7.7
2+
PORTVERSION= 0.7.8
33
DISTVERSIONPREFIX= v
44
CATEGORIES= sysutils
55
# Local test before release: copy the tarball into DISTDIR (see `make -V DISTDIR`

contrib/gghstats.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ GGHSTATS_ENABLED_LOCALES=en,es,de,fr,pt-br
4141
# GGHSTATS_LOG_LEVEL=info
4242

4343
# Rate limiting (enabled by default; set to false to disable)
44+
# Rate limiting (gghstats >= 0.7.5) — exempt: /metrics, /healthz, /api/v1/badge/*
4445
# GGHSTATS_RATE_LIMIT_ENABLED=true
4546
# GGHSTATS_RATE_LIMIT_REQUESTS=120
4647
# GGHSTATS_RATE_LIMIT_PERIOD=1m
4748
# GGHSTATS_RATE_LIMIT_BURST=20
4849

49-
# IP whitelist (gghstats >= 0.7.6) — restrict access to specific IPs/CIDRs
50+
# IP whitelist (gghstats >= 0.7.6) — /api/v1/badge/* stays public for README embeds
5051
# GGHSTATS_WHITELIST=10.0.0.0/8,172.16.0.0/12
5152
# GGHSTATS_WHITELIST_PATHS=/api/,/h2h

contrib/man/man1/gghstats.1

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.\" Man page for gghstats(1)
2-
.TH GGHSTATS 1 "June 2026" "gghstats v0.7.7" "User Commands"
2+
.TH GGHSTATS 1 "June 2026" "gghstats v0.7.8" "User Commands"
33
.SH NAME
44
gghstats \- self-hosted GitHub repository traffic dashboard and CLI
55
.SH SYNOPSIS
@@ -116,12 +116,14 @@ Time window for rate limiting as a Go duration (default: \fB1m\fR).
116116
.TP
117117
.B GGHSTATS_RATE_LIMIT_BURST
118118
Maximum burst of requests before smoothing (default: \fB20\fR).
119+
Exempt paths: \fB/metrics\fR, \fB/api/v1/healthz\fR, \fB/api/v1/badge/*\fR (README embeds).
119120
.TP
120121
.B GGHSTATS_WHITELIST
121122
Comma-separated IPs/CIDRs allowed to access the server (empty = all allowed).
122123
.TP
123124
.B GGHSTATS_WHITELIST_PATHS
124125
Comma-separated path prefixes where the whitelist applies (empty = all routes).
126+
Badge URLs under \fB/api/v1/badge/\fR remain public even when \fB/api/\fR is listed.
125127
.SH EXAMPLES
126128
.TP
127129
Run the dashboard locally:

contrib/openbsd/port/Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
# See README.md and PORT-RELEASE.md.
99

1010
COMMENT = GitHub traffic dashboard and CLI (SQLite)
11-
DISTNAME = gghstats_0.7.7_openbsd_${MACHINE_ARCH:S/aarch64/arm64/}
12-
PKGNAME = gghstats-0.7.7
11+
DISTNAME = gghstats_0.7.8_openbsd_${MACHINE_ARCH:S/aarch64/arm64/}
12+
PKGNAME = gghstats-0.7.8
1313
CATEGORIES = sysutils
1414
HOMEPAGE = https://github.com/hrodrig/gghstats
1515
MAINTAINER = hrodrig@usb.ve
@@ -21,8 +21,8 @@ PERMIT_DISTFILES = Yes
2121
# Local test before release: copy tarball to DISTDIR or use
2222
# make fetch MASTER_SITES=file:///absolute/path/to/dir/
2323
# See README.md "Test with a local tarball".
24-
MASTER_SITES = https://github.com/hrodrig/gghstats/releases/download/v0.7.7/
25-
DISTFILES = gghstats_0.7.7_openbsd_${MACHINE_ARCH:S/aarch64/arm64/}.tar.gz
24+
MASTER_SITES = https://github.com/hrodrig/gghstats/releases/download/v0.7.8/
25+
DISTFILES = gghstats_0.7.8_openbsd_${MACHINE_ARCH:S/aarch64/arm64/}.tar.gz
2626
EXTRACT_SUFX = .tar.gz
2727

2828
ONLY_FOR_ARCHS = amd64 arm64 aarch64

internal/server/middleware_skip.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package server
2+
3+
import "strings"
4+
5+
// BadgePathPrefix is the HTTP path prefix for README embed badges (must stay public).
6+
const BadgePathPrefix = "/api/v1/badge/"
7+
8+
// MiddlewareSkip lists request paths exempt from rate limiting or IP whitelist.
9+
// Exact matches the full path; Prefixes match path prefixes (for badge routes).
10+
type MiddlewareSkip struct {
11+
Exact []string
12+
Prefixes []string
13+
}
14+
15+
// PublicMiddlewareSkip returns paths that must stay reachable without rate limits
16+
// or whitelist checks (metrics scrape, probes, README badge embeds).
17+
func PublicMiddlewareSkip() MiddlewareSkip {
18+
return MiddlewareSkip{
19+
Exact: []string{MetricsPath, HealthzPath},
20+
Prefixes: []string{BadgePathPrefix},
21+
}
22+
}
23+
24+
func (s MiddlewareSkip) matches(path string) bool {
25+
for _, p := range s.Exact {
26+
if path == p {
27+
return true
28+
}
29+
}
30+
for _, p := range s.Prefixes {
31+
if strings.HasPrefix(path, p) {
32+
return true
33+
}
34+
}
35+
return false
36+
}

0 commit comments

Comments
 (0)