deploy(M00-E): 完善菜单式部署骨架

This commit is contained in:
Codex
2026-06-15 16:13:30 +08:00
parent 46c6450ceb
commit c51ba43fa9
18 changed files with 545 additions and 59 deletions
+14 -1
View File
@@ -1,4 +1,17 @@
# Ubuntu 菜单脚本目录
后续 M00-D/M00-E 将在此目录补充 `/opt/apps` 初始化、Gitea 拉取、业务部署、EMQX、Nginx、证书、备份、恢复、回滚和诊断脚本
本目录为根目录 `setup.sh` 提供模块化函数
| 文件 | 用途 |
|---|---|
| `lib.sh` | 固定路径、仓库、域名、状态输出和架构检查。 |
| `preflight.sh` | 启动快检:架构、基础命令、Docker 禁用提醒、Redis 预留状态。 |
| `init-layout.sh` | 创建 `/opt/apps` 目录布局并写入 `run/layout.json`。 |
| `repo-status.sh` | 检查固定仓库、分支、DIRTY/AHEAD/BEHIND/DIVERGED 状态。 |
| `deploy-business.sh` | 克隆/更新仓库并生成 dry-run release manifest。 |
| `backup.sh` | 生成 manifest-only 备份记录。 |
| `restore.sh` | 输出人工恢复要求,不自动改动生产数据。 |
| `rollback.sh` | 列出 release 回滚点。 |
| `diagnose.sh` | 汇总快检、仓库、磁盘、服务和公开端点。 |
M00 阶段脚本必须保持可重复执行和非破坏性。真实数据库、证书、EMQX ACL、Nginx 写入和 PM2 切换将在后续模块具备配置后继续补全。
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
qipai_backup() {
qipai_require_root_for_write
local backup_dir manifest
backup_dir="${APP_ROOT}/backups/manual/$(date +%Y%m%d%H%M%S)"
mkdir -p "$backup_dir"
manifest="${backup_dir}/backup.json"
cat >"$manifest" <<EOF
{
"generatedAt": "$(qipai_timestamp)",
"type": "M00_MANIFEST_ONLY",
"mysql": "SKIPPED_NO_CONFIG",
"emqx": "SKIPPED_NO_CONFIG",
"uploads": "SKIPPED_NO_UPLOAD_DIR",
"note": "Real backup commands will be enabled after production configuration is present."
}
EOF
qipai_pass "backup manifest written: $manifest"
}
if [ "${1:-}" = "--run" ]; then
qipai_backup
fi
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
# shellcheck source=repo-status.sh
. "${SCRIPT_DIR}/repo-status.sh"
qipai_deploy_business() {
qipai_require_root_for_write
local repo_dir release_id release_dir manifest
repo_dir="$(qipai_repo_dir)"
if [ ! -d "${repo_dir}/.git" ]; then
qipai_info "cloning repository to ${repo_dir}"
git clone --branch "$QIPAI_BRANCH" "$QIPAI_REPO_URL" "$repo_dir"
fi
qipai_repo_status
git -C "$repo_dir" pull --ff-only origin "$QIPAI_BRANCH"
release_id="$(date +%Y%m%d%H%M%S)-$(git -C "$repo_dir" rev-parse --short HEAD)"
release_dir="$(qipai_release_dir)/${release_id}"
mkdir -p "$release_dir"
manifest="${release_dir}/release.json"
cat >"$manifest" <<EOF
{
"releaseId": "${release_id}",
"generatedAt": "$(qipai_timestamp)",
"commit": "$(git -C "$repo_dir" rev-parse HEAD)",
"branch": "${QIPAI_BRANCH}",
"backendBuild": "SKIPPED_NO_PROJECT",
"adminBuild": "SKIPPED_NO_PROJECT",
"miniappMirror": "RECORDED_SOURCE_COMMIT_ONLY",
"databaseMigration": "SKIPPED_NO_MIGRATIONS",
"deployed": false
}
EOF
ln -sfn "$release_dir" "${APP_ROOT}/current-release"
cp "$manifest" "$(qipai_current_release_file)"
qipai_pass "release manifest written: $manifest"
qipai_warn "business build is a safe M00 dry run until backend/admin projects exist"
}
if [ "${1:-}" = "--run" ]; then
qipai_deploy_business
fi
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
# shellcheck source=preflight.sh
. "${SCRIPT_DIR}/preflight.sh"
# shellcheck source=repo-status.sh
. "${SCRIPT_DIR}/repo-status.sh"
qipai_diagnose() {
qipai_preflight || true
qipai_repo_status || true
qipai_info "disk usage:"
df -h "$APP_ROOT" 2>/dev/null || df -h /
qipai_info "service status summary:"
for service in nginx mysql emqx gitea; do
if command -v systemctl >/dev/null 2>&1; then
systemctl is-active --quiet "$service" 2>/dev/null && qipai_pass "${service}: active" || qipai_warn "${service}: inactive or missing"
fi
done
qipai_info "public endpoints:"
qipai_info "app health: ${QIPAI_API_ORIGIN}/app-api/health"
qipai_info "admin health: ${QIPAI_API_ORIGIN}/admin-api/health"
}
if [ "${1:-}" = "--run" ]; then
qipai_diagnose
fi
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
qipai_init_layout() {
qipai_require_root_for_write
local dirs=(
"${APP_ROOT}"
"${APP_ROOT}/qipai-repo"
"${APP_ROOT}/qipai-backend"
"${APP_ROOT}/qipai-admin"
"${APP_ROOT}/qipai-miniapp"
"${APP_ROOT}/releases"
"${APP_ROOT}/backups/mysql"
"${APP_ROOT}/backups/emqx"
"${APP_ROOT}/backups/files"
"${APP_ROOT}/logs"
"${APP_ROOT}/nginx"
"${APP_ROOT}/redis-reserved"
"${APP_ROOT}/run"
)
for dir in "${dirs[@]}"; do
mkdir -p "$dir"
qipai_pass "directory exists: $dir"
done
if id qipai >/dev/null 2>&1; then
qipai_pass "user qipai exists"
else
qipai_warn "user qipai does not exist; create manually before production deployment"
fi
if id git >/dev/null 2>&1; then
qipai_pass "user git exists"
else
qipai_warn "user git does not exist; required for native Gitea service"
fi
cat >"${APP_ROOT}/run/layout.json" <<EOF
{
"generatedAt": "$(qipai_timestamp)",
"appRoot": "${APP_ROOT}",
"repo": "$(qipai_repo_dir)",
"backend": "${APP_ROOT}/qipai-backend",
"admin": "${APP_ROOT}/qipai-admin",
"miniapp": "${APP_ROOT}/qipai-miniapp",
"releases": "$(qipai_release_dir)",
"redis": "RESERVED/DISABLED"
}
EOF
qipai_pass "layout manifest written: ${APP_ROOT}/run/layout.json"
}
if [ "${1:-}" = "--run" ]; then
qipai_init_layout
fi
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
QIPAI_DEPLOY_VERSION="${QIPAI_DEPLOY_VERSION:-0.1.0-m00-deploy-baseline}"
APP_ROOT="${APP_ROOT:-/opt/apps}"
QIPAI_REPO_URL="${QIPAI_REPO_URL:-ssh://git@127.0.0.1:2222/panda/qipai.git}"
QIPAI_PUBLIC_REPO_URL="${QIPAI_PUBLIC_REPO_URL:-ssh://git@git.txyundm.cn:2222/panda/qipai.git}"
QIPAI_BRANCH="${QIPAI_BRANCH:-main}"
QIPAI_DOMAIN="${QIPAI_DOMAIN:-api.txyundm.cn}"
QIPAI_API_ORIGIN="https://${QIPAI_DOMAIN}"
qipai_timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}
qipai_status() {
local level="$1"
shift
printf '%s: %s\n' "$level" "$*"
}
qipai_info() {
qipai_status "INFO" "$@"
}
qipai_pass() {
qipai_status "PASS" "$@"
}
qipai_warn() {
qipai_status "WARN" "$@"
}
qipai_fail() {
qipai_status "FAIL" "$@"
}
qipai_require_root_for_write() {
if [ "$(id -u)" -ne 0 ]; then
qipai_fail "This option writes under ${APP_ROOT}; run with sudo on Ubuntu."
return 1
fi
}
qipai_check_arch() {
local kernel_arch dpkg_arch bits
kernel_arch="$(uname -m 2>/dev/null || echo unknown)"
dpkg_arch="$(dpkg --print-architecture 2>/dev/null || echo unknown)"
bits="$(getconf LONG_BIT 2>/dev/null || echo unknown)"
[ "$kernel_arch" = "x86_64" ] && qipai_pass "kernel architecture: ${kernel_arch}" || {
qipai_fail "kernel architecture must be x86_64, actual: ${kernel_arch}"
return 1
}
[ "$dpkg_arch" = "amd64" ] && qipai_pass "dpkg architecture: ${dpkg_arch}" || {
qipai_fail "dpkg architecture must be amd64, actual: ${dpkg_arch}"
return 1
}
[ "$bits" = "64" ] && qipai_pass "userspace bits: ${bits}" || {
qipai_fail "userspace must be 64-bit, actual: ${bits}"
return 1
}
}
qipai_command_status() {
local command_name="$1"
if command -v "$command_name" >/dev/null 2>&1; then
qipai_pass "${command_name}: $(command -v "$command_name")"
else
qipai_warn "${command_name}: not installed"
fi
}
qipai_repo_dir() {
printf '%s/qipai-repo\n' "$APP_ROOT"
}
qipai_release_dir() {
printf '%s/releases\n' "$APP_ROOT"
}
qipai_current_release_file() {
printf '%s/current-release.json\n' "$APP_ROOT"
}
qipai_print_context() {
qipai_info "deploy version: ${QIPAI_DEPLOY_VERSION}"
qipai_info "app root: ${APP_ROOT}"
qipai_info "repo: ${QIPAI_REPO_URL}"
qipai_info "branch: ${QIPAI_BRANCH}"
qipai_info "api origin: ${QIPAI_API_ORIGIN}"
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
qipai_preflight() {
qipai_print_context
qipai_check_arch
qipai_command_status git
qipai_command_status bash
qipai_command_status node
qipai_command_status npm
qipai_command_status mysql
qipai_command_status nginx
qipai_command_status pm2
qipai_command_status openssl
qipai_command_status curl
if command -v docker >/dev/null 2>&1; then
qipai_warn "docker detected but this project does not use Docker"
else
qipai_pass "docker: not installed or not in PATH"
fi
if command -v redis-server >/dev/null 2>&1; then
qipai_warn "redis-server detected; Redis is reserved/disabled for MVP"
else
qipai_pass "redis: RESERVED/DISABLED"
fi
}
if [ "${1:-}" = "--run" ]; then
qipai_preflight
fi
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
qipai_repo_status() {
local repo_dir
repo_dir="$(qipai_repo_dir)"
if [ ! -d "${repo_dir}/.git" ]; then
qipai_warn "repo not cloned: ${repo_dir}"
qipai_info "expected repo: ${QIPAI_REPO_URL}"
return 0
fi
git -C "$repo_dir" remote -v
local branch status
branch="$(git -C "$repo_dir" branch --show-current)"
status="$(git -C "$repo_dir" status --short --branch --untracked-files=all)"
qipai_info "branch: ${branch}"
printf '%s\n' "$status"
if [ "$branch" != "$QIPAI_BRANCH" ]; then
qipai_fail "branch mismatch: expected ${QIPAI_BRANCH}, actual ${branch}"
return 1
fi
if git -C "$repo_dir" diff --quiet && git -C "$repo_dir" diff --cached --quiet; then
qipai_pass "repo is not dirty"
else
qipai_fail "repo is DIRTY; deployment must stop"
return 1
fi
git -C "$repo_dir" fetch origin "$QIPAI_BRANCH"
local counts
counts="$(git -C "$repo_dir" rev-list --left-right --count "${QIPAI_BRANCH}...origin/${QIPAI_BRANCH}")"
qipai_info "ahead/behind: ${counts}"
case "$counts" in
"0 0") qipai_pass "repo matches origin/${QIPAI_BRANCH}" ;;
0$'\t'*) qipai_warn "repo is BEHIND origin/${QIPAI_BRANCH}" ;;
*$'\t'0) qipai_fail "repo is AHEAD origin/${QIPAI_BRANCH}; deployment must stop"; return 1 ;;
*) qipai_fail "repo is DIVERGED; deployment must stop"; return 1 ;;
esac
}
if [ "${1:-}" = "--run" ]; then
qipai_repo_status
fi
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
qipai_restore() {
qipai_warn "restore is not automatic in M00 baseline"
qipai_info "Required manual inputs: backup path, MySQL dump, uploads archive, EMQX export"
qipai_info "No production data was changed."
}
if [ "${1:-}" = "--run" ]; then
qipai_restore
fi
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "${SCRIPT_DIR}/lib.sh"
qipai_rollback() {
local releases
releases="$(qipai_release_dir)"
if [ ! -d "$releases" ]; then
qipai_warn "no releases directory: $releases"
return 0
fi
qipai_info "available releases:"
find "$releases" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -r | head -20
qipai_warn "M00 baseline lists rollback points only; automatic switch requires a selected release id"
}
if [ "${1:-}" = "--run" ]; then
qipai_rollback
fi