1.背景

服务器上有多个erp站点(圣地亚哥,非国内),都是同一套代码,部署的时候不想进每个站点挨着拉一遍,先用了jenkins,但是服务器内存占用有点高,就换了别的方式。用了这个方法内存从之前的常驻80降低到了常驻56,最少给老板省了200块钱,哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈

2.原理

推送代码到gite自动同步到github,触发代码的自动部署webhook,webhook里面写了git pull/composer install等命令,消息队列的自动部署是写好配置文件,通过脚本把配置文件复制到宝塔supervisor的配置文件目录,进而实现自动部署,有一个问题需要知悉!!!gite同步到github有小小的问题,第一分钟推了代码可以立即同步到github,第一分钟10s再次推送就没办法同步到github,具体规律尚不清晰

3.github操作

创建一个全新的空的仓库!全新的!空的!

4.宝塔操作

下面是部署脚本,在宝塔的webhook里面配置这个脚本,完成之后获取密钥就可以看到完整密钥,获取密钥配置到gite就可以了

#!/usr/bin/env bash

set -Eeuo pipefail

# ======================
# 配置
# ======================
NAME=”video”
PROJECT_PATH=”/www/wwwroot/hsgo.test”
RUN_USER=”www”
COMPOSER=”composer”
PHP=”php”

ROLLBACK_COMMIT=””

# ======================
# 日志
# ======================
log() {
echo “”
echo “[$(date ‘+%F %T’)] $1”
}

# ======================
# 错误处理
# ======================
on_error() {
local exit_code=$?
local line=$1
echo “”
echo “❌ 部署失败(第 ${line} 行,退出码 ${exit_code})”

if [ -n “$ROLLBACK_COMMIT” ]; then
echo “”
echo “如需回滚代码,请执行:”
echo ” sudo -u $RUN_USER bash -c \”cd ‘$PROJECT_PATH’ && git reset –hard $ROLLBACK_COMMIT\””
fi
exit “$exit_code”
}
trap ‘on_error $LINENO’ ERR

die() {
echo “”
echo “❌ 部署失败:$1″
exit 1
}

# ======================
# 以运行用户在项目目录执行命令
# ======================
run() {
local desc=”$1”
shift
log “$desc”
sudo -u “$RUN_USER” bash -c “cd ‘$PROJECT_PATH’ && $*” \
|| die “$desc”
}

run_capture() {
sudo -u “$RUN_USER” bash -c “cd ‘$PROJECT_PATH’ && $*”
}

# 检查仓库是否处于冲突 / merge 未完成状态(不依赖 git 退出码)
check_git_conflict() {
local stage=”$1″

if run_capture “test -f .git/MERGE_HEAD”; then
echo “”
echo “❌ ${stage}:检测到未完成的 merge(.git/MERGE_HEAD 存在)”
run_capture “git status –short” || true
die “${stage} 失败,服务器仓库存在未解决的冲突”
fi

local unmerged
unmerged=”$(run_capture “git ls-files -u” || true)”
if [ -n “$unmerged” ]; then
echo “”
echo “❌ ${stage}:检测到未解决冲突的文件”
echo “$unmerged”
die “${stage} 失败,服务器仓库存在未解决的冲突”
fi
}

echo “=================================”
echo “开始部署:$(date ‘+%F %T’)”
echo “项目:$NAME”
echo “=================================”

START_TS=$(date +%s)

# —- 前置检查 —-
[ -d “$PROJECT_PATH” ] || die “项目目录不存在:$PROJECT_PATH”
[ -d “$PROJECT_PATH/.git” ] || die “不是 git 仓库:$PROJECT_PATH”
id “$RUN_USER” >/dev/null 2>&1 || die “用户不存在:$RUN_USER”
command -v “$COMPOSER” >/dev/null || die “未找到 composer”

# 拉取前先检查是否已有历史遗留冲突
check_git_conflict “部署前检查”

ROLLBACK_COMMIT=”$(run_capture ‘git rev-parse HEAD’)”
log “当前版本:$ROLLBACK_COMMIT”

# —- 更新代码 —-
log “更新代码”
if ! run_capture “git pull”; then
echo “”
echo “当前工作区状态:”
run_capture “git status –short” || true
die “更新代码”
fi

