deploy(M00-E): 完善菜单式部署骨架
This commit is contained in:
+12
-1
@@ -29,5 +29,16 @@
|
|||||||
9. 诊断
|
9. 诊断
|
||||||
0. 退出
|
0. 退出
|
||||||
|
|
||||||
当前版本仅提供 M00 菜单骨架和安全提示,生产安装动作将在后续 M00-D/M00-E 完善。
|
当前版本提供 M00-D/M00-E 部署基线:
|
||||||
|
|
||||||
|
- 启动快检:架构、基础命令、Docker 禁用提醒、Redis 预留状态。
|
||||||
|
- 首次安装:创建 `/opt/apps` 目录布局,写入 `run/layout.json`。
|
||||||
|
- 更新业务服务:克隆/检查固定仓库,生成 release manifest。正式后端/后台未生成前仅执行安全 dry-run。
|
||||||
|
- EMQX:输出原生 Apt/无 Docker/MQTTX 禁止策略和本机命令状态。
|
||||||
|
- HTTPS:输出固定域名、API 路径和 Nginx 配置检查。
|
||||||
|
- 状态:显示部署上下文、快检、仓库状态和当前 release manifest。
|
||||||
|
- 备份:当前生成 manifest-only 备份记录,不触碰真实数据库。
|
||||||
|
- 恢复/回滚:列出人工输入和回滚点,不自动改动生产数据。
|
||||||
|
- 诊断:汇总环境、仓库、磁盘和服务状态。
|
||||||
|
|
||||||
|
生产执行仍必须由管理员在 Ubuntu 上运行 `/opt/apps/setup.sh` 或同源脚本;Codex 不从 Windows/WSL 自动触发生产部署。
|
||||||
|
|||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
0.0.1-m00-baseline
|
0.1.0-m00-deploy-baseline
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# 部署变更记录
|
# 部署变更记录
|
||||||
|
|
||||||
## 2026-06-15 / 0.0.1-m00-baseline
|
## 2026-06-15 / 0.1.0-m00-deploy-baseline
|
||||||
|
|
||||||
- 关联模块:M00
|
- 关联模块:M00
|
||||||
- 关联 commit:本地 HEAD(push 待完成)
|
- 关联 commit:本地 HEAD(push 待完成)
|
||||||
- 变化内容:建立根目录 `setup.sh`、部署目录和状态文档。
|
- 变化内容:建立根目录 `setup.sh`、`scripts/setup/*.sh` 模块化部署脚本、部署目录和状态文档。
|
||||||
- 配置变化:固定 `api.txyundm.cn`、`/opt/apps`、Ubuntu 24.04 amd64。
|
- 配置变化:固定 `api.txyundm.cn`、`/opt/apps`、Ubuntu 24.04 amd64。
|
||||||
- 数据库变化:无。
|
- 数据库变化:无。
|
||||||
- 兼容性:当前仅菜单骨架,不执行生产改动。
|
- 兼容性:当前仅菜单骨架,不执行生产改动。
|
||||||
- 已执行验证:待运行脚本。
|
- 已执行验证:Windows 本地检查、WSL `bash -n setup.sh scripts/setup/*.sh`、`setup.sh --preflight`、`setup.sh --status`、`setup.sh --diagnose`;生产 Ubuntu 未执行。
|
||||||
- 回滚方式:尚未生产部署,无生产回滚。
|
- 回滚方式:尚未生产部署,无生产回滚。
|
||||||
- 生产环境人工步骤:后续由管理员在 Ubuntu `/opt/apps/setup.sh` 执行。
|
- 生产环境人工步骤:后续由管理员在 Ubuntu `/opt/apps/setup.sh` 执行。
|
||||||
|
|||||||
+12
-12
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
| 项目 | 当前值 |
|
| 项目 | 当前值 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 菜单脚本版本 | 0.0.1-m00-baseline |
|
| 菜单脚本版本 | 0.1.0-m00-deploy-baseline |
|
||||||
| Gitea 仓库 Web | https://git.txyundm.cn/panda/qipai.git |
|
| Gitea 仓库 Web | https://git.txyundm.cn/panda/qipai.git |
|
||||||
| API 固定域名 | https://api.txyundm.cn |
|
| API 固定域名 | https://api.txyundm.cn |
|
||||||
| 小程序 API | https://api.txyundm.cn/app-api |
|
| 小程序 API | https://api.txyundm.cn/app-api |
|
||||||
@@ -25,18 +25,18 @@
|
|||||||
| 命令行 MQTT 工具 | mosquitto-clients:未安装/未验证 |
|
| 命令行 MQTT 工具 | mosquitto-clients:未安装/未验证 |
|
||||||
| Windows MQTTX 验证 | 未验证 |
|
| Windows MQTTX 验证 | 未验证 |
|
||||||
| WSL 环境验证 | 已完成轻量检查、shell 语法检查、临时副本准备和清理;完整构建待正式项目生成 |
|
| WSL 环境验证 | 已完成轻量检查、shell 语法检查、临时副本准备和清理;完整构建待正式项目生成 |
|
||||||
| 最近环境快检 | 2026-06-15 本地骨架检查 |
|
| 最近环境快检 | 2026-06-15 WSL 执行 `setup.sh --preflight/--status/--diagnose` 通过,WARN 项已记录 |
|
||||||
| 最近部署后复检 | 未执行 |
|
| 最近部署后复检 | 未执行 |
|
||||||
| 最近验证 commit | 远端 HEAD;M00-C 推送脚本语法检查通过 |
|
| 最近验证 commit | 远端 HEAD;M00-C 推送脚本语法检查通过 |
|
||||||
| 最近验证日期 | 2026-06-15 |
|
| 最近验证日期 | 2026-06-15 |
|
||||||
| 已验证系统 | Ubuntu 24.04 / 未验证 |
|
| 已验证系统 | Ubuntu 24.04 / 未验证 |
|
||||||
| 菜单 1 首次安装 | 未验证 |
|
| 菜单 1 首次安装 | 脚本已实现目录布局;未在生产 Ubuntu 执行 |
|
||||||
| 菜单 2 更新业务 | 未验证 |
|
| 菜单 2 更新业务 | 脚本已实现仓库检查和 dry-run 发布清单;未在生产 Ubuntu 执行 |
|
||||||
| 菜单 3 MQTT | 未验证 |
|
| 菜单 3 MQTT | 状态检查已实现;EMQX 安装未执行 |
|
||||||
| 菜单 4 域名与 HTTPS | 未验证 |
|
| 菜单 4 域名与 HTTPS | 状态检查已实现;证书申请/续期未执行 |
|
||||||
| 菜单 5 状态 | 未验证 |
|
| 菜单 5 状态 | 已实现 |
|
||||||
| 菜单 6 备份 | 未验证 |
|
| 菜单 6 备份 | manifest-only 已实现;真实备份待生产配置 |
|
||||||
| 菜单 7 恢复 | 未验证 |
|
| 菜单 7 恢复 | 人工恢复提示已实现 |
|
||||||
| 菜单 8 回滚 | 未验证 |
|
| 菜单 8 回滚 | 回滚点列表已实现;自动切换待正式 release |
|
||||||
| 菜单 9 诊断 | 未验证 |
|
| 菜单 9 诊断 | 已实现 |
|
||||||
| 已知限制 | 当前仅为 M00 基线骨架,未执行生产部署。 |
|
| 已知限制 | 未执行生产部署;WSL 快检显示 Node/Nginx/PM2 缺失或未安装,生产 Ubuntu 需重新验证;正式后端/后台未生成前,业务构建为 dry-run。 |
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- 将含密钥/依赖/真实数据风险的原始参考包和 SQL 从 Git 跟踪中移出,仅保留哈希和审计结论。
|
- 将含密钥/依赖/真实数据风险的原始参考包和 SQL 从 Git 跟踪中移出,仅保留哈希和审计结论。
|
||||||
- 生成 `docs/reference-page-map.md`、`docs/db-schema-inventory.md` 和 `docs/reference-api-inventory.md` 作为可提交脱敏摘要。
|
- 生成 `docs/reference-page-map.md`、`docs/db-schema-inventory.md` 和 `docs/reference-api-inventory.md` 作为可提交脱敏摘要。
|
||||||
- 生成 `scripts/dev/windows/push-module.ps1` 和 `check-gitea-ssh.ps1`,串联模块检查、提交、推送和远端校验。
|
- 生成 `scripts/dev/windows/push-module.ps1` 和 `check-gitea-ssh.ps1`,串联模块检查、提交、推送和远端校验。
|
||||||
|
- 生成 `scripts/setup/*.sh` 和新版 `setup.sh` 菜单,覆盖 `/opt/apps` 目录、仓库状态、dry-run 发布清单、备份 manifest、恢复/回滚提示和诊断。
|
||||||
|
|
||||||
## 起始状态
|
## 起始状态
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
|
|
||||||
## 部署影响
|
## 部署影响
|
||||||
|
|
||||||
有。新增 `setup.sh` 菜单骨架和部署状态文档,但未执行生产部署。
|
有。新增并升级 `setup.sh` 与 `scripts/setup/*.sh` 菜单式部署骨架,但未执行生产部署。
|
||||||
|
|
||||||
## 测试记录
|
## 测试记录
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
- 脱敏摘要:已生成页面地图、数据库结构清单和接口线索文档。
|
- 脱敏摘要:已生成页面地图、数据库结构清单和接口线索文档。
|
||||||
- Windows 推送脚本:已新增,遇到 SSH 主机指纹问题时停止,不自动覆盖 `known_hosts`。
|
- Windows 推送脚本:已新增,遇到 SSH 主机指纹问题时停止,不自动覆盖 `known_hosts`。
|
||||||
- PowerShell 语法检查:`push-module.ps1`、`check-gitea-ssh.ps1` 均已通过 Parser 检查。
|
- PowerShell 语法检查:`push-module.ps1`、`check-gitea-ssh.ps1` 均已通过 Parser 检查。
|
||||||
|
- 部署脚本:WSL 已执行 `bash -n setup.sh scripts/setup/*.sh`、`setup.sh --preflight`、`setup.sh --status`、`setup.sh --diagnose`;输出 PASS/WARN,未执行生产写入。
|
||||||
- WSL 检查:已执行 `check-env.sh`、`check-workspace.sh`、`verify-linux.sh`、`prepare-test-copy.sh` 和 `cleanup-test-copy.sh`;临时副本创建和清理通过。完整 Linux 构建待正式后端/后台项目生成后执行。
|
- WSL 检查:已执行 `check-env.sh`、`check-workspace.sh`、`verify-linux.sh`、`prepare-test-copy.sh` 和 `cleanup-test-copy.sh`;临时副本创建和清理通过。完整 Linux 构建待正式后端/后台项目生成后执行。
|
||||||
- Git fetch/push:首次多次失败后,`git push origin main` 已成功;随后 `git fetch origin main` 校验 `HEAD == origin/main` 通过。
|
- Git fetch/push:首次多次失败后,`git push origin main` 已成功;随后 `git fetch origin main` 校验 `HEAD == origin/main` 通过。
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
| API-001 | 固定 HTTPS API 域名 | M00-E/M01/M08/M10 | TODO | - | - | DNS/HTTPS 生产验证未执行。 | 在部署脚本和代码中统一 `api.txyundm.cn`。 |
|
| API-001 | 固定 HTTPS API 域名 | M00-E/M01/M08/M10 | TODO | - | - | DNS/HTTPS 生产验证未执行。 | 在部署脚本和代码中统一 `api.txyundm.cn`。 |
|
||||||
| TLS-001 | Nginx 与证书自动化 | M00-E/M10 | TODO | - | - | DNS/80/443 生产验证未执行。 | 补菜单脚本和证书检查。 |
|
| TLS-001 | Nginx 与证书自动化 | M00-E/M10 | TODO | - | - | DNS/80/443 生产验证未执行。 | 补菜单脚本和证书检查。 |
|
||||||
| WXNET-001 | 微信合法域名与真机验证 | M00-E/M08/M10 | TODO | - | - | 微信后台/真机未验证。 | 先完成检查报告模板。 |
|
| WXNET-001 | 微信合法域名与真机验证 | M00-E/M08/M10 | TODO | - | - | 微信后台/真机未验证。 | 先完成检查报告模板。 |
|
||||||
| OPS-001 | 固定 `/opt/apps` 目录 | M00-D/M10 | TODO | - | - | Ubuntu 生产操作未执行。 | 生成部署菜单和目录检查。 |
|
| OPS-001 | 固定 `/opt/apps` 目录 | M00-D/M10 | PARTIAL | 本地 HEAD | `scripts/setup/init-layout.sh` 已生成目录布局和 manifest;未在生产 Ubuntu 执行。 | 生产操作未执行。 | 由管理员在 Ubuntu 菜单执行并记录结果。 |
|
||||||
| OPS-002 | 单仓库 Gitea 推送与拉取部署 | M00-B/M10 | PARTIAL | 远端 HEAD | Windows 到 Gitea `origin/main` 首次 push 与远端校验通过。 | 生产服务器只读拉取部署尚未验证。 | M00-D/M00-E 继续完善生产拉取、菜单部署和状态检查。 |
|
| OPS-002 | 单仓库 Gitea 推送与拉取部署 | M00-B/M10 | PARTIAL | 远端 HEAD | Windows 到 Gitea `origin/main` 首次 push 与远端校验通过。 | 生产服务器只读拉取部署尚未验证。 | M00-D/M00-E 继续完善生产拉取、菜单部署和状态检查。 |
|
||||||
| OPS-003 | 整仓发布清单 | M00-E/M10 | TODO | - | - | - | 生成 release manifest 模板。 |
|
| OPS-003 | 整仓发布清单 | M00-E/M10 | PARTIAL | 本地 HEAD | `deploy-business.sh` 可生成 dry-run release manifest;正式构建待项目生成。 | 后端/后台尚未生成。 | M01/M09 后接入真实构建结果。 |
|
||||||
| OPS-004 | 菜单式更新与环境监测 | M00-E/M10 | TODO | - | - | - | 生成 `setup.sh` 和 `scripts/setup`。 |
|
| OPS-004 | 菜单式更新与环境监测 | M00-E/M10 | PARTIAL | 本地 HEAD | `setup.sh` 已接入初始化、更新、MQTT、HTTPS、状态、备份、恢复、回滚和诊断菜单;WSL 快检/status/diagnose 通过。 | 未在生产 Ubuntu 执行。 | 生产执行后补部署记录。 |
|
||||||
| IOT-001 | MQTT Broker 生产部署 | M00/M06 | TODO | - | - | 缺生产 EMQX 实机验证。 | 先完成部署文档和 MQTT 状态模板。 |
|
| IOT-001 | MQTT Broker 生产部署 | M00/M06 | PARTIAL | 本地 HEAD | 菜单 3 已输出 EMQX 原生 Apt、禁止 MQTTX 和 mosquitto-clients 状态检查;未安装。 | 缺生产 EMQX 实机验证。 | M00/M06 继续补官方 Apt 安装和 ACL。 |
|
||||||
| SYS-001 | 微信原生小程序 | M00-C/M08 | TODO | - | - | 参考小程序压缩包尚未深度整理。 | 后续导入正式 `miniapp/`。 |
|
| SYS-001 | 微信原生小程序 | M00-C/M08 | TODO | - | - | 参考小程序压缩包尚未深度整理。 | 后续导入正式 `miniapp/`。 |
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
| 项目 | 当前值 |
|
| 项目 | 当前值 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 最近发布 commit | 未发布;本地 M00 基线提交为本地 HEAD |
|
| 最近发布 commit | 未发布;M00 部署脚本仅生成 dry-run 发布清单 |
|
||||||
| 发布分支 | main |
|
| 发布分支 | main |
|
||||||
| 后端构建 | 未执行 |
|
| 后端构建 | 未执行 |
|
||||||
| 后台构建 | 未执行 |
|
| 后台构建 | 未执行 |
|
||||||
| 小程序检查 | 未执行 |
|
| 小程序检查 | 未执行 |
|
||||||
| 数据库迁移 | 未执行 |
|
| 数据库迁移 | 未执行 |
|
||||||
| 部署结果 | 未部署 |
|
| 部署结果 | 未部署;生产需管理员人工执行菜单 |
|
||||||
| 回滚点 | - |
|
| 回滚点 | - |
|
||||||
|
|||||||
+14
-1
@@ -1,4 +1,17 @@
|
|||||||
# Ubuntu 菜单脚本目录
|
# 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 切换将在后续模块具备配置后继续补全。
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -1,52 +1,101 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="0.0.1-m00-baseline"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
APP_ROOT="/opt/apps"
|
SETUP_DIR="${SCRIPT_DIR}/scripts/setup"
|
||||||
REPO_SSH="ssh://git@127.0.0.1:2222/panda/qipai.git"
|
|
||||||
PUBLIC_API="https://api.txyundm.cn"
|
# shellcheck source=scripts/setup/lib.sh
|
||||||
|
. "${SETUP_DIR}/lib.sh"
|
||||||
|
# shellcheck source=scripts/setup/preflight.sh
|
||||||
|
. "${SETUP_DIR}/preflight.sh"
|
||||||
|
# shellcheck source=scripts/setup/init-layout.sh
|
||||||
|
. "${SETUP_DIR}/init-layout.sh"
|
||||||
|
# shellcheck source=scripts/setup/repo-status.sh
|
||||||
|
. "${SETUP_DIR}/repo-status.sh"
|
||||||
|
# shellcheck source=scripts/setup/deploy-business.sh
|
||||||
|
. "${SETUP_DIR}/deploy-business.sh"
|
||||||
|
# shellcheck source=scripts/setup/backup.sh
|
||||||
|
. "${SETUP_DIR}/backup.sh"
|
||||||
|
# shellcheck source=scripts/setup/restore.sh
|
||||||
|
. "${SETUP_DIR}/restore.sh"
|
||||||
|
# shellcheck source=scripts/setup/rollback.sh
|
||||||
|
. "${SETUP_DIR}/rollback.sh"
|
||||||
|
# shellcheck source=scripts/setup/diagnose.sh
|
||||||
|
. "${SETUP_DIR}/diagnose.sh"
|
||||||
|
|
||||||
print_header() {
|
print_header() {
|
||||||
echo "自助棋牌室系统部署菜单 ${VERSION}"
|
echo "自助棋牌室系统部署菜单 ${QIPAI_DEPLOY_VERSION}"
|
||||||
echo "生产目录: ${APP_ROOT}"
|
echo "生产目录: ${APP_ROOT}"
|
||||||
echo "固定 API: ${PUBLIC_API}"
|
echo "固定 API: ${QIPAI_API_ORIGIN}"
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
check_arch() {
|
pause_menu() {
|
||||||
local kernel_arch dpkg_arch bits
|
echo
|
||||||
kernel_arch="$(uname -m 2>/dev/null || echo unknown)"
|
printf "按回车返回菜单..."
|
||||||
dpkg_arch="$(dpkg --print-architecture 2>/dev/null || echo unknown)"
|
read -r _ || true
|
||||||
bits="$(getconf LONG_BIT 2>/dev/null || echo unknown)"
|
}
|
||||||
echo "内核架构: ${kernel_arch}"
|
|
||||||
echo "DPKG 架构: ${dpkg_arch}"
|
run_action() {
|
||||||
echo "用户空间位数: ${bits}"
|
local title="$1"
|
||||||
if [ "${kernel_arch}" != "x86_64" ] || [ "${dpkg_arch}" != "amd64" ] || [ "${bits}" != "64" ]; then
|
shift
|
||||||
echo "FAIL: 仅支持 Ubuntu 24.04 x86-64/amd64。"
|
echo
|
||||||
exit 1
|
echo "== ${title} =="
|
||||||
|
"$@"
|
||||||
|
pause_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
qipai_print_context
|
||||||
|
qipai_preflight || true
|
||||||
|
qipai_repo_status || true
|
||||||
|
if [ -f "$(qipai_current_release_file)" ]; then
|
||||||
|
qipai_info "current release manifest: $(qipai_current_release_file)"
|
||||||
|
cat "$(qipai_current_release_file)"
|
||||||
|
else
|
||||||
|
qipai_warn "current release manifest not found"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
quick_check() {
|
show_mqtt_status() {
|
||||||
echo "== 启动快检 =="
|
qipai_info "EMQX target: native Ubuntu Apt package, no Docker"
|
||||||
check_arch
|
qipai_info "MQTT broker host: 101.42.38.246"
|
||||||
command -v git >/dev/null 2>&1 && echo "Git: PASS" || echo "Git: WARN 未安装"
|
qipai_info "MQTTX on server: forbidden"
|
||||||
command -v nginx >/dev/null 2>&1 && echo "Nginx: PASS" || echo "Nginx: WARN 未安装"
|
if command -v emqx >/dev/null 2>&1; then
|
||||||
command -v node >/dev/null 2>&1 && echo "Node.js: PASS" || echo "Node.js: WARN 未安装"
|
qipai_pass "emqx command exists"
|
||||||
echo "仓库: ${REPO_SSH}"
|
emqx version 2>/dev/null || true
|
||||||
|
else
|
||||||
|
qipai_warn "emqx command not found"
|
||||||
|
fi
|
||||||
|
if command -v mosquitto_pub >/dev/null 2>&1; then
|
||||||
|
qipai_pass "mosquitto-clients available"
|
||||||
|
else
|
||||||
|
qipai_warn "mosquitto-clients not installed"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
not_implemented() {
|
show_https_status() {
|
||||||
echo "当前 M00 基线仅提供菜单骨架;该选项将在后续子阶段完善。"
|
qipai_info "domain: ${QIPAI_DOMAIN}"
|
||||||
|
qipai_info "origin: ${QIPAI_API_ORIGIN}"
|
||||||
|
qipai_info "app api: ${QIPAI_API_ORIGIN}/app-api"
|
||||||
|
qipai_info "admin api: ${QIPAI_API_ORIGIN}/admin-api"
|
||||||
|
if command -v nginx >/dev/null 2>&1; then
|
||||||
|
nginx -t
|
||||||
|
else
|
||||||
|
qipai_warn "nginx not installed"
|
||||||
|
fi
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
qipai_info "TLS live check requires public DNS/network and is not forced in M00"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main_menu() {
|
main_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
print_header
|
print_header
|
||||||
echo "1. 首次安装"
|
echo "1. 首次安装 / 初始化目录"
|
||||||
echo "2. 更新业务服务"
|
echo "2. 更新业务服务 / 生成发布清单"
|
||||||
echo "3. 安装或检查 EMQX"
|
echo "3. 安装或检查 EMQX"
|
||||||
echo "4. 配置域名与 HTTPS"
|
echo "4. 配置域名与 HTTPS / 状态检查"
|
||||||
echo "5. 查看状态"
|
echo "5. 查看状态"
|
||||||
echo "6. 备份"
|
echo "6. 备份"
|
||||||
echo "7. 恢复"
|
echo "7. 恢复"
|
||||||
@@ -56,14 +105,29 @@ main_menu() {
|
|||||||
printf "请选择: "
|
printf "请选择: "
|
||||||
read -r choice
|
read -r choice
|
||||||
case "${choice}" in
|
case "${choice}" in
|
||||||
1|2|3|4|6|7|8) not_implemented ;;
|
1) run_action "首次安装 / 初始化目录" qipai_init_layout ;;
|
||||||
5|9) quick_check ;;
|
2) run_action "更新业务服务 / 生成发布清单" qipai_deploy_business ;;
|
||||||
|
3) run_action "安装或检查 EMQX" show_mqtt_status ;;
|
||||||
|
4) run_action "配置域名与 HTTPS / 状态检查" show_https_status ;;
|
||||||
|
5) run_action "查看状态" show_status ;;
|
||||||
|
6) run_action "备份" qipai_backup ;;
|
||||||
|
7) run_action "恢复" qipai_restore ;;
|
||||||
|
8) run_action "回滚" qipai_rollback ;;
|
||||||
|
9) run_action "诊断" qipai_diagnose ;;
|
||||||
0) exit 0 ;;
|
0) exit 0 ;;
|
||||||
*) echo "无效选项" ;;
|
*) echo "无效选项"; pause_menu ;;
|
||||||
esac
|
esac
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--preflight" ]; then
|
||||||
|
qipai_preflight
|
||||||
|
elif [ "${1:-}" = "--status" ]; then
|
||||||
|
show_status
|
||||||
|
elif [ "${1:-}" = "--diagnose" ]; then
|
||||||
|
qipai_diagnose
|
||||||
|
else
|
||||||
main_menu "$@"
|
main_menu "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user