Write the Code. Change the World.

7月 28

现在有这样一个需求。本地推送前端代码到 codeberg 后,服务端拉取代码,打包后,放到对应的位置。

服务器服务,全是由 docker 构建,文件复制移动全靠 volume 。

构建 api 接口服务

新建 webhook-server 目录,里边有下边几个文件。

.
├── Dockerfile
├── package.json
└── server.js

Dockerfile

FROM node:latest

WORKDIR /app

# 新增:安装docker客户端
RUN apt-get update && apt-get install -y ca-certificates curl \
    && curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-27.3.1.tgz \
    | tar -xz -C /usr/local/bin --strip-components=1 docker/docker \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

COPY package.json ./
RUN npm install --omit=dev

COPY server.js ./

EXPOSE 9000
CMD ["node", "server.js"]

package.json

{
  "name": "webhook-server",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "express": "^5.0"
  }
}

server.js

/**
 * Webhook 接收服务
 *
 * 接收 Codeberg (Gitea) 的 push 通知,验证签名后触发 deploy.sh
 */
const express = require('express');
const crypto = require('crypto');
const { spawn } = require('child_process');

const PORT = parseInt(process.env.WEBHOOK_PORT || '9000', 10);
const SECRET = process.env.WEBHOOK_SECRET || '';
const DEPLOY_SCRIPT = process.env.DEPLOY_SCRIPT || '/opt/frontend-cicd/deploy.sh';
const TARGET_BRANCH = `refs/heads/${process.env.BRANCH || 'main'}`;

if (!SECRET || SECRET === 'CHANGE_ME') {
  console.error('[ERROR] 请先设置 WEBHOOK_SECRET');
  process.exit(1);
}

const app = express();
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook/deploy', (req, res) => {
  const body = req.body.toString('utf8');
  const sig = req.headers['x-gitea-signature'] || '';
  const event = req.headers['x-gitea-event'] || '';

  // 验证 HMAC-SHA256 签名
  const expected = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
  if (sig !== expected) {
    console.warn(`[${ts()}] 签名验证失败`);
    return res.status(403).json({ error: 'Invalid signature' });
  }

  if (event !== 'push') return res.json({ msg: `忽略事件: ${event}` });

  let payload;
  try { payload = JSON.parse(body); } catch { return res.status(400).end(); }

  const ref = payload.ref || '';
  if (ref !== TARGET_BRANCH) return res.json({ msg: `忽略分支: ${ref}` });

  const pusher = payload.pusher?.login || '?';
  const commits = (payload.commits || []).length;
  console.log(`[${ts()}] ${pusher} 推送了 ${commits} 个提交到 ${ref},开始部署...`);

  // 先返回 200,避免 Codeberg 超时
  res.json({ msg: 'deploy triggered' });

        console.log(`[${ts()}] 开始执行部署脚本: ${DEPLOY_SCRIPT}`);
        const child = spawn('bash', [DEPLOY_SCRIPT], { timeout: 600000 });

        child.stdout.on('data', (data) => {
          console.log(`[${ts()}] [部署stdout] ${data.toString()}`);
        });
        child.stderr.on('data', (data) => {
          console.error(`[${ts()}] [部署stderr] ${data.toString()}`);
        });

        child.on('close', (code) => {
          if (code === 0) {
            console.log(`[${ts()}] 部署完成,退出码:${code}`);
          } else {
            console.error(`[${ts()}] 部署失败,脚本退出码:${code}`);
          }
        });

        child.on('error', (err) => {
          console.error(`[${ts()}] 启动脚本异常:`, err);
        });
});

app.get('/health', (_, res) => {
  res.json({ ok: true, uptime: process.uptime(), time: new Date().toISOString() });
});

app.listen(PORT, () => {
  console.log(`Webhook 服务已启动,端口 ${PORT}`);
  console.log(`  POST /webhook/deploy  (分支: ${TARGET_BRANCH})`);
  console.log(`  GET  /health`);
});

function ts() { return new Date().toISOString(); }

运行 api 接口服务

这里是完整的目录和文件。

