From 3d57f9a537b715867ab119d31823f57376669c97 Mon Sep 17 00:00:00 2001 From: uno/hurtki Date: Tue, 24 Mar 2026 21:56:35 +0200 Subject: [PATCH 1/5] add: REST API description for renderer ans storage ms --- renderer/docs/api.yaml | 141 +++++++++++++++++++++++++++++++++++++++++ storage/docs/api.yaml | 78 +++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 renderer/docs/api.yaml create mode 100644 storage/docs/api.yaml diff --git a/renderer/docs/api.yaml b/renderer/docs/api.yaml new file mode 100644 index 0000000..20b1ed7 --- /dev/null +++ b/renderer/docs/api.yaml @@ -0,0 +1,141 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: GitHub Banners Renderer + description: Renderer service for rendering banners using ready data +paths: + /preview: + post: + summary: Render banner using ready stats + description: | + Gets statistics for banner render and returns ready SVG image. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BannerInfoV1' + example: + username: "hurtki" + banner_type: "dark" + fetched_at: "2024-01-15T12:00:00Z" + stats: + total_repos: 42 + original_repos: 30 + forked_repos: 12 + total_stars: 150 + total_forks: 20 + languages: + Go: 18500 + Python: 4200 + TypeScript: 1100 + responses: + '200': + description: Successfully rendered banner + content: + image/svg+xml: + schema: + type: string + format: binary + example: '...' + '400': + description: Invalid request — bad JSON body, unknown banner type, or invalid username + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalid_body: + summary: Malformed JSON + value: + error: "Invalid request body format" + invalid_username: + summary: Invalid username + value: + error: "invalid username" + invalid_banner_type: + summary: Unknown banner type + value: + error: "invalid banner type" + '500': + description: Internal server error during rendering + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: "Internal server error" +components: + schemas: + BannerInfoV1: + type: object + required: + - username + - banner_type + - stats + - fetched_at + properties: + username: + type: string + description: GitHub username + example: "hurtki" + banner_type: + type: string + enum: ['dark', 'default'] + description: Visual theme of the banner + example: "dark" + stats: + $ref: '#/components/schemas/StatsV1' + fetched_at: + type: string + format: date-time + description: Timestamp when the stats were fetched from GitHub + example: "2024-01-15T12:00:00Z" + StatsV1: + type: object + required: + - total_repos + - original_repos + - forked_repos + - total_stars + - total_forks + - languages + properties: + total_repos: + type: integer + description: Total number of repositories + example: 42 + original_repos: + type: integer + description: Number of repositories created by the user (non-forks) + example: 30 + forked_repos: + type: integer + description: Number of forked repositories + example: 12 + total_stars: + type: integer + description: Total stars received across all repositories + example: 150 + total_forks: + type: integer + description: Total forks received across all repositories + example: 20 + languages: + type: object + description: Map of programming language name to total bytes of code + additionalProperties: + type: integer + example: + Go: 18500 + Python: 4200 + TypeScript: 1100 + ErrorResponse: + type: object + required: + - error + properties: + error: + type: string + description: Error message describing what went wrong + example: "Invalid request body format" diff --git a/storage/docs/api.yaml b/storage/docs/api.yaml new file mode 100644 index 0000000..90a727d --- /dev/null +++ b/storage/docs/api.yaml @@ -0,0 +1,78 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: GitHub Banners storage + description: Renderer service for saving banner's data +paths: + /banners: + post: + summary: Upload file into storage + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SaveRequestV1' + examples: + save_banner: + summary: Example banner upload + value: + url_path: "hurtki-dark" + banner_info: "PHN2ZyBoZWlnaHQ9IjEwMCIgd2lkdGg9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8Y2lyY2xlIHI9IjQ1IiBjeD0iNTAiIGN5PSI1MCIgZmlsbD0icmVkIiAvPgo8L3N2Zz4g" + banner_format: "svg" + responses: + '200': + description: saved successfully + '400': + description: invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalid_url_path: + summary: url path in request cannot be used + value: + error: "invalid url path" + invalid_banner_format: + summary: banner format in request is not supported + value: + error: "invalid banner format" + '500': + description: Server Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: "can't save banner" +components: + schemas: + SaveRequestV1: + type: object + required: + - url_path + - banner_info + - banner_format + properties: + url_path: + type: string + description: unified path ( not os path ) / banner identificator + example: "hurtki-dark" + banner_info: + type: string + description: base 64 encoded bytes | banner data + example: PHN2ZyBoZWlnaHQ9IjEwMCIgd2lkdGg9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8Y2lyY2xlIHI9IjQ1IiBjeD0iNTAiIGN5PSI1MCIgZmlsbD0icmVkIiAvPgo8L3N2Zz4g + banner_format: + type: string + description: format of the banner + enum: ["svg"] + ErrorResponse: + type: object + required: + - error + properties: + error: + type: string + description: Error message describing what went wrong + example: "Invalid request body format" From 8697100c235d549ace9668304710fb0ebc074720 Mon Sep 17 00:00:00 2001 From: uno/hurtki Date: Tue, 24 Mar 2026 21:56:55 +0200 Subject: [PATCH 2/5] fix: context for async task --- api/internal/domain/user_stats/service.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/internal/domain/user_stats/service.go b/api/internal/domain/user_stats/service.go index 11e93d1..35a3f76 100644 --- a/api/internal/domain/user_stats/service.go +++ b/api/internal/domain/user_stats/service.go @@ -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) }() From 580542f3fe2a6b9b8df6dc3eb0f20fa23c45842a Mon Sep 17 00:00:00 2001 From: uno/hurtki Date: Tue, 24 Mar 2026 22:47:37 +0200 Subject: [PATCH 3/5] add: bundled global api description refactor: README.md --- .github/workflows/staticcheck.yaml | 10 +- README.md | 153 +++++++++--------------- docs/api.yaml | 39 +++++++ docs/bundled.yaml | 179 +++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 103 deletions(-) create mode 100644 docs/api.yaml create mode 100644 docs/bundled.yaml diff --git a/.github/workflows/staticcheck.yaml b/.github/workflows/staticcheck.yaml index 4de5bb9..39ad4f6 100644 --- a/.github/workflows/staticcheck.yaml +++ b/.github/workflows/staticcheck.yaml @@ -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 @@ -28,7 +28,7 @@ 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 @@ -36,3 +36,9 @@ jobs: 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/openapi.yaml -o docs/openapi.bundle.generated.yaml + git diff --exit-code docs/openapi.bundle.generated.yaml docs/openapi.bundle.yaml diff --git a/README.md b/README.md index 78308a3..e02c9c9 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ GitHub Banners fetches user data from the GitHub API, calculates aggregated stat ## Architecture -image +- updated architecture pic ### Github Stats Caching Strategy @@ -62,11 +62,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 | --- @@ -89,127 +89,78 @@ 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`: - -```env -# CORS -CORS_ORIGINS=example.com,www.example.com +- Root `.env` +- `api/.env` +- `renderer/.env` +- `storage/.env` -# GitHub tokens (comma-separated for load balancing) -GITHUB_TOKENS=ghp_token1,ghp_token2 - -# Rate limiting & Cache -RATE_LIMIT_RPS=10 -CACHE_TTL=5m -REQUEST_TIMEOUT=10s +**3. Start services** -# Logging -LOG_LEVEL=DEBUG -LOG_FORMAT=json +> "dev" mode, 80 port without https -# PostgreSQL -POSTGRES_USER=github_banners -POSTGRES_PASSWORD=your_secure_password -POSTGRES_DB=github_banners -DB_HOST=api-psgr -PGPORT=5432 +```bash +docker compose up --build +# Detached mode ( only build logs ) +docker compose up --build -d ``` -`renderer/.env`: +> "prod" mode, 443 port ( for cloudflare only ) -```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 -``` +Cloudflare configuration: +-pic -**3. Start services** +Where to get cert and key: +-pic -```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) | --- diff --git a/docs/api.yaml b/docs/api.yaml new file mode 100644 index 0000000..9731744 --- /dev/null +++ b/docs/api.yaml @@ -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' diff --git a/docs/bundled.yaml b/docs/bundled.yaml new file mode 100644 index 0000000..d10a197 --- /dev/null +++ b/docs/bundled.yaml @@ -0,0 +1,179 @@ +openapi: 3.0.3 +info: + title: Github banners + version: 1.0.0 +paths: + /banners/preview: + get: + summary: Get banner preview for a GitHub user + description: | + Generates and returns an SVG banner with GitHub user statistics. + + The banner includes: + - Total repositories count + - Original vs forked repositories breakdown + - Total stars received + - Total forks + - Top programming languages used + operationId: getBannerPreview + parameters: + - name: username + in: query + required: true + description: GitHub username + schema: + type: string + example: torvalds + - name: type + in: query + required: true + description: Banner type + schema: + type: string + enum: + - dark + - default + example: dark + responses: + '200': + description: Successfully generated banner + content: + image/svg+xml: + schema: + type: string + format: binary + example: ... + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalid_banner_type: + value: + error: invalid banner type + invalid_inputs: + value: + error: invalid inputs + '404': + description: User not found on GitHub + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + user_doesnt_exist: + value: + error: user doesn't exist + '500': + description: Internal server error or service unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + cant_get_preview: + value: + error: can't get preview + /banners: + post: + summary: Create a persistent banner + description: | + Creates a long-term stored banner for a GitHub user. + + - Generate and store banner in storage service + - Return a relative URL for embedding + - Support automatic refresh of stored banners + operationId: createBanner + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateBannerRequest' + responses: + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalid_json: + value: + error: invalid json + invalid_banner_type: + value: + error: invalid banner type + '404': + description: Requested user doesn't exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + user_doesnt_exist: + value: + error: user doesn't exist + '500': + description: Server can't create banner due to internal issues + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + cant_create_banner: + value: + error: can't create banner + /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: + type: object + required: + - error + properties: + error: + type: string + description: Error message describing what went wrong + CreateBannerRequest: + type: object + required: + - username + - type + properties: + username: + type: string + description: GitHub username + example: torvalds + type: + type: string + enum: + - dark + - default + description: Type of banner to create + example: dark From 9a84dc0b0de1031b7078961cc0df8cdc3585a136 Mon Sep 17 00:00:00 2001 From: Alexey <162747234+hurtki@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:03:26 +0200 Subject: [PATCH 4/5] Update README.md --- README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e02c9c9..8682348 100644 --- a/README.md +++ b/README.md @@ -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. +

