Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/staticcheck.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
echo "Go files are not formatted. Run 'gofmt -s -w .' to fix."
exit 1
fi
# Running tests
# tests
- name: Run tests
run: |
cd api
Expand All @@ -28,11 +28,17 @@ jobs:
cd ../storage/
go test -v ./... --count=1
cd ..
# Spelling check
# Spelling
- name: Check spelling
run: |
curl -L -o ./install-misspell.sh https://git.io/misspell
sh ./install-misspell.sh
mv ./bin/misspell .
chmod +x ./misspell
./misspell -source=auto -error .
# Global api description is bundled
- name: check api bundle
run: |
npm install -g @redocly/cli
redocly bundle docs/api.yaml -o docs/bundled.generated.yaml
git diff --exit-code docs/bundled.generated.yaml docs/bundled.yaml
160 changes: 60 additions & 100 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@

A high-performance backend service that generates dynamic banners displaying GitHub user statistics. Perfect for enhancing your GitHub profile README with real-time stats.

<p align="center">
<img width="350" src="https://api.bnrs.dev/banners/elastic-dark?a=sgt0m9" />
<img width="350" src="https://api.bnrs.dev/banners/torvalds-default?a=ie3e7q" />
</p>

## Overview

GitHub Banners fetches user data from the GitHub API, calculates aggregated statistics (repositories, stars, forks, languages), and renders beautiful SVG banners that automatically update.
Expand Down Expand Up @@ -48,8 +53,7 @@ GitHub Banners fetches user data from the GitHub API, calculates aggregated stat
---

## Architecture