# 拉取后再校验一次(兜底:即使 git pull 返回 0 也拦住冲突态)
check_git_conflict “git pull”

NEW_COMMIT=”$(run_capture ‘git rev-parse HEAD’)”
if [ “$NEW_COMMIT” = “$ROLLBACK_COMMIT” ]; then
log “代码无变化(已是最新),仍继续执行依赖与迁移检查”
else
log “更新到版本:$NEW_COMMIT”
fi

# —- 安装依赖(生产参数)—-
run “安装 Composer 依赖” \
“$COMPOSER install –no-dev –optimize-autoloader –no-interaction –no-progress”

# —- 数据库迁移 —-
log “执行数据库迁移”
MIGRATE_LOG=”/tmp/deploy_migrate.log”
if ! sudo -u “$RUN_USER” bash -c “cd ‘$PROJECT_PATH’ && $PHP think migrate:run” >”$MIGRATE_LOG” 2>&1; then
cat “$MIGRATE_LOG”
die “执行数据库迁移”
fi
cat “$MIGRATE_LOG”

# think 出错时可能仍返回 0,按输出兜底判定
grep -qE ‘All Done|Nothing to migrate’ “$MIGRATE_LOG” \
|| die “数据库迁移未正常结束(详见 $MIGRATE_LOG)”
if grep -qiE ‘exception|fatal|sqlstate’ “$MIGRATE_LOG”; then
die “数据库迁移报错(详见 $MIGRATE_LOG)”
fi

# —- 清理 / 生成缓存 —-
log “清理并优化缓存”
OPT_LOG=”/tmp/deploy_optimize.log”
if ! sudo -u “$RUN_USER” bash -c “cd ‘$PROJECT_PATH’ && $PHP think optimize” >”$OPT_LOG” 2>&1; then
cat “$OPT_LOG”
die “清理并优化缓存”
fi
cat “$OPT_LOG”

grep -q ‘optimize:config run succeed’ “$OPT_LOG” || die “optimize:config 失败(详见 $OPT_LOG)”
grep -q ‘optimize:route run succeed’ “$OPT_LOG” || die “optimize:route 失败(详见 $OPT_LOG)”
grep -q ‘optimize:schema run succeed’ “$OPT_LOG” || die “optimize:schema 失败(详见 $OPT_LOG)”

# —- 修正文件权限 —-
run “修正 runtime 权限” “chown -R $RUN_USER:$RUN_USER runtime && chmod -R 775 runtime”

# —- 同步消息队列守护进程配置 —-
log “同步消息队列守护进程配置”
if ! sudo -u root bash “${PROJECT_PATH}/public/supervisor/supervisor.sh”; then
echo “”
echo “提示:检查 .env 中是否已配置 SIGN(缺失时退出码 12),”
echo ” 以及 ${PROJECT_PATH}/.env 是否存在(缺失时退出码 11)。”
die “同步 Supervisor 队列配置”
fi
log “队列守护进程配置已同步”

END_TS=$(date +%s)

echo “”
echo “=================================”
echo “✅ 部署成功”
echo “版本:$NEW_COMMIT”
echo “耗时:$((END_TS – START_TS)) 秒”
echo “结束时间:$(date ‘+%F %T’)”
echo “=================================”

webhook脚本

5.gite操作

gite项目仓库–管理–WebHooks–配置自己的url和密码–在宝塔的软件webhook进行配置获取

gite项目仓库–管理–仓库镜像管理–和新创建的github仓库进行关联

6.github自动部署代码

推送到gite之后,gite会自动同步到github镜像仓库,有了下面的代码就能让github自动请求webhook地址

name: Deploy
on:
  push:
    branches:
      - api
jobs:
  deploy:
    name: 触发部署
    runs-on: ubuntu-latest
    steps:
      - name: 23
        run: |
          curl -s "http://bt.zntag.com:8888/hook?access_key=VoUNrzVy5425cy0AmGHJ0EkY4XWGU"
      - name: 34
        run: |
          curl -s "http://bt.zntag.com:8888/hook?access_key=Dy8mys3451WkjsT6QvWqZ4ER8c3JNkciYT"
      - name: 234234
        run: |
          curl -s "http://bt.zntag.com:8888/hook?access_key=BOst00lQi345a7BiOzfkm2gASuR4e1"

github自动action