.
├── deploy.sh
├── docker-compose.yml
├── setup.sh
└── webhook-server
    ├── Dockerfile
    ├── package.json
    └── server.js

deploy.sh

#!/bin/bash
# ============================================================
# 自动部署脚本 —— 兼容 webhook docker 容器运行
# ============================================================
set -euo pipefail

# ======================== 配置(修改路径!) ========================
PROJECT_DIR="/alidata/code/editor.manhuariji.com"
DEPLOY_DIR="/alidata/www/editor.manhuariji.com/public"
DOCKER_IMAGE="xr-node:20"
CONTAINER_NAME="editor-mhrj"
BUILD_CMD="npm run build:h5"
BUILD_OUTPUT="dist"
DETAIL_OUTPUT="build/h5"
BRANCH="main"
# 【修改点1】日志放到容器内/app目录,拥有写入权限!
LOG_FILE="/app/editor-mhrj-deploy.log"
LOCK_FILE="/tmp/editor-mhrj-deploy.lock"
# =====================================================

# ---- 防并发 ----
if [ -f "$LOCK_FILE" ]; then
  echo "$(date '+%Y-%m-%d %H:%M:%S') [WARN] 有部署正在运行,跳过" >> "$LOG_FILE"
  exit 0
fi
touch "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT

# log函数:同时输出到日志文件 AND 控制台(调试关键!)
log() {
    local msg="$(date '+%Y-%m-%d %H:%M:%S') $1"
    echo "$msg" >> "$LOG_FILE"
    echo "$msg"   # 打印到终端,Node服务可以捕获
}

log "========================================"
log "开始部署"
log "========================================"

# ---- 1. Git 拉取代码 ----
log "[1/3] cd $PROJECT_DIR && git pull ..."
cd "$PROJECT_DIR"

PREV=$(git rev-parse --short HEAD 2>/dev/null || echo "none")

# 强制同步远端代码,丢弃本地所有修改(适合自动部署服务器)
log "[INFO] 重置本地代码,对齐远端origin/${BRANCH}"
git fetch origin "${BRANCH}" >> "$LOG_FILE" 2>&1
git reset --hard "origin/${BRANCH}" >> "$LOG_FILE" 2>&1

NEW=$(git rev-parse --short HEAD)

#if [ "$PREV" = "$NEW" ]; then
#   log "没有新提交 ($NEW),跳过构建"
#   exit 0
# fi

log "代码更新: $PREV -> $NEW"

# ---- 2. Docker 容器内构建 ----
# 新增:清理宿主机缓存,腾出内存
log "[INFO] 清理系统缓存释放内存"
sync; echo 3 > /proc/sys/vm/drop_caches

log "[2/3] Docker 容器构建 ($DOCKER_IMAGE) ..."
log "  npm install + $BUILD_CMD"

docker run --rm \
  --memory=2G \
  --memory-swap=3G \
  --name "$CONTAINER_NAME" \
  -v "${PROJECT_DIR}":/www \
  -w /www \
  "$DOCKER_IMAGE" \
  bash -c "
    npm config set registry https://registry.npmmirror.com
    npm install
    NODE_OPTIONS='--max-old-space-size=1400' npm run build:h5
  " >> "$LOG_FILE" 2>&1

if [ $? -ne 0 ]; then
  log "[ERROR] 构建失败!"
  exit 1
fi

if [ ! -d "${PROJECT_DIR}/${BUILD_OUTPUT}" ]; then
  log "[ERROR] 构建产物目录 '${BUILD_OUTPUT}' 不存在"
  exit 1
fi

log "构建完成,产物: ${PROJECT_DIR}/${BUILD_OUTPUT}"

# ---- 3. 部署 ----
log "[3/3] 部署到 $DEPLOY_DIR ..."

# 备份逻辑保留(可选)
#if [ -d "$DEPLOY_DIR" ] && [ "$(ls -A "$DEPLOY_DIR" 2>/dev/null)" ]; then
#  BACKUP="${DEPLOY_DIR}.backup"
#  rm -rf "$BACKUP"
#  mv "$DEPLOY_DIR" "$BACKUP"
#  log "旧版本备份到 $BACKUP"
#fi

