3 Commits

6 changed files with 112 additions and 168 deletions

View File

@@ -1,114 +1,18 @@
name: CI/CD Pipeline name: Build and Push Docker Image
on: on:
push:
branches:
- master
- main
paths-ignore:
- '**.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
pull_request:
branches:
- master
- main
release: release:
types: [published] types: [published]
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
jobs: jobs:
quality-checks: build:
name: Code Quality & Security name: Build & Push Multi-Platform Docker Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run linter
run: bun run lint
- name: Check code formatting
run: bunx prettier --check .
continue-on-error: true
- name: Run tests
run: bun run test:all
- name: Security audit
run: bun audit
continue-on-error: true
build-test:
name: Build & Test
runs-on: ubuntu-latest
needs: quality-checks
if: github.event_name == 'push' || github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build project
run: |
PORT=5000 bun run start &
SERVER_PID=$!
sleep 10
kill $SERVER_PID || true
- name: Test API endpoints
run: |
PORT=5000 bun run start &
SERVER_PID=$!
sleep 10
curl -f http://localhost:5000/ping || exit 1
kill $SERVER_PID || true
publish-docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
if: github.event_name == 'release'
permissions: permissions:
contents: read contents: read
packages: write packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -122,15 +26,15 @@ jobs:
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ghcr.io/${{ github.repository }} images: ghcr.io/${{ github.repository_owner }}/hianime-api
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
@@ -147,5 +51,3 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View File

@@ -8,11 +8,54 @@ on:
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- '.gitignore' - '.gitignore'
pull_request:
branches:
- master
- main
jobs: jobs:
quality-checks:
name: Code Quality & Security
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run linter
run: bun run lint
- name: Check code formatting
run: bunx prettier --check .
continue-on-error: true
- name: Run tests
run: bun run test:all
release: release:
name: Tag and Release name: Tag and Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: quality-checks
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')
permissions: permissions:
contents: write contents: write

View File

@@ -215,14 +215,7 @@ docker-compose up -d
1. Fork or clone the repository to your GitHub account 1. Fork or clone the repository to your GitHub account
2. Sign up at [Vercel](https://vercel.com) 2. Sign up at [Vercel](https://vercel.com)
3. Create a new project and import your repository 3. Create a new project and import your repository
4. Configure environment variables in Vercel Dashboard: 4. Click "Deploy"
- `UPSTASH_REDIS_REST_URL` (Required - Get from [Upstash](https://upstash.com))
- `UPSTASH_REDIS_REST_TOKEN` (Required)
- `ORIGIN=*` (or your frontend domain)
- `RATE_LIMIT_ENABLED=true`
- `RATE_LIMIT_WINDOW_MS=60000`
- `RATE_LIMIT_LIMIT=100`
5. Click "Deploy"
**Why Vercel?** **Why Vercel?**
- ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Serverless architecture with automatic scaling - ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Serverless architecture with automatic scaling
@@ -230,18 +223,7 @@ docker-compose up -d
- ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Free tier with generous limits - ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Free tier with generous limits
- ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Automatic HTTPS and custom domains - ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Automatic HTTPS and custom domains
- ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Git-based deployments (auto-deploy on push) - ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Git-based deployments (auto-deploy on push)
- ![Supported](https://img.shields.io/badge/Supported-brightgreen?style=flat-square) Built-in Redis support via Upstash
**Environment Variables:**
| Key | Value | Required |
|-----|-------|----------|
| `UPSTASH_REDIS_REST_URL` | Your Upstash Redis URL | Yes |
| `UPSTASH_REDIS_REST_TOKEN` | Your Upstash Redis Token | Yes |
| `ORIGIN` | `*` or your domain | No |
| `RATE_LIMIT_ENABLED` | `true` | No |
| `RATE_LIMIT_WINDOW_MS` | `60000` | No |
| `RATE_LIMIT_LIMIT` | `100` | No |
--- ---

View File

@@ -1,46 +1,62 @@
import app from '../src/app'; import { Hono } from 'hono';
import { handle } from 'hono/vercel';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import hiAnimeRoutes from '../src/routes/routes';
import config from '../src/config/config';
import { AppError } from '../src/utils/errors';
import { fail } from '../src/utils/response';
type VercelResponse = { const app = new Hono();
status: (code: number) => VercelResponse;
setHeader: (name: string, value: string) => VercelResponse;
send: (body: string | object | Buffer) => VercelResponse;
json: (body: object) => VercelResponse;
};
export default async function handler( // CORS Configuration
req: { const origins = config.origin.includes(',')
headers: Record<string, string | string[] | undefined>; ? config.origin.split(',').map(o => o.trim())
method: string; : config.origin === '*'
url: string; ? '*'
body: unknown; : [config.origin];
},
res: VercelResponse
) {
try {
const protocol = req.headers['x-forwarded-proto'] || 'https';
const host = req.headers['x-forwarded-host'] || req.headers.host;
const url = `${protocol}://${host}${req.url}`;
const webRequest = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
body: req.method !== 'GET' && req.method !== 'HEAD' ? JSON.stringify(req.body) : undefined,
});
const webResponse = await app.fetch(webRequest); app.use(
res.status(webResponse.status); '*',
webResponse.headers.forEach((value: string, key: string) => { cors({
res.setHeader(key, value); origin: origins,
}); allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposeHeaders: ['Content-Length', 'X-Request-Id'],
maxAge: 600,
credentials: true,
})
);
const body = await webResponse.text(); // Logging
res.send(body); if (!config.isProduction || config.enableLogging) {
} catch (error: unknown) { app.use('/api/v2/*', logger());
const err = error as Error;
console.error('Vercel handler error:', err);
res.status(500).json({
success: false,
error: 'Internal Server Error',
message: err.message,
});
} }
// Health Check
app.get('/ping', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
environment: 'vercel',
});
});
// Routes
app.route('/api/v2', hiAnimeRoutes);
// Error Handling
app.onError((err, c) => {
if (err instanceof AppError) {
return fail(c, err.message, err.statusCode, err.details);
} }
console.error('Vercel Unexpected Error:', err.message);
return fail(c, 'Internal server error', 500);
});
app.notFound((c) => {
return fail(c, 'Route not found', 404);
});
export default handle(app);

View File

@@ -1,6 +1,6 @@
{ {
"name": "hianime-api", "name": "hianime-api",
"version": "2.0.0", "version": "2.1.0",
"type": "module", "type": "module",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -23,7 +23,8 @@
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
"test:jest": "NODE_OPTIONS='--experimental-vm-modules' jest", "test:jest": "NODE_OPTIONS='--experimental-vm-modules' jest",
"test:all": "bun run test && bun run test:jest", "test:all": "bun run test && bun run test:jest",
"clean": "rm -rf dist node_modules/.cache" "clean": "rm -rf dist node_modules/.cache",
"vercel-build": "bun build api/index.ts --outfile api/index.js --target node"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.8.2", "@hono/node-server": "^1.8.2",

View File

@@ -4,7 +4,7 @@
"rewrites": [ "rewrites": [
{ {
"source": "/(.*)", "source": "/(.*)",
"destination": "/api" "destination": "/api/index.js"
} }
] ]
} }