7.webhook–消息队列和socket

public/supervisor下的自动配置supervisor脚本

#!/bin/bash

# 获取脚本的绝对路径
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# 定义 .env 文件路径(上上层目录:父目录的父目录)
PARENT_DIR=$(dirname "$SCRIPT_DIR")
GRANDPARENT_DIR=$(dirname "$PARENT_DIR")
ENV_PATH="$GRANDPARENT_DIR/.env"

# 检查 .env 文件是否存在
if [ ! -f "$ENV_PATH" ]; then
    echo "错误: 找不到 .env 文件"
    exit 11
fi


# 安全地读取 .env 文件,获取sign
while IFS= read -r line || [[ -n "$line" ]]; do
    # 跳过空行和注释
    if [[ -z "$line" ]] || [[ "$line" =~ ^[[:space:]]*# ]]; then
        continue
    fi

    # 移除 export 关键字和空格
    line_clean=$(echo "$line" | sed 's/^export[[:space:]]*//')

    # 提取键值对
    key=$(echo "$line_clean" | cut -d '=' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    value=$(echo "$line_clean" | cut -d '=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')

    # 去除可能的引号
    value=$(echo "$value" | sed -e "s/^['\"]//;s/['\"]$//")

    # 根据键设置变量(注意:.env 中是大写的 SIGN)
    case "$key" in
        SIGN)
            sign="$value"
            ;;
    esac
done < "$ENV_PATH"

# 检查必需的变量
if [ -z "${sign}" ]; then
    echo "错误: .env 文件中缺少 'SIGN' 配置"
    exit 12
fi

# supervisor 配置路径
supervisor_path="/www/server/panel/plugin/supervisor/profile"

# 创建一个函数来处理文件复制和替换逻辑
process_file() {
    local source_file="$1"
    local target_file="$2"

    if [ -e "$source_file" ]; then
        # 读取文件内容
        content=$(cat "$source_file")

        # 替换 %path% 和 %sign%(注意模板中的占位符是小写)
        # 将模板中的小写占位符替换为实际值
        replaced_content=$(echo "$content" | sed "s|%path%|${GRANDPARENT_DIR}/|g; s|%sign%|$sign|g")

        # 检查目标文件是否存在
        if [ ! -e "$target_file" ]; then
            echo "$replaced_content" > "$target_file"
        else
            # 目标文件存在,比较内容
            if ! echo "$replaced_content" | diff -q "$target_file" - > /dev/null 2>&1; then
                echo "更新文件: $target_file"
                echo "$replaced_content" > "$target_file"
            else
                echo "文件内容相同,无需更新: $target_file"
            fi
        fi
    else
        echo "警告: 源文件不存在: $source_file"
    fi
}

# 处理每个文件
for file in "$SCRIPT_DIR"/*.ini; do
    if [ -f "$file" ]; then
        filename="${file##*/}"
        filename="${filename%.ini}"  # 去掉 .ini 后缀
        origin="${SCRIPT_DIR}/${filename}.ini"
        target="${supervisor_path}/hsl_${filename}_${sign}.ini"
        process_file "$origin" "$target"
    fi
done
$(/www/server/panel/pyenv/bin/supervisorctl update)
echo "处理完成!"

supervisor配置脚本

消息队列配置文件如下,这么写推送到服务器能自动在supervisor创建守护进程

[program:hsl_expire_%sign%]
command=php think queue:listen --queue=expire_%sign%
directory=%path%
autorestart=true
startsecs=3
startretries=3
stdout_logfile=/www/server/panel/plugin/supervisor/log/expire_%sign%.out.log
stderr_logfile=/www/server/panel/plugin/supervisor/log/expire_%sign%.err.log
stdout_logfile_maxbytes=2MB
stderr_logfile_maxbytes=2MB
user=www
priority=999
numprocs=1
process_name=%(program_name)s_%(process_num)02d

配置文件

.env配置,和配置文件搭配使用

APP_DEBUG = false

APP_URL = https://a23234234234234.cn/
SIGN = amuse

TIMEZONE = America/Santiago
REDIS_SELECT = 3

REGISTER_ADDRESS = 127.0.0.1:1206
GATEWAY_ADDRESS = websocket://0.0.0.0:8206
GATEWAY_START_PORT = 4006

.env配置