ci: align frontend deploy with wx_service secrets-driven flow
This commit is contained in:
+4
-1
@@ -1,2 +1,5 @@
|
|||||||
# 生产环境(前后端同域部署时使用相对路径)
|
# 生产环境 API(前后端同域部署时使用相对路径)
|
||||||
VITE_API_BASE_URL=/
|
VITE_API_BASE_URL=/
|
||||||
|
|
||||||
|
# 生产环境前端路由基路径
|
||||||
|
VITE_PUBLIC_BASE=/panel/
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
name: deploy-admin-frontend-prod
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: admin-frontend-prod
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Package dist
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
tar -czf "/tmp/admin-frontend-${GITHUB_SHA}.tar.gz" -C dist .
|
||||||
|
|
||||||
|
- name: Validate required secrets
|
||||||
|
env:
|
||||||
|
PROD_SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
|
||||||
|
PROD_HOST: ${{ secrets.PROD_HOST }}
|
||||||
|
PROD_PORT: ${{ secrets.PROD_PORT }}
|
||||||
|
PROD_USER: ${{ secrets.PROD_USER }}
|
||||||
|
ADMIN_WEB_ROOT: ${{ secrets.ADMIN_WEB_ROOT }}
|
||||||
|
ADMIN_WEB_USER: ${{ secrets.ADMIN_WEB_USER }}
|
||||||
|
ADMIN_WEB_GROUP: ${{ secrets.ADMIN_WEB_GROUP }}
|
||||||
|
ADMIN_KEEP_BACKUPS: ${{ secrets.ADMIN_KEEP_BACKUPS }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
for key in PROD_SSH_KEY PROD_HOST PROD_PORT PROD_USER ADMIN_WEB_ROOT ADMIN_WEB_USER ADMIN_WEB_GROUP ADMIN_KEEP_BACKUPS; do
|
||||||
|
if [ -z "${!key}" ]; then
|
||||||
|
echo "Missing required secret: ${key}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Prepare SSH
|
||||||
|
env:
|
||||||
|
SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
|
||||||
|
HOST: ${{ secrets.PROD_HOST }}
|
||||||
|
PORT: ${{ secrets.PROD_PORT }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
ssh-keyscan -p "$PORT" "$HOST" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Upload build artifact to server
|
||||||
|
env:
|
||||||
|
HOST: ${{ secrets.PROD_HOST }}
|
||||||
|
PORT: ${{ secrets.PROD_PORT }}
|
||||||
|
USER: ${{ secrets.PROD_USER }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=15 -o ServerAliveInterval=10 -o ServerAliveCountMax=6"
|
||||||
|
scp ${SSH_OPTS} -P "$PORT" \
|
||||||
|
"/tmp/admin-frontend-${GITHUB_SHA}.tar.gz" \
|
||||||
|
"${USER}@${HOST}:/tmp/admin-frontend-${GITHUB_SHA}.tar.gz"
|
||||||
|
|
||||||
|
- name: Deploy static files on server
|
||||||
|
env:
|
||||||
|
HOST: ${{ secrets.PROD_HOST }}
|
||||||
|
PORT: ${{ secrets.PROD_PORT }}
|
||||||
|
USER: ${{ secrets.PROD_USER }}
|
||||||
|
WEB_ROOT: ${{ secrets.ADMIN_WEB_ROOT }}
|
||||||
|
WEB_USER: ${{ secrets.ADMIN_WEB_USER }}
|
||||||
|
WEB_GROUP: ${{ secrets.ADMIN_WEB_GROUP }}
|
||||||
|
KEEP_BACKUPS: ${{ secrets.ADMIN_KEEP_BACKUPS }}
|
||||||
|
HEALTHCHECK_URL: ${{ secrets.ADMIN_HEALTHCHECK_URL }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=15 -o ServerAliveInterval=10 -o ServerAliveCountMax=6"
|
||||||
|
|
||||||
|
ssh ${SSH_OPTS} -p "$PORT" "${USER}@${HOST}" \
|
||||||
|
"DEPLOY_TAR='/tmp/admin-frontend-${GITHUB_SHA}.tar.gz' \
|
||||||
|
TARGET_DIR='${WEB_ROOT}' \
|
||||||
|
RELEASE_ID='${GITHUB_SHA}' \
|
||||||
|
RUN_USER='${WEB_USER}' \
|
||||||
|
RUN_GROUP='${WEB_GROUP}' \
|
||||||
|
KEEP_BACKUPS='${KEEP_BACKUPS}' \
|
||||||
|
HEALTHCHECK_URL='${HEALTHCHECK_URL}' \
|
||||||
|
bash -s" < scripts/ops/deploy_static.sh
|
||||||
@@ -32,3 +32,20 @@ npm run build
|
|||||||
- 会员、保质期、去水印、系统设置接口联调
|
- 会员、保质期、去水印、系统设置接口联调
|
||||||
- 会员管理的记录详情与批量操作
|
- 会员管理的记录详情与批量操作
|
||||||
- 系统设置中的操作日志查询
|
- 系统设置中的操作日志查询
|
||||||
|
|
||||||
|
## 自动构建与发布(GitHub Actions)
|
||||||
|
|
||||||
|
已新增生产自动发布流程:
|
||||||
|
- 工作流:`.github/workflows/deploy-prod.yml`
|
||||||
|
- 远程发布脚本:`scripts/ops/deploy_static.sh`
|
||||||
|
- 详细说明:`docs/deploy_ci.md`
|
||||||
|
|
||||||
|
部署参数全部走 GitHub Secrets(不使用脚本默认值),并与 `wx_service` 对齐:
|
||||||
|
- `PROD_HOST=115.159.198.14`
|
||||||
|
- `PROD_PORT=22`
|
||||||
|
- `PROD_USER=root`
|
||||||
|
- `ADMIN_WEB_ROOT=/www/wwwroot/wx_service/web/admin-frontend/panel`
|
||||||
|
|
||||||
|
密钥说明:
|
||||||
|
- `~/.ssh/id_ed25519.pub` 是公钥(放服务器)
|
||||||
|
- GitHub Secret `PROD_SSH_KEY` 需填写私钥 `~/.ssh/id_ed25519`
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 生产部署(GitHub Actions,前端静态站点)
|
||||||
|
|
||||||
|
本文档用于 `admin-frontend` 的自动化构建发布:
|
||||||
|
- 触发:`main` 分支 push 或手动触发
|
||||||
|
- 流程:GitHub Actions 构建 `dist` -> 通过 SSH 上传到服务器 -> 远程脚本覆盖发布
|
||||||
|
- 特点:参考 `wx_service` 部署流程,全部参数由 GitHub Secrets 驱动
|
||||||
|
|
||||||
|
## 0. 与 wx_service 对齐
|
||||||
|
|
||||||
|
- 使用同一台服务器:`115.159.198.14`
|
||||||
|
- SSH 用户:`root`
|
||||||
|
- 可复用 `wx_service` 的 SSH 密钥体系
|
||||||
|
- 不使用脚本内默认服务器参数,统一由仓库 Secrets 注入
|
||||||
|
|
||||||
|
## 1. Nginx 路径约定(当前配置)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location ^~ /panel/ {
|
||||||
|
root /www/wwwroot/wx_service/web/admin-frontend;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /panel/index.html;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
对应发布目录:
|
||||||
|
- `/www/wwwroot/wx_service/web/admin-frontend/panel`
|
||||||
|
|
||||||
|
## 2. 必填 GitHub Secrets
|
||||||
|
|
||||||
|
在 `admin-frontend` 仓库 `Settings -> Secrets and variables -> Actions` 配置:
|
||||||
|
|
||||||
|
- `PROD_SSH_KEY`:私钥内容(`~/.ssh/id_ed25519`)
|
||||||
|
- `PROD_HOST`:`115.159.198.14`
|
||||||
|
- `PROD_PORT`:`22`
|
||||||
|
- `PROD_USER`:`root`
|
||||||
|
- `ADMIN_WEB_ROOT`:`/www/wwwroot/wx_service/web/admin-frontend/panel`
|
||||||
|
- `ADMIN_WEB_USER`:`www`
|
||||||
|
- `ADMIN_WEB_GROUP`:`www`
|
||||||
|
- `ADMIN_KEEP_BACKUPS`:`5`
|
||||||
|
|
||||||
|
可选:
|
||||||
|
- `ADMIN_HEALTHCHECK_URL`:例如 `http://115.159.198.14/panel/`
|
||||||
|
|
||||||
|
## 3. 密钥说明
|
||||||
|
|
||||||
|
`~/.ssh/id_ed25519.pub` 是公钥,用于写入服务器 `~/.ssh/authorized_keys`。
|
||||||
|
|
||||||
|
GitHub Actions 里的 `PROD_SSH_KEY` 必须填写私钥:
|
||||||
|
- `~/.ssh/id_ed25519`
|
||||||
|
|
||||||
|
## 4. 工作流文件
|
||||||
|
|
||||||
|
- `/.github/workflows/deploy-prod.yml`
|
||||||
|
|
||||||
|
每次主分支变更会自动:
|
||||||
|
1. `npm ci`
|
||||||
|
2. `npm run build`(生产基路径 `/panel/`)
|
||||||
|
3. 上传构建包到服务器 `/tmp`
|
||||||
|
4. 调用 `scripts/ops/deploy_static.sh` 覆盖发布
|
||||||
|
|
||||||
|
## 5. 回滚
|
||||||
|
|
||||||
|
在服务器执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lt /www/wwwroot/wx_service/web/admin-frontend/backups/admin-frontend.*.tar.gz
|
||||||
|
|
||||||
|
TARGET_DIR=/www/wwwroot/wx_service/web/admin-frontend/panel
|
||||||
|
BACKUP_FILE=/www/wwwroot/wx_service/web/admin-frontend/backups/admin-frontend.<backup_id>.tar.gz
|
||||||
|
|
||||||
|
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||||
|
tar -xzf "$BACKUP_FILE" -C "$TARGET_DIR"
|
||||||
|
chown -R www:www "$TARGET_DIR"
|
||||||
|
```
|
||||||
Executable
+121
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# 待发布的构建产物(tar.gz),由 CI 上传到服务器后传入
|
||||||
|
DEPLOY_TAR="${DEPLOY_TAR:-}"
|
||||||
|
# 静态站点目录(与 Nginx location /panel/ 对齐)
|
||||||
|
TARGET_DIR="${TARGET_DIR:-}"
|
||||||
|
# 发布标识(通常用 commit sha)
|
||||||
|
RELEASE_ID="${RELEASE_ID:-}"
|
||||||
|
# 文件归属用户/组
|
||||||
|
RUN_USER="${RUN_USER:-}"
|
||||||
|
RUN_GROUP="${RUN_GROUP:-}"
|
||||||
|
# 备份保留数量
|
||||||
|
KEEP_BACKUPS="${KEEP_BACKUPS:-}"
|
||||||
|
# 可选:发布后探活地址,例如 http://example.com/panel/
|
||||||
|
HEALTHCHECK_URL="${HEALTHCHECK_URL:-}"
|
||||||
|
|
||||||
|
BACKUP_ROOT="${BACKUP_ROOT:-$(dirname "$TARGET_DIR")/backups}"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local name="$1"
|
||||||
|
local value="$2"
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
echo "missing required env: $name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
health_check() {
|
||||||
|
if [ -z "$HEALTHCHECK_URL" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
log "curl 不存在,跳过 HEALTHCHECK_URL 检查"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tries=15
|
||||||
|
while [ "$tries" -gt 0 ]; do
|
||||||
|
if curl -fsS --max-time 5 "$HEALTHCHECK_URL" >/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
tries=$((tries - 1))
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env "DEPLOY_TAR" "$DEPLOY_TAR"
|
||||||
|
require_env "TARGET_DIR" "$TARGET_DIR"
|
||||||
|
require_env "RELEASE_ID" "$RELEASE_ID"
|
||||||
|
require_env "RUN_USER" "$RUN_USER"
|
||||||
|
require_env "RUN_GROUP" "$RUN_GROUP"
|
||||||
|
require_env "KEEP_BACKUPS" "$KEEP_BACKUPS"
|
||||||
|
|
||||||
|
if [ ! -f "$DEPLOY_TAR" ]; then
|
||||||
|
echo "deploy tar not found: $DEPLOY_TAR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_dir="/tmp/admin-frontend-${RELEASE_ID:0:12}-$$"
|
||||||
|
backup_file=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
log "deploy start, release: ${RELEASE_ID}"
|
||||||
|
mkdir -p "$tmp_dir" "$TARGET_DIR" "$BACKUP_ROOT"
|
||||||
|
|
||||||
|
log "extracting artifact"
|
||||||
|
tar -xzf "$DEPLOY_TAR" -C "$tmp_dir"
|
||||||
|
|
||||||
|
if [ ! -f "$tmp_dir/index.html" ]; then
|
||||||
|
echo "invalid artifact: index.html not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 发布前备份当前站点,支持快速回滚
|
||||||
|
if [ -n "$(ls -A "$TARGET_DIR" 2>/dev/null)" ]; then
|
||||||
|
backup_file="$BACKUP_ROOT/admin-frontend.${RELEASE_ID}.tar.gz"
|
||||||
|
tar -czf "$backup_file" -C "$TARGET_DIR" .
|
||||||
|
log "backup created: $backup_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 覆盖发布:先清空目录再写入新构建
|
||||||
|
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||||
|
tar -cf - -C "$tmp_dir" . | tar -xf - -C "$TARGET_DIR"
|
||||||
|
chown -R "$RUN_USER:$RUN_GROUP" "$TARGET_DIR" || true
|
||||||
|
|
||||||
|
if health_check; then
|
||||||
|
if [ -n "$HEALTHCHECK_URL" ]; then
|
||||||
|
log "health check success: $HEALTHCHECK_URL"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "health check failed, rolling back"
|
||||||
|
if [ -n "$backup_file" ] && [ -f "$backup_file" ]; then
|
||||||
|
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||||
|
tar -xzf "$backup_file" -C "$TARGET_DIR"
|
||||||
|
chown -R "$RUN_USER:$RUN_GROUP" "$TARGET_DIR" || true
|
||||||
|
log "rollback done"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 清理上传到 /tmp 的临时制品
|
||||||
|
if [[ "$DEPLOY_TAR" == /tmp/* ]]; then
|
||||||
|
rm -f "$DEPLOY_TAR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 清理旧备份,仅保留最近 KEEP_BACKUPS 份
|
||||||
|
ls -1t "$BACKUP_ROOT"/admin-frontend.*.tar.gz 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | xargs -r rm -f
|
||||||
|
|
||||||
|
log "deploy done"
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const loginPath = `${import.meta.env.BASE_URL}login`
|
||||||
|
|
||||||
// 创建 axios 实例
|
// 创建 axios 实例
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/',
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/',
|
||||||
@@ -33,7 +35,7 @@ request.interceptors.response.use(
|
|||||||
// 401: 未登录
|
// 401: 未登录
|
||||||
if (res.code === 401) {
|
if (res.code === 401) {
|
||||||
localStorage.removeItem('admin_token')
|
localStorage.removeItem('admin_token')
|
||||||
window.location.href = '/login'
|
window.location.href = loginPath
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error(res.message || '请求失败'))
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
@@ -50,7 +52,7 @@ request.interceptors.response.use(
|
|||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
ElMessage.error('登录已过期,请重新登录')
|
ElMessage.error('登录已过期,请重新登录')
|
||||||
localStorage.removeItem('admin_token')
|
localStorage.removeItem('admin_token')
|
||||||
window.location.href = '/login'
|
window.location.href = loginPath
|
||||||
} else if (status === 403) {
|
} else if (status === 403) {
|
||||||
ElMessage.error('没有权限访问')
|
ElMessage.error('没有权限访问')
|
||||||
} else if (status === 404) {
|
} else if (status === 404) {
|
||||||
|
|||||||
+20
-12
@@ -1,18 +1,26 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [vue()],
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
server: {
|
|
||||||
proxy: {
|
// 生产默认挂载在 /panel/,可通过 VITE_PUBLIC_BASE 覆盖
|
||||||
'/api': {
|
const base = env.VITE_PUBLIC_BASE || (mode === 'production' ? '/panel/' : '/')
|
||||||
target: 'http://localhost:8080',
|
|
||||||
changeOrigin: true
|
return {
|
||||||
},
|
base,
|
||||||
'/healthz': {
|
plugins: [vue()],
|
||||||
target: 'http://localhost:8080',
|
server: {
|
||||||
changeOrigin: true
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/healthz': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user