5 Commits

8 changed files with 118 additions and 170 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-v2
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

@@ -15,6 +15,7 @@
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.0.0",
"@vercel/ncc": "^0.38.4",
"@vitest/ui": "^4.1.1", "@vitest/ui": "^4.1.1",
"bun-types": "^1.3.11", "bun-types": "^1.3.11",
"eslint": "^8.57.0", "eslint": "^8.57.0",
@@ -331,6 +332,8 @@
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@vercel/ncc": ["@vercel/ncc@0.38.4", "", { "bin": { "ncc": "dist/ncc/cli.js" } }, "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ=="],
"@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="], "@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="],
"@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="], "@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="],

View File

@@ -1,6 +1,6 @@
{ {
"name": "hianime-api", "name": "hianime-v2",
"version": "2.0.0", "version": "2.2.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": "ncc build api/index.ts -o api"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.8.2", "@hono/node-server": "^1.8.2",
@@ -36,6 +37,7 @@
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.0.0",
"@vercel/ncc": "^0.38.4",
"@vitest/ui": "^4.1.1", "@vitest/ui": "^4.1.1",
"bun-types": "^1.3.11", "bun-types": "^1.3.11",
"eslint": "^8.57.0", "eslint": "^8.57.0",

View File

@@ -1,5 +1,5 @@
const config = { const config = {
baseurl: 'https://hianime.to', baseurl: 'https://aniwatchtv.to',
baseurl2: 'https://aniwatchtv.to', baseurl2: 'https://aniwatchtv.to',
origin: '*', origin: '*',
port: 5000, port: 5000,

View File

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