Merge pull request 'fix: 修复 deploy 工作流缺少 Node.js 导致 checkout 失败的问题' #72

Merged
huanghaosheng merged 61 commits from develop into main 2026-06-14 14:37:27 +08:00
6 changed files with 272 additions and 0 deletions
Showing only changes of commit 7b3f4706e1 - Show all commits

View File

@@ -0,0 +1,53 @@
name: Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# ---- PR 时:验证 Docker 镜像可构建 ----
verify:
if: github.event_name == 'pull_request'
runs-on: aliyun
steps:
- name: Checkout
uses: "http://8.161.227.145:3000/huanghaosheng/checkout@releases/v4"
- name: Verify Frontend Build
run: docker build -t camtalk-frontend-test ./frontend
- name: Verify Backend Build
run: docker build -t camtalk-backend-test ./backend
- name: Cleanup
run: |
docker rmi camtalk-frontend-test camtalk-backend-test || true
# ---- push main 时:构建并部署 ----
deploy:
if: github.event_name == 'push'
runs-on: aliyun
steps:
- name: Checkout
uses: "http://8.161.227.145:3000/huanghaosheng/checkout@releases/v4"
- name: Build and Deploy
run: |
chmod +x deploy.sh
./deploy.sh build
./deploy.sh restart
- name: Health Check
run: |
for i in $(seq 1 10); do
if curl -sf http://localhost/api/health > /dev/null 2>&1; then
echo "服务启动成功"
exit 0
fi
echo "等待服务启动... ($i/10)"
sleep 3
done
echo "服务启动超时"
exit 1

27
backend/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# ---- 构建阶段 ----
FROM golang:1.26-alpine AS builder
WORKDIR /app
# 先复制依赖清单,利用 Docker 缓存层
COPY go.mod go.sum ./
RUN go mod download
# 复制源码并构建
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /camtalk ./cmd/server
# ---- 运行阶段 ----
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
# 复制二进制和配置
COPY --from=builder /camtalk .
COPY config.yaml .
EXPOSE 8080
ENTRYPOINT ["./camtalk"]

91
deploy.sh Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$PROJECT_DIR"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# 确保 .env 存在
ensure_env() {
if [ ! -f .env ]; then
if [ -f .env.example ]; then
warn ".env 文件不存在,从 .env.example 复制模板"
cp .env.example .env
warn "请编辑 .env 填入实际的 API Key然后重新运行"
exit 1
else
error ".env.example 也不存在,请手动创建 .env"
exit 1
fi
fi
}
cmd_build() {
info "构建 Docker 镜像..."
docker compose build
info "构建完成"
}
cmd_up() {
ensure_env
info "启动服务..."
docker compose up -d
info "服务已启动"
info "前端: http://localhost"
info "健康检查: http://localhost/api/health"
}
cmd_down() {
info "停止服务..."
docker compose down
info "服务已停止"
}
cmd_restart() {
info "重启服务..."
cmd_down
cmd_up
}
cmd_logs() {
docker compose logs -f "${@}"
}
cmd_status() {
docker compose ps
}
usage() {
cat <<EOF
CamTalk 部署脚本
用法: $0 <命令>
命令:
build 构建 Docker 镜像
up 启动服务(后台运行)
down 停止服务
restart 重启服务
logs 查看日志(可加服务名,如: $0 logs backend
status 查看服务状态
EOF
}
case "${1:-}" in
build) cmd_build ;;
up) cmd_up ;;
down) cmd_down ;;
restart) cmd_restart ;;
logs) shift; cmd_logs "$@" ;;
status) cmd_status ;;
*) usage; exit 1 ;;
esac

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: camtalk-frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- camtalk-net
restart: unless-stopped
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: camtalk-backend
env_file:
- .env
environment:
- APP_ENV=production
networks:
- camtalk-net
restart: unless-stopped
networks:
camtalk-net:
driver: bridge

25
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# ---- 构建阶段 ----
FROM node:22-alpine AS builder
WORKDIR /app
# 先复制依赖清单,利用 Docker 缓存层
COPY package.json package-lock.json ./
RUN npm ci
# 复制源码并构建
COPY . .
RUN npm run build
# ---- 运行阶段 ----
FROM nginx:stable-alpine
# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

46
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,46 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# 前端静态资源
location / {
try_files $uri $uri/ /index.html;
}
# REST API 反代
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket 反代
location /ws {
proxy_pass http://backend:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
# Gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 256;
}