Files
BYW-VoiceHub/patches/0002-ci-Gitea-Actions-Runner-Deploy.patch
T

812 lines
36 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
From 5d015a3529957b98b6b4719f1fad4f4d4c0a5fd4 Mon Sep 17 00:00:00 2001
From: HenryDu8133 <813367384@qq.com>
Date: Sun, 6 Sep 2026 08:44:11 +0800
Subject: [PATCH 02/58] =?UTF-8?q?ci:=20=E6=96=B0=E5=A2=9E=20Gitea=20Action?=
=?UTF-8?q?s=20=E9=83=A8=E7=BD=B2=E6=B5=81=E6=B0=B4=E7=BA=BF=20+=20Runner/?=
=?UTF-8?q?Deploy=20=E4=B8=80=E9=94=AE=E8=84=9A=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitea/workflows/deploy.yml | 321 ++++++++++++++++++++++++++---------
scripts/setup-deploy-user.sh | 159 +++++++++++++++++
scripts/setup-runner.sh | 211 +++++++++++++++++++++++
3 files changed, 609 insertions(+), 82 deletions(-)
create mode 100644 scripts/setup-deploy-user.sh
create mode 100644 scripts/setup-runner.sh
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index ca3da1f..e8d783b 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -1,164 +1,321 @@
-name: 构建并部署 VoiceHub
-run-name: ${{ gitea.actor }} 触发部署 #${{{{ gitea.run_number }}}}
+name: 构建 & 部署 BYW VoiceHub
+run-name: ${{ github.actor }} 触发部署 · Run ${{ github.run_number }}
on:
push:
- branches:
- - main
- - master
- # 允许在 Gitea 页面手动点「运行工作流」
+ branches: [main, master]
workflow_dispatch:
inputs:
deploy_target:
- description: 是否在构建后自动部署到服务器
+ description: 构建完成后是否自动部署到服务器
type: boolean
default: true
required: false
+ skip_build_cache:
+ description: 跳过 BuildKit 分层缓存,强制重新构建(排错用)
+ type: boolean
+ default: false
+ required: false
-# 同一时刻只跑一个部署,避免部署竞态
+# 同一时刻只允许一个部署在跑,避免冲突;如果新提交进来,取消前一个未完成的
concurrency:
group: deploy-production
- cancel-in-progress: false
+ cancel-in-progress: true
env:
- # ====== 按需修改(改成你的 Gitea 域名,不要 http:// 前缀) ======
- GITEA_REGISTRY: gitea.example.com
- # ====== 一般不改 ======
+ # ============================================================
+ # 👇👇👇 这里改成你自己的 Gitea 域名(不要 https:// 前缀,不要端口默认 443/80)
+ # 例:git.baoyiwai.com 或 gitea.example.com:3000
+ GITEA_HOST: gitea.example.com
+ # 👆👆👆
+ # ============================================================
IMAGE_NAME: byw-voicehub
DEPLOY_PATH: /opt/byw-voicehub
- NODE_VERSION: "22"
jobs:
# ============================================================
- # Job 1: 构建 & 推送 Docker 镜像到 Gitea 自带容器仓库
+ # Job 1: 校验构建环境 & 打包产物(npm ci + build,不依赖 Docker)
# ============================================================
- build-image:
- name: 构建 Docker 镜像
+ preflight:
+ name: Preflight 语法 & 构建检查
runs-on: ubuntu-latest
- outputs:
- image_tag: ${{ steps.meta.outputs.tag }}
- image_full: ${{ steps.meta.outputs.full }}
+ timeout-minutes: 15
steps:
- - name: 📥 拉取代码
+ - name: 📥 Checkout 代码
uses: actions/checkout@v4
with:
fetch-depth: 1
- - name: 🔧 设置 Node.js
+ - name: 🔧 安装 Node.js 22
uses: actions/setup-node@v4
with:
- node-version: ${{ env.NODE_VERSION }}
- registry-url: https://registry.npmmirror.com # 国内加速,不需要可删
+ node-version: "22"
+ cache: "npm"
- - name: 🧪 环境校验
+ - name: 🧪 安装依赖(npm ci,严格按锁文件)
run: |
set -euxo pipefail
- node -v
- npm -v
- npm ci --no-audit --no-fund
+ # 国内加速镜像(Runner 在国外可以删掉这两行)
+ npm config set registry https://registry.npmmirror.com || true
+ npm config set fund false
+ npm config set audit false
+ npm ci
- - name: 🔐 登录 Gitea 容器仓库
- uses: docker/login-action@v3
+ - name: 🔎 检查 package.json 没有 pnpm 残留
+ run: |
+ set -euo pipefail
+ if grep -qE '"packageManager"\s*:\s*"pnpm' package.json; then
+ echo "❌ package.json 里有 pnpm 包管理器声明,和用户要求冲突"
+ exit 1
+ fi
+ echo "✅ package.json 未发现 pnpm 声明"
+
+ - name: 🏗️ 本地 nuxt build(独立于 Docker 先验证代码是可构建的)
+ run: |
+ set -euxo pipefail
+ NODE_OPTIONS="--max-old-space-size=2048" npm run build
+ echo "✅ .output/ 生成成功:"
+ ls -la .output/ 2>/dev/null || true
+ du -sh .output 2>/dev/null || true
+
+ # ============================================================
+ # Job 2: 构建 Docker 镜像 → 推送到 Gitea 自带容器仓库
+ # ============================================================
+ build-image:
+ name: 构建并推送镜像
+ needs: preflight
+ runs-on: ubuntu-latest
+ timeout-minutes: 40
+ outputs:
+ image_tag: ${{ steps.meta.outputs.tag }}
+ image_full: ${{ steps.meta.outputs.full }}
+ image_latest: ${{ steps.meta.outputs.latest }}
+ steps:
+ - name: 📥 Checkout 代码
+ uses: actions/checkout@v4
with:
- registry: ${{ env.GITEA_REGISTRY }}
- username: ${{ gitea.repository_owner }}
- password: ${{ secrets.REGISTRY_TOKEN || gitea.token }}
+ fetch-depth: 1
- - name: 🏷️ 生成镜像标签
+ - name: 🏷️ 计算镜像 tag(日期 + 短 SHA)
id: meta
+ shell: bash
run: |
set -euo pipefail
- SHORT_SHA="${GITEA_SHA:0:7}"
+ SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
+ SHORT_SHA="${SHA:0:7}"
DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}"
- FULL="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
- LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
- echo "tag=${DATE_TAG}" >> "$GITEA_OUTPUT"
- echo "full=${FULL}" >> "$GITEA_OUTPUT"
- echo "latest=${LATEST}" >> "$GITEA_OUTPUT"
- echo "✅ 镜像 tag: ${DATE_TAG}"
- echo "✅ 镜像完整: ${FULL}"
-
- - name: 🐳 构建并推送镜像(BuildKit 多阶段 + 缓存)
+ IMG_FULL="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
+ IMG_LATEST="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:latest"
+ echo "tag=${DATE_TAG}" >> "$GITHUB_OUTPUT"
+ echo "full=${IMG_FULL}" >> "$GITHUB_OUTPUT"
+ echo "latest=${IMG_LATEST}" >> "$GITHUB_OUTPUT"
+ echo "======================================"
+ echo "🪪 镜像版本 tag : ${DATE_TAG}"
+ echo "🪪 镜像完整名 : ${IMG_FULL}"
+ echo "🪪 镜像 latest : ${IMG_LATEST}"
+ echo "======================================"
+
+ - name: 🔧 启用 QEMU(跨架构小概率用得到,保险)
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: linux/amd64
+
+ - name: 🔧 启用 Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ with:
+ install: true
+
+ - name: 🔐 登录 Gitea 容器仓库(Packages)
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITEA_HOST }}
+ # Secrets 里 REGISTRY_USER 没填就默认用触发构建的用户
+ username: ${{ secrets.REGISTRY_USER || github.actor }}
+ # Secrets 里 REGISTRY_TOKEN 优先,没填就用 Gitea Actions 自动注入的仓库级 token
+ password: ${{ secrets.REGISTRY_TOKEN || github.token }}
+
+ - name: 🐳 构建 & 推送(BuildKit 分层缓存,skip_build_cache=true 时禁用)
+ id: build_push
uses: docker/build-push-action@v6
+ env:
+ CACHE_REF: ${{ env.GITEA_HOST }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
with:
context: .
file: ./Dockerfile
+ platforms: linux/amd64
push: true
+ provenance: false
tags: |
${{ steps.meta.outputs.full }}
${{ steps.meta.outputs.latest }}
- cache-from: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
- cache-to: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
build-args: |
BUILDKIT_INLINE_CACHE=1
+ NODE_ENV=production
+ cache-from: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0}', env.CACHE_REF) }}
+ cache-to: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0},mode=max', env.CACHE_REF) }}
labels: |
- org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }}
- org.opencontainers.image.revision=${{ gitea.sha }}
+ org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
+ org.opencontainers.image.revision=${{ github.sha }}
+ org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
+
+ - name: 🧮 镜像体积 & 层信息(日志里看一下,避免镜像越来越大)
+ run: |
+ set -euxo pipefail
+ docker pull "${{ steps.meta.outputs.full }}" || true
+ docker image inspect "${{ steps.meta.outputs.full }}" --format='{{.Size}}' 2>/dev/null \
+ | awk '{printf "✅ 镜像体积:%.2f MB\n", $1/1024/1024}' || true
+ docker history --no-trunc "${{ steps.meta.outputs.full }}" 2>/dev/null | head -20 || true
# ============================================================
- # Job 2: SSH 到服务器拉取新镜像并重启
+ # Job 3: SSH 到服务器 → 拉新镜像 → 滚动重启
# ============================================================
deploy:
- name: 部署到服务器
+ name: 部署到生产服务器
needs: build-image
- if: ${{ !cancelled() && !failure() && (inputs.deploy_target == true || github.event_name == 'push') }}
+ # push 事件默认自动部署;手动触发时看 checkbox
+ if: ${{ !cancelled() && !failure() && (github.event_name == 'push' || inputs.deploy_target == true) }}
runs-on: ubuntu-latest
- environment: production
+ timeout-minutes: 25
+ environment:
+ name: production
+ url: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
steps:
- - name: 🌐 SSH 部署到生产服务器
- uses: https://github.com/appleboy/ssh-action@v1.2.0
+ - name: ⚠️ 检查部署所需 Secrets 是否都配置了
+ shell: bash
+ run: |
+ set -euo pipefail
+ MISSING=()
+ [ -n "${{ secrets.DEPLOY_HOST }}" ] || MISSING+=("DEPLOY_HOST")
+ [ -n "${{ secrets.DEPLOY_USER }}" ] || MISSING+=("DEPLOY_USER")
+ [ -n "${{ secrets.DEPLOY_SSH_KEY }}" ] || MISSING+=("DEPLOY_SSH_KEY")
+ if [ ${#MISSING[@]} -gt 0 ]; then
+ echo "❌ 缺少以下 Repository Secrets:${MISSING[*]}"
+ echo "👉 去 Gitea 仓库 → Settings → Secrets and variables → Actions 里添加"
+ exit 1
+ fi
+ echo "✅ 部署 Secrets 配置完整"
+
+ - name: 🚀 一键滚动部署(SSH)
+ uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.DEPLOY_HOST }}
+ port: ${{ secrets.DEPLOY_PORT || 22 }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
- port: ${{ secrets.DEPLOY_PORT || 22 }}
- script_stop: true
+ # 部署可能慢,给足 30 分钟
command_timeout: 30m
+ # 出错立刻停,不继续往下跑
+ script_stop: true
script: |
set -euxo pipefail
- # 1) 首次部署时自动创建目录 & 空的 docker-compose.yml/.env
- mkdir -p "${DEPLOY_PATH}"
- cd "${DEPLOY_PATH}"
+ # ---------- 0. 基本参数 ----------
+ IMAGE_NEW="${{ needs.build-image.outputs.image_full }}"
+ IMAGE_LATEST="${{ needs.build-image.outputs.image_latest }}"
+ DEPLOY_DIR="${DEPLOY_PATH}"
+ COMPOSE="docker compose"
+
+ echo "=============================================="
+ echo "🚢 本次部署镜像: ${IMAGE_NEW}"
+ echo "🗂️ 部署目录 : ${DEPLOY_DIR}"
+ echo "👤 执行用户 : $(whoami)"
+ echo "=============================================="
- # 2) 登录 Gitea 镜像仓库(用一个仅 pull 权限的 token 更安全)
- echo "${{ secrets.REGISTRY_PULL_TOKEN || gitea.token }}" \
- | docker login "${GITEA_REGISTRY}" -u "${{ secrets.REGISTRY_PULL_USER || gitea.repository_owner }}" --password-stdin 2>/dev/null || true
+ # ---------- 1. 确保部署目录存在 ----------
+ mkdir -p "${DEPLOY_DIR}"
+ cd "${DEPLOY_DIR}"
- # 3) 拉最新镜像
- IMAGE_LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
- docker pull "$IMAGE_LATEST"
+ # ---------- 2. 登录 Gitea 容器仓库(用 pull token,最小权限) ----------
+ if [ -n "${{ secrets.REGISTRY_PULL_TOKEN }}" ] && [ -n "${{ secrets.REGISTRY_PULL_USER }}" ]; then
+ echo "🔐 使用专用 pull 凭证登录 Gitea Container Registry"
+ echo "${{ secrets.REGISTRY_PULL_TOKEN }}" \
+ | docker login "${GITEA_HOST}" \
+ -u "${{ secrets.REGISTRY_PULL_USER }}" \
+ --password-stdin 2>/dev/null || true
+ fi
+
+ # ---------- 3. 先拉镜像(提前下好,后面 up 不会卡) ----------
+ echo "⬇️ 开始拉取新镜像 ${IMAGE_NEW} ..."
+ docker pull "${IMAGE_NEW}"
+ docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
+ echo "✅ 新镜像拉取完成,并已打 latest tag"
- # 4) 写一个最小的 docker-compose.override.yml,指向刚拉的镜像
- # (避免改动服务器上已配置好的 docker-compose.yml 和 .env)
- cat > docker-compose.override.yml <<'YAML'
+ # ---------- 4. 生成 / 更新 docker-compose.override.yml ----------
+ # 指向新的精确 tag(不是 latest,出问题能快速回滚)
+ cat > docker-compose.override.yml <<YAML
services:
voicehub:
- image: __REPLACE_WITH_IMAGE__
+ image: ${IMAGE_NEW}
YAML
- sed -i "s|__REPLACE_WITH_IMAGE__|${IMAGE_LATEST}|g" docker-compose.override.yml
+ echo "📝 docker-compose.override.yml 已写入,指定镜像=${IMAGE_NEW}"
- # 5) 启动(如果没有 docker-compose.yml 就自动从你的仓库默认模板里下载一份)
+ # ---------- 5. 没有 docker-compose.yml? 自动从仓库下一份模板 ----------
if [ ! -f docker-compose.yml ]; then
- echo "ℹ️ 首次部署:写入默认 docker-compose.yml 模板(请稍后在服务器上编辑 .env)"
- curl -fsSL "${{ gitea.server_url }}/${{ gitea.repository }}/raw/branch/main/docker-compose.yml" \
- -o docker-compose.yml || echo "下载失败,跳过(你需要手动上传 docker-compose.yml)"
+ echo "ℹ️ 未发现 docker-compose.yml,自动从仓库模板下载"
+ TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY:-${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
+ echo " ↓ URL=${TEMPLATE_URL}"
+ curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
+ || (
+ echo "⚠️ 自动下载模板失败,跳过(你需要手动上传 docker-compose.yml 和 .env)"
+ # 不 exit,因为用户可能已经有一套自定义部署
+ )
+ fi
+
+ # ---------- 6. 没有 .env? 给一份占位(方便首次部署发现问题) ----------
+ if [ ! -f .env ]; then
+ echo "================================================================="
+ echo "⚠️ WARNING:${DEPLOY_DIR}/.env 不存在!!!"
+ echo "👉 第一次部署请先在服务器上创建 .env(至少填 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
+ echo "👉 参考仓库根的 .env.example 或部署文档"
+ echo "================================================================="
+ # 不退出,因为 compose 会用 environment 默认值尝试启动(只是大概率连不上 DB)
fi
- # 6) 健康拉取 + 滚动重启(先拉好再 up,downtime 最小)
- docker compose pull --quiet voicehub 2>/dev/null || true
- docker compose up -d --remove-orphans
+ # ---------- 7. 滚动启动(如果只有 voicehub 单服务,不会动 postgres/redis) ----------
+ echo "🔄 docker compose up -d(只重建变更过的 voicehub)..."
+ ${COMPOSE} up -d --remove-orphans
- # 7) 等健康检查通过,最多等 120s
- for i in $(seq 1 24); do
- if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
- echo "✅ 容器健康"
+ # ---------- 8. 等待应用变 healthy,最多 2 分钟 ----------
+ MAX_WAIT=24 # 24 × 5s = 2min
+ WAITED=0
+ echo "⏳ 等待容器 healthcheck 通过(最多 120s)..."
+ while [ ${WAITED} -lt ${MAX_WAIT} ]; do
+ STATUS="$(${COMPOSE} ps --format json 2>/dev/null || true)"
+ if echo "${STATUS}" | grep -q '"Health":"healthy"'; then
+ echo "✅ voicehub 容器健康检查通过"
break
fi
+ # 同时也检查是否已经 running 但健康检查还没跑完(防止 healthcheck 写太严误判)
+ if echo "${STATUS}" | grep -q '"State":"running"'; then
+ :
+ else
+ # 连 running 都不是 → 直接打印日志末尾 60 行帮忙排错
+ echo "⚠️ voicehub 还没 running,当前状态:"
+ ${COMPOSE} ps || true
+ fi
sleep 5
+ WAITED=$((WAITED + 1))
done
+ if [ ${WAITED} -ge ${MAX_WAIT} ]; then
+ echo "⚠️ 等待超时,下面是最近 120 行 voicehub 日志:"
+ ${COMPOSE} logs --tail=120 voicehub || true
+ exit 1
+ fi
- # 8) 清理旧镜像(只留最近 2 个 tag,省磁盘)
+ # ---------- 9. 清理旧镜像(保留最近 2 个 tag + latest) ----------
+ echo "🧹 清理 3 天前的旧镜像 / 悬空层,防止磁盘爆"
docker image prune -af --filter "until=72h" || true
+ docker builder prune -af || true
- echo "🎉 部署完成!镜像=${{ needs.build-image.outputs.image_full }}"
+ # ---------- 10. 回滚指引 ----------
+ echo ""
+ echo "================================================================="
+ echo "🎉 部署成功!访问: ${{ secrets.NUXT_PUBLIC_SITE_URL || '请查看你的站点 URL' }}"
+ echo "🔁 如需紧急回滚到上一个版本,在服务器执行:"
+ echo " cd ${DEPLOY_DIR}"
+ echo " # 1) 先看有哪些 tag:"
+ echo " docker images | grep ${IMAGE_NAME} | head -5"
+ echo " # 2) 选一个老 tag 填到 override:"
+ echo " echo 'services:\n voicehub:\n image: ${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:<旧TAG>' > docker-compose.override.yml"
+ echo " docker compose up -d"
+ echo "================================================================="
diff --git a/scripts/setup-deploy-user.sh b/scripts/setup-deploy-user.sh
new file mode 100644
index 0000000..38ac0b1
--- /dev/null
+++ b/scripts/setup-deploy-user.sh
@@ -0,0 +1,159 @@
+#!/usr/bin/env bash
+# ============================================================================
+# setup-deploy-user.sh
+# 在「生产服务器」上一键做好 CI 自动部署所需的系统配置:
+# 1) 新建 deploy 用户(低权限,禁止密码登录,只能 key 登录)
+# 2) 生成 ed25519 SSH 密钥对(私钥你粘去 Gitea Secrets DEPLOY_SSH_KEY)
+# 3) 加入 docker 组,允许 docker compose 操作
+# 4) 给 /opt/byw-voicehub 目录读写权限(setfacl)
+# 5) 打印私钥内容 & 公钥指纹,便于你检查和复制
+#
+# 用法(在生产服务器上,sudo 运行):
+# sudo bash scripts/setup-deploy-user.sh [--deploy-dir /opt/byw-voicehub]
+# ============================================================================
+set -euo pipefail
+
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[1;36m'; NC='\033[0m'
+info(){ echo -e "${GREEN}[INFO]${NC} $*"; }
+warn(){ echo -e "${YELLOW}[WARN]${NC} $*"; }
+err(){ echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
+ok(){ echo -e "${CYAN}[ OK ]${NC} $*"; }
+
+DEPLOY_DIR="/opt/byw-voicehub"
+DEPLOY_USER="deploy"
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
+ --user) DEPLOY_USER="$2"; shift 2 ;;
+ -h|--help)
+ sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
+ exit 0
+ ;;
+ *) err "未知参数 $1(--help 查看帮助)" ;;
+ esac
+done
+
+[[ $EUID -eq 0 ]] || err "请用 sudo 运行本脚本"
+command -v docker >/dev/null 2>&1 || err "先装完 Docker 再跑本脚本(路径一的教程里有)"
+command -v setfacl >/dev/null 2>&1 || {
+ warn "没装 acl 工具,现在 apt install"
+ apt-get update && apt-get install -y acl
+}
+
+# -------- 1. 建用户 --------
+if ! id -u "${DEPLOY_USER}" >/dev/null 2>&1; then
+ useradd --create-home --shell /usr/bin/bash --disabled-password "${DEPLOY_USER}"
+ ok "新建用户:${DEPLOY_USER}(无密码,仅 SSH key 登录)"
+else
+ info "用户 ${DEPLOY_USER} 已存在,跳过创建"
+fi
+
+# -------- 2. 加入 docker 组 --------
+usermod -aG docker "${DEPLOY_USER}"
+ok "${DEPLOY_USER} 加入 docker 组(有权限执行 docker compose)"
+
+# -------- 3. 生成 SSH 密钥(无密码) --------
+SSH_DIR="/home/${DEPLOY_USER}/.ssh"
+mkdir -p "${SSH_DIR}"
+chmod 700 "${SSH_DIR}"
+chown "${DEPLOY_USER}:${DEPLOY_USER}" "${SSH_DIR}"
+
+KEY_PATH="${SSH_DIR}/id_ed25519"
+if [[ ! -f "${KEY_PATH}" ]]; then
+ sudo -u "${DEPLOY_USER}" ssh-keygen -t ed25519 \
+ -C "gitea-actions-deploy-$(hostname)" \
+ -f "${KEY_PATH}" \
+ -N "" >/dev/null
+ ok "SSH ed25519 密钥对生成:${KEY_PATH}"
+else
+ info "密钥对已存在,跳过生成:${KEY_PATH}"
+fi
+
+# 公钥加入 authorized_keys(允许持对应私钥的人以 deploy 用户 ssh 登录)
+AUTH_KEYS="${SSH_DIR}/authorized_keys"
+touch "${AUTH_KEYS}"
+PUBKEY_CONTENT="$(cat "${KEY_PATH}.pub")"
+if ! grep -qF "${PUBKEY_CONTENT}" "${AUTH_KEYS}" 2>/dev/null; then
+ echo "${PUBKEY_CONTENT}" >> "${AUTH_KEYS}"
+ ok "公钥已写入 authorized_keys"
+fi
+chmod 600 "${AUTH_KEYS}"
+chmod 644 "${KEY_PATH}.pub"
+chmod 600 "${KEY_PATH}"
+chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "${SSH_DIR}"
+
+# -------- 4. sshd 加固(只允许公钥登录;别让 root 密码登录;deploy 禁止转发)--------
+SSHD_SNIPPET="/etc/ssh/sshd_config.d/99-deploy-user.conf"
+if [[ ! -f "${SSHD_SNIPPET}" ]]; then
+ mkdir -p /etc/ssh/sshd_config.d
+ cat > "${SSHD_SNIPPET}" <<SSHD
+# 针对 deploy 用户:仅允许公钥登录,禁用 TCP 转发等
+Match User ${DEPLOY_USER}
+ PasswordAuthentication no
+ PubkeyAuthentication yes
+ PermitTTY yes
+ AllowTcpForwarding no
+ X11Forwarding no
+SSHD
+ systemctl reload sshd 2>/dev/null || service ssh reload || true
+ ok "sshd 配置已加固(deploy 只允许公钥登录,并禁用端口转发)"
+fi
+
+# -------- 5. 创建部署目录并给 deploy 权限 --------
+mkdir -p "${DEPLOY_DIR}"
+# 既设组权限,又加 ACL,确保后续新建文件也有
+setfacl -R -m "u:${DEPLOY_USER}:rwX" "${DEPLOY_DIR}"
+setfacl -R -d -m "u:${DEPLOY_USER}:rwX" "${DEPLOY_DIR}"
+ok "部署目录 ${DEPLOY_DIR} 已给 ${DEPLOY_USER} 完整读写权限(+ 新建文件继承权限)"
+
+# 如果目录里有 compose/环境文件,顺便改一下属组,避免「root 建的文件 deploy 改不了」
+chgrp -R docker "${DEPLOY_DIR}" 2>/dev/null || true
+chmod -R g+rwX "${DEPLOY_DIR}" 2>/dev/null || true
+
+# -------- 6. 验证 docker 是否真能用(不报错)--------
+DOCKER_RC=0
+sudo -u "${DEPLOY_USER}" docker ps >/dev/null 2>&1 || DOCKER_RC=$?
+if [[ ${DOCKER_RC} -eq 0 ]]; then
+ ok "✅ ${DEPLOY_USER} 可以执行 docker 命令"
+else
+ warn "⚠️ ${DEPLOY_USER} 暂时跑不了 docker(可能需要重新登录一次刷新组),部署时不会有影响(但验证时会再触发一次)"
+fi
+
+# -------- 7. 打印信息 --------
+FINGERPRINT="$(ssh-keygen -l -f "${KEY_PATH}.pub" 2>/dev/null || true)"
+PUBKEY_CONTENT="$(cat "${KEY_PATH}.pub")"
+PRIVATE_CONTENT="$(cat "${KEY_PATH}")"
+
+clear
+echo -e "${GREEN}================================================================================${NC}"
+echo -e "${GREEN} 🎉 部署用户 & SSH 密钥 配置完成!${NC}"
+echo -e "${GREEN}================================================================================${NC}"
+echo -e " 👤 部署用户名 :${CYAN}${DEPLOY_USER}${NC}"
+echo -e " 📂 部署目录 :${CYAN}${DEPLOY_DIR}${NC}"
+echo -e " 🔑 私钥文件路径 :${CYAN}${KEY_PATH}${NC}"
+echo -e " 🧾 公钥指纹 :${FINGERPRINT}"
+echo -e " 🧾 公钥内容(方便核对):"
+echo -e " ${PUBKEY_CONTENT}"
+echo ""
+echo -e "${YELLOW}================================================================================${NC}"
+echo -e "${YELLOW} ⚠️ 下面这一整段是【私钥】,完整复制(从 BEGIN 到 END 包括两行)到:${NC}"
+echo -e "${YELLOW} Gitea 仓库 → Settings → Secrets and variables → Actions → New Secret${NC}"
+echo -e "${YELLOW} Name = DEPLOY_SSH_KEY${NC}"
+echo -e "${YELLOW} Value = 粘贴下面一整段(包括 -----BEGIN/END-----)${NC}"
+echo -e "${YELLOW}================================================================================${NC}"
+echo ""
+echo "${PRIVATE_CONTENT}"
+echo ""
+echo -e "${YELLOW}================================================================================${NC}"
+echo -e "${YELLOW} ⚠️ 上面就是 DEPLOY_SSH_KEY,不要在任何地方贴到别的文档或聊天。看完 Ctrl+L 清屏。${NC}"
+echo -e "${YELLOW}================================================================================${NC}"
+echo ""
+echo -e "${CYAN}👉 同时你还需要补以下几个 Repository Secrets(都在同一页添加):${NC}"
+echo -e " • DEPLOY_HOST = 你的服务器公网 IP 或域名"
+echo -e " • DEPLOY_PORT = SSH 端口(一般 22)"
+echo -e " • DEPLOY_USER = ${DEPLOY_USER}(就是这个用户名)"
+echo -e " • REGISTRY_TOKEN = 你的 Gitea 个人访问令牌(Package 读写权限)"
+echo -e " • REGISTRY_PULL_USER = 同 REGISTRY_USER(服务器上拉镜像用,一般和 REGISTRY_USER 一样)"
+echo -e " • REGISTRY_PULL_TOKEN= 上面令牌同一个或新建一个仅 pull 权限的更安全"
+echo -e " • NUXT_PUBLIC_SITE_URL = https://你的域名.com(用来显示部署成功后的跳转按钮)"
+echo ""
diff --git a/scripts/setup-runner.sh b/scripts/setup-runner.sh
new file mode 100644
index 0000000..bc424e7
--- /dev/null
+++ b/scripts/setup-runner.sh
@@ -0,0 +1,211 @@
+#!/usr/bin/env bash
+# ============================================================================
+# setup-runner.sh —— 一键安装并注册 Gitea Actions Runner(act_runner)
+#
+# 使用方法:
+# ① 先在你的 Gitea 仓库页面 → Settings → Actions → Runners → 「Create Runner」
+# 选「Linux + amd64」,它会给你一条注册命令,里面有一串 registration token,
+# 格式像: GFxxxxxxxxxxxxxxxxxxxx ,复制它。
+# ② 再在你要跑 Runner 的机器上执行:
+# curl -fsSL https://<你的-gitea>/<仓库>/-/raw/branch/main/scripts/setup-runner.sh \
+# | sudo bash -s -- --gitea https://<你的-gitea> --token GFxxxxxxxxxxxxxxxxxxxx
+#
+# 或者直接本地执行:
+# sudo bash scripts/setup-runner.sh \
+# --gitea https://git.baoyiwai.com \
+# --token GFxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# ============================================================================
+set -euo pipefail
+
+# ---------- 颜色 ----------
+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} $*"; }
+err() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
+
+# ---------- 解析参数 ----------
+GITEA_URL=""
+REG_TOKEN=""
+RUNNER_NAME="runner-$(hostname | tr '[:upper:]' '[:lower:]')"
+RUNNER_USER="gitrunner"
+INSTALL_DIR="/opt/act_runner"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --gitea) GITEA_URL="${2}"; shift 2 ;;
+ --token) REG_TOKEN="${2}"; shift 2 ;;
+ --name) RUNNER_NAME="${2}"; shift 2 ;;
+ --user) RUNNER_USER="${2}"; shift 2 ;;
+ --dir) INSTALL_DIR="${2}"; shift 2 ;;
+ -h|--help)
+ sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
+ exit 0
+ ;;
+ *) err "未知参数:$1(--help 查看帮助)" ;;
+ esac
+done
+
+[[ -n "${GITEA_URL}" ]] || err "必须传 --gitea https://你的-gitea-地址"
+[[ -n "${REG_TOKEN}" ]] || err "必须传 --token GF……(Gitea 页面里拿的注册 token)"
+[[ $EUID -eq 0 ]] || err "请用 sudo 运行本脚本(需要写 /opt / /etc/systemd/system)"
+
+# 去掉末尾 /
+GITEA_URL="${GITEA_URL%/}"
+
+# ---------- 0. 架构检测 ----------
+ARCH="$(uname -m)"
+case "${ARCH}" in
+ x86_64|amd64) PLATFORM="linux-amd64" ;;
+ aarch64|arm64) PLATFORM="linux-arm64" ;;
+ armv7l) PLATFORM="linux-arm" ;;
+ *) err "暂不支持的架构:${ARCH}(需要 x86_64 / aarch64 / armv7)" ;;
+esac
+info "检测到架构:${ARCH} → 下载版本:${PLATFORM}"
+
+# ---------- 1. 安装依赖:docker(Runner 要用 Docker-in-Docker 执行 buildx 等)----------
+if ! command -v docker >/dev/null 2>&1; then
+ info "检测到没装 Docker,现在用官方一键脚本安装(国内自动换清华源)..."
+ curl -fsSL https://get.docker.com -o /tmp/get-docker.sh || err "下载 docker-install 脚本失败"
+ if curl -sI https://mirrors.tuna.tsinghua.edu.cn >/dev/null; then
+ bash /tmp/get-docker.sh --mirror Aliyun || bash /tmp/get-docker.sh
+ else
+ bash /tmp/get-docker.sh
+ fi
+ systemctl enable --now docker
+ info "✅ Docker 安装完成:$(docker --version)"
+else
+ info "✅ Docker 已存在:$(docker --version)"
+fi
+
+# ---------- 2. 建运行用户 gitrunner,加入 docker 组 ----------
+if ! id -u "${RUNNER_USER}" >/dev/null 2>&1; then
+ useradd -r -s /usr/sbin/nologin -d "${INSTALL_DIR}" "${RUNNER_USER}"
+ info "✅ 新建系统用户 ${RUNNER_USER}"
+else
+ info "ℹ️ 用户 ${RUNNER_USER} 已存在,跳过创建"
+fi
+usermod -aG docker "${RUNNER_USER}"
+info "✅ 把 ${RUNNER_USER} 加入 docker 组(允许操作 Docker daemon)"
+
+mkdir -p "${INSTALL_DIR}"
+chown -R "${RUNNER_USER}:${RUNNER_USER}" "${INSTALL_DIR}"
+
+# ---------- 3. 下载最新 act_runner ----------
+info "⬇️ 正在拉取 act_runner 最新 release 元数据..."
+LATEST_JSON="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/act_runner/releases/latest || true)"
+if [[ -z "${LATEST_JSON}" ]]; then
+ warn "gitea.com 不通,改用硬编码版本 0.2.10(你之后可手动升级)"
+ VERSION="0.2.10"
+else
+ VERSION="$(echo "${LATEST_JSON}" | grep -oE '"tag_name":\s*"[^"]+"' | head -1 | cut -d'"' -f4 | sed 's/^v//')"
+ [[ -n "${VERSION}" ]] || VERSION="0.2.10"
+fi
+BIN_URL="https://dl.gitea.com/act_runner/${VERSION}/act_runner-${VERSION}-${PLATFORM}"
+info "⬇️ 下载 act_runner v${VERSION}:${BIN_URL}"
+curl -fsSL "${BIN_URL}" -o /usr/local/bin/act_runner \
+ || err "下载失败。请手动打开 ${BIN_URL} 看是否 404,然后换个版本"
+chmod +x /usr/local/bin/act_runner
+info "✅ act_runner 已安装到 /usr/local/bin,版本:$(act_runner --version 2>&1 || true)"
+
+# ---------- 4. 注册 Runner ----------
+cd "${INSTALL_DIR}"
+info "🔗 开始注册 Runner 到 ${GITEA_URL}(名称:${RUNNER_NAME})"
+
+# 切到 gitrunner 用户注册,避免 root 跑(安全)
+rm -f .runner 2>/dev/null || true
+sudo -u "${RUNNER_USER}" env HOME="${INSTALL_DIR}" \
+ act_runner register \
+ --instance "${GITEA_URL}" \
+ --token "${REG_TOKEN}" \
+ --name "${RUNNER_NAME}" \
+ --labels "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" \
+ --no-interactive \
+ || err "注册失败!常见原因:\n 1) token 不对(复制时别带空格)\n 2) Runner 机器出不了网到 ${GITEA_URL}\n 3) Gitea 站点时间和这台机器差太远(证书验证失败)"
+
+chown "${RUNNER_USER}:${RUNNER_USER}" .runner 2>/dev/null || true
+
+# ---------- 5. 生成配置文件(建议开启 docker.host 特权)----------
+CONFIG_FILE="${INSTALL_DIR}/config.yaml"
+if [[ ! -f "${CONFIG_FILE}" ]]; then
+ cat > "${CONFIG_FILE}" <<'YAML'
+log:
+ level: info
+runner:
+ file: .runner
+ capacity: 2 # 同一台 Runner 最多同时跑 2 个 Job
+ envs:
+ GOPROXY: https://goproxy.cn,direct
+ NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
+ timeout: 3h
+cache:
+ enabled: true
+ dir: /opt/act_runner/cache
+container:
+ network: "bridge"
+ privileged: true # build-push-action 用到 Docker daemon
+ options: |
+ --add-host "host.docker.internal:host-gateway"
+YAML
+ chown "${RUNNER_USER}:${RUNNER_USER}" "${CONFIG_FILE}"
+ mkdir -p "${INSTALL_DIR}/cache"
+ chown -R "${RUNNER_USER}:${RUNNER_USER}" "${INSTALL_DIR}/cache"
+ info "✅ 默认 config 写入 ${CONFIG_FILE}"
+fi
+
+# ---------- 6. 写 systemd 服务,开机自启 ----------
+UNIT_FILE="/etc/systemd/system/gitea-act-runner.service"
+cat > "${UNIT_FILE}" <<SYSTEMD
+[Unit]
+Description=Gitea Actions Runner (${RUNNER_NAME})
+After=network.target docker.service
+Requires=docker.service
+
+[Service]
+Type=simple
+User=${RUNNER_USER}
+Group=${RUNNER_USER}
+WorkingDirectory=${INSTALL_DIR}
+Environment=HOME=${INSTALL_DIR}
+ExecStart=/usr/local/bin/act_runner daemon --config ${CONFIG_FILE}
+Restart=always
+RestartSec=5
+KillMode=mixed
+TimeoutStopSec=30
+
+# 安全加固
+NoNewPrivileges=true
+PrivateTmp=true
+ProtectSystem=strict
+ReadWritePaths=${INSTALL_DIR} /var/run/docker.sock /tmp /var/tmp
+
+[Install]
+WantedBy=multi-user.target
+SYSTEMD
+
+systemctl daemon-reload
+systemctl enable --now gitea-act-runner
+info "✅ systemd 服务已写入并启动:${UNIT_FILE}"
+
+sleep 3 # 给 daemon 点时间注册
+info "📌 当前 Runner 状态:"
+systemctl --no-pager status gitea-act-runner | head -15 || true
+info "📌 最近 30 行日志(验证 OK):"
+journalctl --no-pager -u gitea-act-runner --since "2 minutes ago" -n 30 || true
+
+echo ""
+echo -e "${GREEN}===========================================================================${NC}"
+echo -e "${GREEN} 🎉 Gitea Actions Runner 安装 & 注册 & 开机自启 全部完成!${NC}"
+echo -e "${GREEN}===========================================================================${NC}"
+echo " ✅ Runner 名称 : ${RUNNER_NAME}"
+echo " ✅ 运行用户 : ${RUNNER_USER}"
+echo " ✅ 安装目录 : ${INSTALL_DIR}"
+echo " ✅ 绑定 Gitea : ${GITEA_URL}"
+echo ""
+echo " 👉 下一步验证(建议立刻做):"
+echo " 1. 浏览器打开:${GITEA_URL} → 仓库 → Settings → Actions → Runners"
+echo " 应该能看到一个 Runner,状态 = Idle"
+echo " 2. 回到终端,模拟跑一个 workflow 没问题就彻底大功告成!"
+echo " 👉 常用命令:"
+echo " systemctl {start|stop|restart|status} gitea-act-runner"
+echo " journalctl -u gitea-act-runner -f # 实时看日志"
+echo ""
--
2.45.1.windows.1