mkdir -p "$DEPLOY_DIR"
SRC="${PROJECT_DIR}/${BUILD_OUTPUT}/${DETAIL_OUTPUT}"

# 使用 cp -r 源头目录/. 方案,完美规避所有通配符问题
if [ -d "$SRC" ]; then
  cp -r "${SRC}/." "${DEPLOY_DIR}/"
else
  log "[ERROR] 构建目录不存在: $SRC"
  exit 1
fi

log "========================================"
log "部署完成!($PREV -> $NEW)"
log "========================================"

# ---- 日志截断 ----
MAX_LINES=3000
if [ -f "$LOG_FILE" ]; then
  LINES=$(wc -l < "$LOG_FILE")
  if [ "$LINES" -gt "$MAX_LINES" ]; then
    tail -n "$MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp"
    mv "${LOG_FILE}.tmp" "$LOG_FILE"
  fi
fi

bash.sh

#!/bin/bash
# ============================================================
# 一键初始化脚本
#
# 用法:
#   1. scp -r frontend-cicd root@服务器IP:/opt/
#   2. 编辑 /opt/frontend-cicd/.env(填仓库地址和 Secret)
#   3. bash /opt/frontend-cicd/setup.sh
# ============================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/.env"

echo "=========================================="
echo " editor.manhuariji.com 自动部署初始化"
echo "=========================================="

# ---- 1. 检查环境 ----
echo ""
echo "[1/5] 检查环境 ..."
for cmd in git docker; do
  if ! command -v $cmd &>/dev/null; then
    echo "[ERROR] $cmd 未安装"
    exit 1
  fi
done
echo "  git:    $(git --version)"
echo "  docker: $(docker --version)"

if [ "$WEBHOOK_SECRET" = "CHANGE_ME" ]; then
  echo ""
  echo "[ERROR] 请先编辑 .env,把 WEBHOOK_SECRET 改成随机字符串"
  echo "  生成方法: openssl rand -hex 24"
  exit 1
fi

# ---- 2. 克隆项目 ----
echo ""
echo "[2/5] 项目代码 ..."
if [ -d "$PROJECT_DIR/.git" ]; then
  echo "  已存在,git pull ..."
  cd "$PROJECT_DIR" && git pull
else
  echo "  克隆 $REPO_URL ..."
  mkdir -p "$(dirname "$PROJECT_DIR")"
  git clone "$REPO_URL" "$PROJECT_DIR"
fi

# ---- 3. 首次构建验证 ----
echo ""
echo "[3/5] 首次构建验证 ..."
cd "$PROJECT_DIR"

docker run --rm \
  --name "$CONTAINER_NAME" \
  -v "${PROJECT_DIR}":/www \
  -w /www \
  "$DOCKER_IMAGE" \
  bash -c "npm install && $BUILD_CMD"

if [ -d "$BUILD_OUTPUT" ]; then
  mkdir -p "$(dirname "$DEPLOY_DIR")"
  rm -rf "$DEPLOY_DIR"
  mv "$BUILD_OUTPUT" "$DEPLOY_DIR"
  echo "  首次部署完成 -> $DEPLOY_DIR"
else
  echo "[WARN] 构建产物不存在,跳过部署"
fi

# ---- 4. 准备 Webhook 镜像依赖 ----
echo ""
echo "[4/5] 准备 Webhook 镜像 ..."
# 把宿主机的 docker CLI 复制到 webhook-server 目录,Dockerfile 会 COPY 进去
# 这样构建镜像时不需要拉任何外部镜像
cp "$(which docker)" "$SCRIPT_DIR/webhook-server/docker"
echo "  已复制 docker CLI -> webhook-server/docker"

# ---- 5. 启动 Webhook ----
echo ""
echo "[5/5] 启动 Webhook 服务 ..."
cd "$SCRIPT_DIR"
docker compose up -d --build

