feat: complete MVP setup with CI/CD, Makefile, docker, and backlog docs

This commit is contained in:
2026-08-24 21:43:21 -07:00
parent 7a51931386
commit 4197d86626
20 changed files with 590 additions and 24 deletions
+72
View File
@@ -0,0 +1,72 @@
name: CI Quality Checks & Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
backend-test:
name: Go Lint & Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Verify Formatting (gofmt)
run: |
UNFORMATTED=$(gofmt -l internal/ cmd/)
if [ -n "$UNFORMATTED" ]; then
echo "Unformatted files found:"
echo "$UNFORMATTED"
exit 1
fi
- name: Run Go Vet
run: go vet ./...
- name: Run Go Unit Tests
run: go test -v ./...
frontend-build:
name: Frontend Typecheck & Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Install Frontend Dependencies
working-directory: web
run: npm ci || npm install
- name: TypeScript Check & Vite Build
working-directory: web
run: npm run build
security-scan:
name: Gitleaks Secret Detection
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks Scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+49
View File
@@ -0,0 +1,49 @@
name: Build & Publish Release Image
on:
push:
tags:
- 'v*'
jobs:
docker-release:
name: Build & Push Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea / Container Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_HOST || 'git.pengzhan.dev' }}
username: ${{ secrets.REGISTRY_USERNAME || github.actor }}
password: ${{ secrets.REGISTRY_TOKEN || secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.REGISTRY_HOST || 'git.pengzhan.dev' }}/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+16
View File
@@ -0,0 +1,16 @@
title = "gitleaks config"
[extend]
useDefault = true
[allowlist]
description = "Global allowlist"
paths = [
'''sample_subscriptions\.opml''',
'''docs/''',
'''web/node_modules/'''
]
regexes = [
'''admin123456''',
'''example_jwt_secret_key'''
]
+51
View File
@@ -0,0 +1,51 @@
# ==============================================================================
# Stage 1: Build React Frontend PWA
# ==============================================================================
FROM node:20-alpine AS frontend-builder
WORKDIR /app/web
COPY web/package*.json ./
RUN npm ci || npm install
COPY web/ ./
RUN npm run build
# ==============================================================================
# Stage 2: Build Go Backend Binary
# ==============================================================================
FROM golang:1.22-alpine AS backend-builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=frontend-builder /app/web/dist ./web/dist
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o rsspipe cmd/server/main.go
# ==============================================================================
# Stage 3: Minimal Production Runtime
# ==============================================================================
FROM alpine:3.20
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=backend-builder /app/rsspipe /app/rsspipe
COPY --from=frontend-builder /app/web/dist /app/web/dist
RUN mkdir -p /app/data
ENV PORT=8080 \
DB_PATH=/app/data/rsspipe.db \
GIN_MODE=release
VOLUME ["/app/data"]
EXPOSE 8080
ENTRYPOINT ["/app/rsspipe"]
+76
View File
@@ -0,0 +1,76 @@
SHELL := /usr/bin/env bash
.PHONY: all help build build-frontend build-backend dev test test-race lint check clean docker-build docker-up docker-down
# Default target
all: build
## help: Show this help message
help:
@echo "RSSPipe Build & Development Commands"
@echo "====================================="
@sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /'
## build: Build frontend and backend production binary
build: build-frontend build-backend
## build-frontend: Build React PWA frontend bundle (Vite + TypeScript)
build-frontend:
@echo "==> Building frontend bundle..."
@cd web && npm install && npm run build
@echo "✓ Frontend build complete: web/dist"
## build-backend: Build Go backend binary
build-backend:
@echo "==> Compiling Go binary..."
@mkdir -p bin
@CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/rsspipe cmd/server/main.go
@echo "✓ Backend binary ready: bin/rsspipe"
## dev: Run server locally
dev: build-frontend
@echo "==> Starting local RSSPipe server..."
@go run cmd/server/main.go
## test: Run backend Go unit tests
test:
@echo "==> Running Go unit tests..."
@go test -v ./...
## check: Run all quality gate checks (formatting, vet, tests, tsc, gitleaks)
check:
@chmod +x scripts/check.sh
@./scripts/check.sh
## lint: Run gofmt and go vet
lint:
@echo "==> Verifying Go formatting..."
@test -z "$$(gofmt -l internal/ cmd/)" || (echo "Unformatted files:" && gofmt -l internal/ cmd/ && exit 1)
@echo "==> Running Go vet..."
@go vet ./...
@echo "✓ Lint passed."
## format: Format all Go files with gofmt
format:
@echo "==> Formatting Go files..."
@gofmt -w internal/ cmd/
@echo "✓ Formatted."
## docker-build: Build production Docker image
docker-build:
@echo "==> Building Docker image rsspipe:latest..."
@docker build -t rsspipe:latest .
## docker-up: Launch RSSPipe with Docker Compose
docker-up:
@docker compose up -d
## docker-down: Stop Docker Compose services
docker-down:
@docker compose down
## clean: Remove built binaries and temporary artifacts
clean:
@echo "==> Cleaning build artifacts..."
@rm -rf bin/ web/dist/
@echo "✓ Cleaned."
+140
View File
@@ -0,0 +1,140 @@
# RSSPipe 🚀
> **Modern, High-Performance RSS Information Gateway & Distraction-Free Reader**
RSSPipe is a single-user, self-hosted RSS information gateway designed to replace traditional feeds readers with an elegant, ultra-fast, and distraction-free reading experience. Built with a robust Go backend and a modern React PWA interface.
---
## ✨ Key Features
- 🖥️ **Reeder 4 Minimal 3-Pane View**: Designed for desktop power users with collapsible navigation sidebar, dense article list, and distraction-free reading canvas.
- 📱 **Xiaohongshu-Style Masonry Waterfall**: Designed for mobile and media-rich browsing with adaptive masonry columns, category filter chips, and full-screen post reader.
- ⚡ **High-Speed Crawler & Engine**: Concurrent worker pool crawler in Go with per-feed polling intervals, custom timeouts, and HTTP conditional caching (ETag / Last-Modified 304).
- 🔒 **Single-User Security & Auth**: JWT-based session security and bcrypt hashing to secure access over the public internet.
- 📁 **OPML 2.0 Import & Export**: Effortless migration from Miniflux, Feedly, Inoreader, NetNewsWire, or Reeder.
- 🔗 **Standalone Article Sharing**: Instant public HTML snapshot sharing (`/share/{uuid}.html`) for individual articles.
- ⌨️ **Keyboard Navigation**: Complete single-key shortcut navigation (`j`/`k` next/previous, `m` mark read, `s` star, `e` archive, `v` view original, `[` sidebar toggle).
- 💾 **Local State & Reading Persistence**: Restores exact view modes, filter selections, active feeds, and reading positions upon page refresh.
---
## 🛠️ Architecture & Tech Stack
```
┌─────────────────────────────────────────────────────────┐
│ React 18 PWA Frontend │
│ (Vite + TypeScript + Lucide + Modern CSS) │
└──────────────────────────┬──────────────────────────────┘
│ REST API + Static Asset Serving
┌──────────────────────────▼──────────────────────────────┐
│ Go 1.22 HTTP Server │
│ (Chi Router + JWT Auth) │
├──────────────────────────┬──────────────────────────────┤
│ Background Crawler │ SQLite Engine (WAL) │
│ (Worker Pool + Parser) │ (modernc.org/sqlite Driver) │
└──────────────────────────┴──────────────────────────────┘
```
- **Backend**: Go 1.22+, `go-chi/chi`, `gofeed` parser, `golang-jwt/jwt`.
- **Database**: Embedded SQLite with WAL mode (`modernc.org/sqlite` pure Go driver, no CGO required).
- **Frontend**: React 18, TypeScript, Vite, CSS custom properties.
---
## 🚀 Quickstart
### Option 1: Run with Docker Compose
```bash
# Clone the repository
git clone https://git.pengzhan.dev/haopengzhan/rsspipe.git
cd rsspipe
# Start RSSPipe
docker compose up -d
```
Visit **`http://localhost:8080`**.
Default credentials:
- **Username**: `admin`
- **Password**: `admin123456` *(configurable via environment variables)*
---
### Option 2: Local Development Setup
#### Prerequisites
- **Go 1.22+**
- **Node.js 18+ & npm**
```bash
# Clone the repository
git clone https://git.pengzhan.dev/haopengzhan/rsspipe.git
cd rsspipe
# Build and start server using Makefile
make dev
```
Or build separately:
```bash
make build # Compiles frontend bundle + backend binary into bin/rsspipe
./bin/rsspipe # Runs production binary
```
The server binds to `http://localhost:8080` and serves both the API endpoints and static frontend assets.
---
## ⚙️ Configuration & Environment Variables
| Variable | Default | Description |
| :--- | :--- | :--- |
| `PORT` | `8080` | Server HTTP port |
| `DB_PATH` | `./data/rsspipe.db` | Path to SQLite database file |
| `JWT_SECRET` | *(auto-generated)* | 32-byte secret key for signing JWT tokens |
| `ADMIN_USERNAME` | `admin` | Initial default administrator username |
| `ADMIN_PASSWORD` | `admin123456` | Initial default administrator password |
| `CRAWLER_WORKERS` | `10` | Number of concurrent fetch worker threads |
| `CRAWLER_INTERVAL` | `15` | Default background crawler interval (minutes) |
---
## ⌨️ Keyboard Shortcuts
| Shortcut | Action |
| :--- | :--- |
| `j` / `k` | Navigate to Next / Previous article |
| `m` | Toggle Read / Unread status |
| `s` | Toggle Starred status |
| `e` | Mark as read and advance to next |
| `v` | Open original article URL in new tab |
| `[` | Toggle left navigation sidebar |
| `r` | Trigger immediate refresh of feeds |
| `a` | Open "Add Subscription" modal |
| `1` | Switch to Desktop 3-Split View |
| `2` | Switch to Xiaohongshu Waterfall View |
| `3` | Switch to Settings & Management View |
| `Shift + A` | Mark all items in active view as read |
| `?` | Open keyboard shortcuts cheatsheet |
| `Esc` | Close any active modal dialog |
---
## 🧪 Testing & Quality Automation
```bash
make check # Run all quality checks (gofmt, go vet, tests, tsc, gitleaks)
make test # Run backend Go unit tests
make lint # Check formatting and run go vet
make format # Auto-format all Go files
```
---
## 📄 License
MIT License. Free for personal and commercial self-hosted use.
+5 -5
View File
@@ -9,11 +9,11 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/haopengzhan/rsspipe/internal/api" "git.pengzhan.dev/haopengzhan/rsspipe/internal/api"
"github.com/haopengzhan/rsspipe/internal/auth" "git.pengzhan.dev/haopengzhan/rsspipe/internal/auth"
"github.com/haopengzhan/rsspipe/internal/config" "git.pengzhan.dev/haopengzhan/rsspipe/internal/config"
"github.com/haopengzhan/rsspipe/internal/crawler" "git.pengzhan.dev/haopengzhan/rsspipe/internal/crawler"
"github.com/haopengzhan/rsspipe/internal/db" "git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
) )
func main() { func main() {
+25
View File
@@ -0,0 +1,25 @@
version: '3.8'
services:
rsspipe:
build:
context: .
dockerfile: Dockerfile
image: rsspipe:latest
container_name: rsspipe
restart: unless-stopped
ports:
- "8080:8080"
environment:
- PORT=8080
- DB_PATH=/app/data/rsspipe.db
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=admin123456
- CRAWLER_WORKERS=10
- CRAWLER_INTERVAL=15
volumes:
- rsspipe_data:/app/data
volumes:
rsspipe_data:
driver: local
+95
View File
@@ -0,0 +1,95 @@
# RSSPipe Backlog Specification (`003_backlog.md`)
This document records planned features, UX enhancements, and architectural extensions identified during initial testing and validation.
---
## 1. Search Box Auto-Complete & Real-Time Suggestions
- **Goal**: Enable rich auto-complete and search suggestion dropdown as the user types in the global search bar.
- **Scope**:
- Auto-suggest matches across article titles, feed names, author tags, and category labels.
- Keyboard navigation within search dropdown (`ArrowDown`, `ArrowUp`, `Enter` to jump directly to article or feed).
- Search history / recent queries quick access.
---
## 2. Password Management in User Settings
- **Goal**: Allow the single-user administrator to update their login credentials directly from `Settings > Account & System`.
- **Scope**:
- Backend API: `PUT /api/auth/password` verifying `old_password` and updating bcrypt hash for `new_password`.
- Frontend Modal/Card with confirmation field and clear validation indicators.
---
## 3. Comprehensive Keyboard Shortcuts Test Suite
- **Goal**: Automated end-to-end and unit testing for all single-key power-user navigation bindings.
- **Scope**:
- `j` / `k`: Next / previous article navigation.
- `s`: Toggle star.
- `m`: Toggle read status.
- `e`: Archive / mark read and advance.
- `v`: Open original URL in new browser tab.
- `[`: Toggle sidebar collapse/expand.
- `r`: Trigger feed refresh.
- `a`: Open Add Feed modal.
- `1`, `2`, `3`: Switch views (3-Split, Waterfall, Settings).
- `?`: Open keyboard shortcuts cheatsheet modal.
- `Shift + A`: Mark all as read with confirmation.
- Ensure shortcuts are strictly inhibited when focus is inside text inputs, textareas, or modal forms.
---
## 4. Header Brand & Sidebar Toggle Alignment Consistency
- **Goal**: Standardize the top header layout across all view modes (3-Split, Waterfall, and Settings).
- **Scope**:
- In 3-Split view, ensure the RSSPipe logo and brand name occupy the consistent leftmost position matching Waterfall and Settings views.
- Ensure the sidebar toggle icon `[` integrates cleanly into the left group or toolbar without disrupting brand alignment.
---
## 5. Refresh Button Spinning Micro-Animation
- **Goal**: Provide instant visual feedback when a background refresh or manual pull is in progress.
- **Scope**:
- Add smooth CSS keyframe spin animation (`@keyframes spin { 100% { transform: rotate(360deg); } }`) to the refresh icon when `isRefreshing` is active.
---
## 6. Feed Favicon & Website Icon Scraping / Upload
- **Goal**: Enhance visual recognition by displaying brand icons next to each feed.
- **Scope**:
- **Auto-Scraper**: Automatically fetch and cache `favicon.ico`, `<link rel="icon">`, or `<link rel="apple-touch-icon">` from the feed's website root during initial subscription.
- **Manual Customization**: Allow uploading or specifying a custom icon URL in `Settings > Subscriptions > Edit Feed`.
---
## 7. Active Post Reading Persistence on Subscription Switch
- **Goal**: Prevent reader disruption when exploring other subscriptions.
- **Scope**:
- When actively reading an article in the right canvas of 3-Split view, clicking a different subscription or category in the left sidebar should update the middle article list *without* abruptly closing or reloading the active article in the right canvas until a new article is explicitly selected.
---
## 8. Unified Feed & Category Tree Hierarchy ("Feeds" Root)
- **Goal**: Simplify feed organization into a single clean hierarchy.
- **Scope**:
- Eliminate the artificial separation between "Categorized Feeds" and "Uncategorized Feeds".
- Present a single unified **"Feeds"** section where uncategorized feeds reside at the root level alongside collapsible category folders.
- Drag-and-drop or simple dropdown assignment to move feeds between root and folders.
---
## 9. Enhanced Xiaohongshu Masonry Waterfall Experience
- **Goal**: Further elevate the card aesthetics and mobile interaction to closely match Xiaohongshu.
- **Scope**:
- Staggered dynamic aspect ratios with prominent media cards.
- Smooth infinite scroll pagination.
- Interactive gesture swipe to dismiss the full-screen reader.
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/haopengzhan/rsspipe module git.pengzhan.dev/haopengzhan/rsspipe
go 1.26.5 go 1.26.5
+6 -6
View File
@@ -11,13 +11,13 @@ import (
"strings" "strings"
"time" "time"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/auth"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/crawler"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/opml"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/parser"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/haopengzhan/rsspipe/internal/auth"
"github.com/haopengzhan/rsspipe/internal/crawler"
"github.com/haopengzhan/rsspipe/internal/db"
"github.com/haopengzhan/rsspipe/internal/models"
"github.com/haopengzhan/rsspipe/internal/opml"
"github.com/haopengzhan/rsspipe/internal/parser"
) )
type Handler struct { type Handler struct {
+1 -1
View File
@@ -6,10 +6,10 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/auth"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/haopengzhan/rsspipe/internal/auth"
) )
func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http.Handler { func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http.Handler {
+2 -2
View File
@@ -8,9 +8,9 @@ import (
"strings" "strings"
"time" "time"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/config"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/haopengzhan/rsspipe/internal/config"
"github.com/haopengzhan/rsspipe/internal/db"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/haopengzhan/rsspipe/internal/db" "git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
"github.com/haopengzhan/rsspipe/internal/models" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
"github.com/haopengzhan/rsspipe/internal/parser" "git.pengzhan.dev/haopengzhan/rsspipe/internal/parser"
) )
type Crawler struct { type Crawler struct {
+1 -1
View File
@@ -9,8 +9,8 @@ import (
"strings" "strings"
"time" "time"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/haopengzhan/rsspipe/internal/models"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/haopengzhan/rsspipe/internal/models" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
) )
func TestDatabaseOperations(t *testing.T) { func TestDatabaseOperations(t *testing.T) {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/haopengzhan/rsspipe/internal/models" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
) )
type OPML struct { type OPML struct {
+1 -1
View File
@@ -3,7 +3,7 @@ package opml
import ( import (
"testing" "testing"
"github.com/haopengzhan/rsspipe/internal/models" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
) )
func TestParseAndGenerateOPML(t *testing.T) { func TestParseAndGenerateOPML(t *testing.T) {
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
"github.com/mmcdole/gofeed" "github.com/mmcdole/gofeed"
"github.com/haopengzhan/rsspipe/internal/models"
) )
type FetchResult struct { type FetchResult struct {
@@ -39,7 +39,7 @@ func (p *Parser) FetchFeed(ctx context.Context, feedURL, etag, lastModified stri
return nil, fmt.Errorf("failed to create request: %w", err) return nil, fmt.Errorf("failed to create request: %w", err)
} }
req.Header.Set("User-Agent", "RSSPipe/1.0 (+https://github.com/haopengzhan/rsspipe)") req.Header.Set("User-Agent", "RSSPipe/1.0 (+https://git.pengzhan.dev/haopengzhan/rsspipe)")
req.Header.Set("Accept", "application/rss+xml, application/atom+xml, application/xml, text/xml, application/json;q=0.9, */*;q=0.8") req.Header.Set("Accept", "application/rss+xml, application/atom+xml, application/xml, text/xml, application/json;q=0.9, */*;q=0.8")
if etag != "" { if etag != "" {
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -e
echo "=================================================="
echo " Running RSSPipe Quality Checks "
echo "=================================================="
echo "==> 1. Checking Go formatting (gofmt)..."
UNFORMATTED=$(gofmt -l internal/ cmd/)
if [ -n "$UNFORMATTED" ]; then
echo "ERROR: The following Go files are not formatted properly:"
echo "$UNFORMATTED"
echo "Run: gofmt -w internal/ cmd/"
exit 1
fi
echo "✓ Go files formatted correctly."
echo "==> 2. Running Go vet..."
go vet ./...
echo "✓ Go vet passed."
echo "==> 3. Running Go tests..."
go test -v ./...
echo "✓ Go tests passed."
echo "==> 4. Building React Frontend (TypeScript + Vite)..."
cd web
npm run build
cd ..
echo "✓ Frontend build passed."
echo "==> 5. Running Secret Leak Scan (Gitleaks)..."
if command -v gitleaks &> /dev/null; then
gitleaks detect --no-git --source . -c .gitleaks.toml -v
echo "✓ Gitleaks scan passed."
else
echo "⚠ Gitleaks not installed locally, skipping local binary check (will run in CI)."
fi
echo "=================================================="
echo " 🎉 All RSSPipe checks passed cleanly! "
echo "=================================================="