<img width="1679" height="856" alt="image" src="https://github.com/user-attachments/assets/dfb60c1c-c3f2-4ff2-bacb-f51008c8e9bd" />
![](https://github.com/user-attachments/assets/41a7b574-3151-4fb8-9cd1-57caa630b88c)

### Github Stats Caching Strategy

Expand All @@ -62,11 +66,11 @@ GitHub Banners fetches user data from the GitHub API, calculates aggregated stat

### Database Schema

| Table | Description |
| -------------- | --------------------------------------- |
| `banners` | Banner configurations and storage paths |
| `users` | GitHub user profile data |
| `repositories` | Repository data linked to users |
| Table | Description |
| -------------------------- | --------------------------------------- |
| `banners` | Banner configurations and storage paths |
| `github_data.users` | GitHub user profile data |
| `github_data.repositories` | Repository data linked to users |

---

Expand All @@ -89,127 +93,83 @@ cd github-banners

**2. Configure environment variables**

Root `.env`:
> Use `.env.example` that lay in every folder

```env
API_INTERNAL_SERVER_PORT=80
# Service authentication
SERVICES_SECRET_KEY=your_secret_key
```

`api/.env`:
- Root `.env`
- `api/.env`
- `renderer/.env`
- `storage/.env`

```env
# CORS
CORS_ORIGINS=example.com,www.example.com
**3. Start services**

# GitHub tokens (comma-separated for load balancing)
GITHUB_TOKENS=ghp_token1,ghp_token2
### Dev mode, 80 port without https

# Rate limiting & Cache
RATE_LIMIT_RPS=10
CACHE_TTL=5m
REQUEST_TIMEOUT=10s
```bash
docker compose up --build
# Detached mode ( only build logs )
docker compose up --build -d
```

# Logging
LOG_LEVEL=DEBUG
LOG_FORMAT=json
#### Production mode, 443 port ( for cloudflare only )

# PostgreSQL
POSTGRES_USER=github_banners
POSTGRES_PASSWORD=your_secure_password
POSTGRES_DB=github_banners
DB_HOST=api-psgr
PGPORT=5432
```
Cloudflare configuration:

`renderer/.env`:
![telegram-cloud-photo-size-4-5915536458141862905-y](https://github.com/user-attachments/assets/999e1068-c555-4242-b78f-cc11d65f6de3)

```env
# DEBUG/INFO/WARN/ERROR
LOG_LEVEL=INFO
# text/json
LOG_FORMAT=json
# separated by comma list of broker instances
KAFKA_BROKERS_ADDRS=kafka:9092
```

**3. Start services**
Where to get cert and key:

![telegram-cloud-photo-size-4-5915536458141862904-m](https://github.com/user-attachments/assets/abd524e5-4cfa-468c-8e3c-4537b9d45d94)

```bash
docker-compose up --build
# Detached mode ( only build logs )
docker-compose up --build -d
```

Put certificate `cert.pem` and private key `key.pem` to `/etc/nginx/ssl/`

### Development

```bash
# Run locally
cd api && go run main.go
> For testing consider using "dev" version of docker compose

```bash
# Run tests
./run_tests.sh

# CI static check:
# fix formatting
gofmt -s -w .
# tests check
./run_tests.sh
# spelling ( go install github.com/client9/misspell/cmd/misspell@latest )
# it will automatically fix all issues
misspell -source=auto -w .
# global api bundle ( only if you touched api.yaml files )
# It will combine description of global api in `/docs/api.yaml`
# into `bundled.yaml`
# for install https://redocly.com/docs/cli/installation
redocly bundle docs/api.yaml -o docs/bundled.yaml
```

---

## Services

| Service | Port | Description |
| ---------- | ---------------- | ------------------------ |
| `api` | 80 ( public ) | Main API service |
| `api-psgr` | 5432 ( private ) | PostgreSQL database |
| `renderer` | - | Banner rendering service |
| `storage` | - | Banner storage service |
| `kafka` | 9092 ( private ) | Apache Kafka broker |
| Service | Port | Description |
| ---------- | ----------------- | ---------------------------- |
| `nginx` | 80/443 ( public ) | API gateway + static banners |
| `api` | 80 | Main API service |
| `api-psgr` | 5432 | PostgreSQL database |
| `renderer` | 80 | Banner rendering service |
| `storage` | 80 | Banner storage service |
| `kafka` | 9092 | Apache Kafka broker |

---

## API Endpoints

| Method | Endpoint | Description |
| ------ | ------------------------------------- | -------------------------------------------------------------- |
| `GET` | `[api-service]/banners/preview` | Get banner preview for a GitHub user ( not fully implemented ) |
| `POST` | `[api-service]/banners` | Create a new banner ( not implemented ) |
| `GET` | `[storage-service]/{banner-url-path}` | Get long term banner ( not implemented ) |

---

## Project Structure

```
github-banners/
├── api/ # Main API service
│ ├── internal/
│ │ ├── app/user_stats/ # Background stats updating worker
│ │ ├── cache/ # In-memory cache
│ │ ├── config/ # Configuration
│ │ ├── domain/ # Business logic
│ │ │ ├── preview/ # Banner preview use case
│ │ │ └── user_stats/ # Statistics service
│ │ ├── handlers/ # HTTP handlers
│ │ ├── infrastructure/ # External integrations
│ │ │ ├── db/ # Database connection
│ │ │ ├── github/ # GitHub API client pool
│ │ │ ├── kafka/ # Kafka producer
│ │ │ ├── renderer/ # Renderer client
│ │ │ └── server/ # HTTP server
│ │ ├── migrations/ # SQL migrations
│ │ └── repo/ # Storages
│ │ ├── banners/ # long term banners storage
│ │ ├── github_user_data/ # github data
│ └── main.go
├── renderer/ # Banner rendering service ( partially implemented )
│ ├── internal/
│ │ ├── infrastructure/ # External integrations
│ │ │ ├── kafka/ # kafka consumer group logic
│ │ ├── handlers/ # Evevnts handling and HTTP requests handling logic
│ └── main.go
├── storage/ # Banner storage service ( not implemented )
├── docker-compose.yaml
└── run_tests.sh
```
| Method | Endpoint | Description |
| ------ | -------------------- | ---------------------------------------------------------------- |
| `GET` | `/banners/preview` | Get banner preview for a GitHub user |
| `POST` | `/banners` | Create a new lont-term banner |
| `GET` | `/{banner-url-path}` | Get long term banner ( constantly updating since you created it) |

---

Expand Down
3 changes: 2 additions & 1 deletion api/internal/domain/user_stats/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ func (s *UserStatsService) GetStats(ctx context.Context, username string) (domai

// state >10mins but <24 hours
go func() {
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
// usage of context.Background(), cause this operation is idependent of parent context
timeoutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, _ = s.RecalculateAndSync(timeoutCtx, username)
}()
Expand Down
39 changes: 39 additions & 0 deletions docs/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
openapi: 3.0.3
info:
title: Github banners
version: 1.0.0
paths:
/banners/preview:
$ref: '../api/docs/api.yaml#/paths/~1banners~1preview'
/banners:
$ref: '../api/docs/api.yaml#/paths/~1banners'
/banners/{filename}:
get:
summary: Get stored banner (SVG)
description: |
Returns a previously created banner from storage (served by nginx).

If banner is not found, default banner may be returned.
operationId: getStoredBanner
parameters:
- name: filename
in: path
required: true
description: Banner file name without extension
schema:
type: string
example: torvalds-dark
responses:
'200':
description: SVG banner
content:
image/svg+xml:
schema:
type: string
format: binary
components:
schemas:
ErrorResponse:
$ref: '../api/docs/api.yaml#/components/schemas/ErrorResponse'
CreateBannerRequest:
$ref: '../api/docs/api.yaml#/components/schemas/CreateBannerRequest'
Loading
Loading