echo ""
echo "=========================================="
echo " 初始化完成!"
echo "=========================================="
echo ""
echo " Webhook: http://服务器IP:$WEBHOOK_PORT/webhook/deploy"
echo " Secret:  $WEBHOOK_SECRET"
echo ""
echo " 验证:"
echo "   curl http://localhost:$WEBHOOK_PORT/health"
echo "   docker logs editor-mhrj-webhook"
echo ""
echo " 下一步:"
echo "   1. 阿里云安全组放行 $WEBHOOK_PORT 端口"
echo "   2. Codeberg → Settings → Webhooks → Add Webhook (Gitea)"
echo "      URL:    http://你的服务器IP:$WEBHOOK_PORT/webhook/deploy"
echo "      Secret: $WEBHOOK_SECRET"
echo "      Event:  Push Events"
echo "   3. 本地 git push 验证"
echo ""

docker-compose.yaml

"docker-compose.yml" 25L, 851C                                                                                                                                                                                                              7,5          全部
services:
  webhook-server:
    build: ./webhook-server
    container_name: editor-mhrj-webhook
    restart: always
    privileged: true
    volumes:
      - ./webhook-server/server.js:/app/server.js
      # 挂载 docker.sock,让容器能调用宿主机的 docker 执行 deploy.sh
      - /var/run/docker.sock:/var/run/docker.sock
      - /root/.ssh:/root/.ssh:ro
      # 挂载部署脚本(只读)
      - ./deploy.sh:/app/deploy.sh:ro
      - /alidata/code/editor.manhuariji.com:/alidata/code/editor.manhuariji.com
      - /alidata/www/editor.manhuariji.com/public:/alidata/www/editor.manhuariji.com/public
    environment:
      - WEBHOOK_SECRET=${WEBHOOK_SECRET}
      - WEBHOOK_PORT=${WEBHOOK_PORT:-9000}
      - BRANCH=${BRANCH:-main}
      - DEPLOY_SCRIPT=/app/deploy.sh
    networks:
      - xrnet
networks:
  xrnet:
    external: true

.env

# ======================== 按需修改 ========================
# Codeberg 仓库 SSH 地址(仅 setup.sh 首次 clone 用,deploy.sh 走已有的 remote)
REPO_URL=git@codeberg.org:manhuariji/manhuariji.h5.git

# 项目在宿主机上的路径
PROJECT_DIR=/alidata/code/editor.manhuariji.com

# 部署目标(Nginx public 目录)
DEPLOY_DIR=/alidata/www/editor.manhuariji.com/public

# 构建用的 Docker 镜像(你自己的镜像)
DOCKER_IMAGE=xr-node:20

# 容器名
CONTAINER_NAME=editor-mhrj

# 构建命令
BUILD_CMD=npm run build:h5

# 构建产物目录名
BUILD_OUTPUT=dist

# 打包输出详情目录
DETAIL_OUTPUT=build/h5/*

# 部署分支
BRANCH=main

# ---- WEBHOOK SECRET(和 git 无关!)----
# 这是 Codeberg 和你的 webhook 服务之间的"暗号",用来验证 HTTP 请求确实来自 Codeberg。
# 防止别人往你的 webhook 地址随便发 POST 触发部署。
# 生成方法:openssl rand -hex 24
# 这个值要和 Codeberg Webhook 设置页填的 Secret 一致。
# WEBHOOK_SECRET=CHANGE_ME
WEBHOOK_SECRET=b85cb03fd42507f5f921ac4534e76155c0045aed49d0236a

# Webhook 监听端口
WEBHOOK_PORT=9000

运行起来: docker compose up -d

nginx 代理

    location /cicd/ {
        proxy_pass http://webhook-server:9000/; 

        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_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Context /cicd;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_connect_timeout 60s;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }

    location = /cicd {
        return 301 /cicd/;
    }

然后通过域名就可以开始部署了: https://xxx.com/cicd/webhook/deploy

codeberg 配置 Forgejo

登录 codeberg,进入项目,点击设置,切换到 web钩子页签。再点击添加web钩子, 选择 Forgejo。开始进行配置。

密钥文件和服务器上的文件一致哈。

可以点击 测试推送进行测试,也可以主动推送代码进行测试。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注