+ + +

+ ## 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. @@ -48,8 +53,7 @@ GitHub Banners fetches user data from the GitHub API, calculates aggregated stat --- ## Architecture - -- updated architecture pic +![](https://github.com/user-attachments/assets/41a7b574-3151-4fb8-9cd1-57caa630b88c) ### Github Stats Caching Strategy @@ -91,6 +95,7 @@ cd github-banners > Use `.env.example` that lay in every folder + - Root `.env` - `api/.env` - `renderer/.env` @@ -98,7 +103,7 @@ cd github-banners **3. Start services** -> "dev" mode, 80 port without https +### Dev mode, 80 port without https ```bash docker compose up --build @@ -106,13 +111,17 @@ docker compose up --build docker compose up --build -d ``` -> "prod" mode, 443 port ( for cloudflare only ) +#### Production mode, 443 port ( for cloudflare only ) Cloudflare configuration: --pic + +![telegram-cloud-photo-size-4-5915536458141862905-y](https://github.com/user-attachments/assets/999e1068-c555-4242-b78f-cc11d65f6de3) + Where to get cert and key: --pic + +![telegram-cloud-photo-size-4-5915536458141862904-m](https://github.com/user-attachments/assets/abd524e5-4cfa-468c-8e3c-4537b9d45d94) + Put certificate `cert.pem` and private key `key.pem` to `/etc/nginx/ssl/` From 772bc49449efe8196f78989fa8775fee2bce21e7 Mon Sep 17 00:00:00 2001 From: uno/hurtki Date: Tue, 24 Mar 2026 23:06:44 +0200 Subject: [PATCH 5/5] fix: paths in CI api bundle check --- .github/workflows/staticcheck.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/staticcheck.yaml b/.github/workflows/staticcheck.yaml index 39ad4f6..dab1ed0 100644 --- a/.github/workflows/staticcheck.yaml +++ b/.github/workflows/staticcheck.yaml @@ -40,5 +40,5 @@ jobs: - name: check api bundle run: | npm install -g @redocly/cli - redocly bundle docs/openapi.yaml -o docs/openapi.bundle.generated.yaml - git diff --exit-code docs/openapi.bundle.generated.yaml docs/openapi.bundle.yaml + redocly bundle docs/api.yaml -o docs/bundled.generated.yaml + git diff --exit-code docs/bundled.generated.yaml docs/bundled.yaml