From 8b3ca2fd62c6af4718bc346ce12d091c0fa849b6 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Mar 2026 00:12:27 +0800 Subject: [PATCH] ci: add non-docker production deployment workflow --- .github/workflows/deploy-prod.yml | 82 +++++++++++++++++ docs/README.md | 1 + docs/common/README.md | 4 + docs/common/deploy_ci.md | 88 ++++++++++++++++++ scripts/ops/deploy_binary.sh | 148 ++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 docs/common/deploy_ci.md create mode 100755 scripts/ops/deploy_binary.sh diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..062d1d3 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,82 @@ +name: deploy-prod-non-docker + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: wx-service-prod + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.x' + + - name: Download modules + run: go mod download + + - name: Build linux binary + run: | + mkdir -p tmp + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o tmp/wx_service ./cmd/api + + - name: Prepare SSH + env: + SSH_KEY: ${{ secrets.PROD_SSH_KEY }} + HOST: ${{ secrets.PROD_HOST }} + PORT: ${{ secrets.PROD_PORT }} + run: | + set -e + if [ -z "$SSH_KEY" ] || [ -z "$HOST" ]; then + echo "Missing required secrets: PROD_SSH_KEY / PROD_HOST" + exit 1 + fi + mkdir -p ~/.ssh + chmod 700 ~/.ssh + printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -p "${PORT:-22}" "$HOST" >> ~/.ssh/known_hosts + + - name: Upload binary to server + env: + HOST: ${{ secrets.PROD_HOST }} + PORT: ${{ secrets.PROD_PORT }} + USER: ${{ secrets.PROD_USER }} + run: | + set -e + REMOTE_BIN="/tmp/wx_service-${GITHUB_SHA}" + scp -P "${PORT:-22}" tmp/wx_service "${USER:-root}@${HOST}:${REMOTE_BIN}" + + - name: Deploy on server + env: + HOST: ${{ secrets.PROD_HOST }} + PORT: ${{ secrets.PROD_PORT }} + USER: ${{ secrets.PROD_USER }} + run: | + set -e + REMOTE_BIN="/tmp/wx_service-${GITHUB_SHA}" + ssh -p "${PORT:-22}" "${USER:-root}@${HOST}" \ + "APP_DIR='/www/wwwroot/wx_service' \ + DIST_DIR='/www/wwwroot/wx_service/dist' \ + SOURCE_BIN='${REMOTE_BIN}' \ + RELEASE_ID='${GITHUB_SHA}' \ + SERVICE_NAME='wx_service' \ + RUN_USER='www' \ + RUN_GROUP='www' \ + PORT='8080' \ + SYNC_CODE='true' \ + DEPLOY_REF='${GITHUB_SHA}' \ + INSTALL_SERVICE='true' \ + bash -s" < scripts/ops/deploy_binary.sh diff --git a/docs/README.md b/docs/README.md index 9f0d49b..ab6f419 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ - `docs/common/upload_qiniu.md` - `docs/common/wechat_official.md` - `docs/common/redis.md` +- `docs/common/deploy_ci.md`(GitHub Actions 非 Docker 自动化发布) ## 去水印小程序 diff --git a/docs/common/README.md b/docs/common/README.md index 662e0dc..d6fb114 100644 --- a/docs/common/README.md +++ b/docs/common/README.md @@ -34,3 +34,7 @@ ## Redis - `docs/common/redis.md` + +## 自动化部署(非 Docker) + +- `docs/common/deploy_ci.md` diff --git a/docs/common/deploy_ci.md b/docs/common/deploy_ci.md new file mode 100644 index 0000000..8999e4d --- /dev/null +++ b/docs/common/deploy_ci.md @@ -0,0 +1,88 @@ +# 生产部署(GitHub Actions,非 Docker) + +本文档用于 `wx_service` 在宝塔服务器上的自动化发布: +- 触发:`main` 分支 push 或手动触发 +- 流程:GitHub Actions 构建二进制 -> SSH 上传到服务器 -> 远程发布脚本重启并健康检查 +- 特点:不依赖 Docker + +## 1. 服务器约定 + +- 代码目录:`/www/wwwroot/wx_service` +- 运行目录:`/www/wwwroot/wx_service/dist` +- 二进制:`/www/wwwroot/wx_service/dist/wx_service` +- 进程端口:`8080` +- 反向代理:宝塔 Nginx -> `127.0.0.1:8080` + +> 远程脚本:`scripts/ops/deploy_binary.sh` + +## 2. GitHub Secrets + +在仓库 `Settings -> Secrets and variables -> Actions` 新增: + +- `PROD_HOST`:生产机 IP 或域名 +- `PROD_PORT`:SSH 端口(默认 `22`) +- `PROD_USER`:SSH 用户(建议 `root` 或具备发布权限的用户) +- `PROD_SSH_KEY`:私钥内容(建议单独部署密钥) + +## 3. 工作流文件 + +- `/.github/workflows/deploy-prod.yml` + +已默认发布到:`/www/wwwroot/wx_service/dist/wx_service`。 + +## 4. 首次上线注意事项 + +1. 服务器必须已存在:`/www/wwwroot/wx_service/.git`(可正常 `git fetch`) +2. 服务器上配置好生产 `.env`(建议放两份): + - `/www/wwwroot/wx_service/.env` + - `/www/wwwroot/wx_service/dist/.env` +3. 开放 `8080` 本地监听,并由 Nginx 反代 +4. 第一次执行会自动尝试创建 `systemd` 服务 `wx_service` + +## 5. 发布行为 + +每次发布会执行: + +1. GitHub Actions 构建 Linux 二进制 +2. 上传到服务器 `/tmp/wx_service-` +3. 远程脚本执行: + - `git fetch` + `git reset --hard `(同步代码) + - 备份旧二进制到 `/www/wwwroot/wx_service/backups/` + - 原子替换 `dist/wx_service` + - 重启服务 + - 健康检查 `http://127.0.0.1:8080/healthz` +4. 若健康检查失败,自动回滚旧二进制并重启 + +## 6. 手动发布(应急) + +在服务器执行: + +```bash +cd /www/wwwroot/wx_service +APP_DIR=/www/wwwroot/wx_service \ +DIST_DIR=/www/wwwroot/wx_service/dist \ +SOURCE_BIN=/tmp/wx_service-manual \ +RELEASE_ID=manual-$(date +%Y%m%d%H%M%S) \ +SERVICE_NAME=wx_service \ +RUN_USER=www RUN_GROUP=www \ +PORT=8080 \ +SYNC_CODE=true DEPLOY_REF=main \ +INSTALL_SERVICE=true \ +bash scripts/ops/deploy_binary.sh +``` + +## 7. 回滚 + +```bash +ls -lt /www/wwwroot/wx_service/backups/ +cp -f /www/wwwroot/wx_service/backups/wx_service..bak /www/wwwroot/wx_service/dist/wx_service +chown www:www /www/wwwroot/wx_service/dist/wx_service +systemctl restart wx_service +``` + +若机器未使用 systemd,可改为: + +```bash +pkill -f /www/wwwroot/wx_service/dist/wx_service +su -s /bin/bash - www -c "cd /www/wwwroot/wx_service/dist && nohup ./wx_service >> /www/wwwlogs/wx_service.stdout.log 2>&1 &" +``` diff --git a/scripts/ops/deploy_binary.sh b/scripts/ops/deploy_binary.sh new file mode 100755 index 0000000..f5ef830 --- /dev/null +++ b/scripts/ops/deploy_binary.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +APP_DIR="${APP_DIR:-/www/wwwroot/wx_service}" +DIST_DIR="${DIST_DIR:-${APP_DIR}/dist}" +BIN_PATH="${BIN_PATH:-${DIST_DIR}/wx_service}" +SOURCE_BIN="${SOURCE_BIN:-}" +SERVICE_NAME="${SERVICE_NAME:-wx_service}" +PORT="${PORT:-8080}" +HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:${PORT}/healthz}" +RUN_USER="${RUN_USER:-www}" +RUN_GROUP="${RUN_GROUP:-www}" +RELEASE_ID="${RELEASE_ID:-manual-$(date +%Y%m%d%H%M%S)}" +KEEP_BACKUPS="${KEEP_BACKUPS:-5}" +SYNC_CODE="${SYNC_CODE:-false}" +DEPLOY_REF="${DEPLOY_REF:-main}" +INSTALL_SERVICE="${INSTALL_SERVICE:-true}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +health_check() { + local tries=30 + while [ "$tries" -gt 0 ]; do + if command -v curl >/dev/null 2>&1; then + if curl -fsS "$HEALTH_URL" >/dev/null; then + return 0 + fi + else + if wget -q -O - "$HEALTH_URL" >/dev/null; then + return 0 + fi + fi + tries=$((tries - 1)) + sleep 2 + done + return 1 +} + +restart_service() { + if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q "^${SERVICE_NAME}.service"; then + log "restarting systemd service: ${SERVICE_NAME}" + systemctl restart "$SERVICE_NAME" + else + log "systemd service not found, fallback to pkill + nohup" + pkill -f "$BIN_PATH" || true + su -s /bin/bash - "$RUN_USER" -c "cd '$DIST_DIR' && nohup '$BIN_PATH' >> /www/wwwlogs/${SERVICE_NAME}.stdout.log 2>&1 &" + fi +} + +create_service_if_needed() { + if [ "$INSTALL_SERVICE" != "true" ]; then + return 0 + fi + + if ! command -v systemctl >/dev/null 2>&1; then + return 0 + fi + + if systemctl list-unit-files | grep -q "^${SERVICE_NAME}.service"; then + return 0 + fi + + log "creating systemd service: ${SERVICE_NAME}" + cat > "/etc/systemd/system/${SERVICE_NAME}.service" <&2 + exit 1 +fi + +if [ ! -f "$SOURCE_BIN" ]; then + echo "binary not found: $SOURCE_BIN" >&2 + exit 1 +fi + +log "deploy start, release: ${RELEASE_ID}" +mkdir -p "$DIST_DIR" "$APP_DIR/backups" + +if [ "$SYNC_CODE" = "true" ] && [ -d "$APP_DIR/.git" ]; then + log "syncing code from github ref: ${DEPLOY_REF}" + git -C "$APP_DIR" fetch --all --prune + git -C "$APP_DIR" reset --hard "$DEPLOY_REF" +fi + +backup_path="" +if [ -f "$BIN_PATH" ]; then + backup_path="$APP_DIR/backups/wx_service.${RELEASE_ID}.bak" + cp -f "$BIN_PATH" "$backup_path" + log "backup created: $backup_path" +fi + +install -m 755 "$SOURCE_BIN" "${BIN_PATH}.new" +mv -f "${BIN_PATH}.new" "$BIN_PATH" +chown "$RUN_USER:$RUN_GROUP" "$BIN_PATH" || true + +if [ ! -f "$DIST_DIR/.env" ] && [ -f "$APP_DIR/.env" ]; then + cp -f "$APP_DIR/.env" "$DIST_DIR/.env" + chown "$RUN_USER:$RUN_GROUP" "$DIST_DIR/.env" || true +fi + +create_service_if_needed +restart_service + +if health_check; then + log "health check success: $HEALTH_URL" +else + log "health check failed, rollback" + if [ -n "$backup_path" ] && [ -f "$backup_path" ]; then + cp -f "$backup_path" "$BIN_PATH" + chown "$RUN_USER:$RUN_GROUP" "$BIN_PATH" || true + restart_service || true + fi + exit 1 +fi + +if [ -f "$SOURCE_BIN" ] && [[ "$SOURCE_BIN" == /tmp/* ]]; then + rm -f "$SOURCE_BIN" +fi + +if [ -d "$APP_DIR/backups" ]; then + ls -1t "$APP_DIR"/backups/wx_service.*.bak 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | xargs -r rm -f +fi + +log "deploy done"