ci deploy (src 255f279)
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* -text
|
||||||
45
app-entry.sh
Normal file
45
app-entry.sh
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# sgame-admin-server 容器守护入口(alpine 容器内由 busybox sh 执行)。
|
||||||
|
# /repo 是 gitsync 容器同步下来的部署仓(只读挂载),里面有:
|
||||||
|
# server — linux/amd64 静态二进制(前端已 embed,时区已内嵌)
|
||||||
|
# config.yaml — 基础配置(敏感项全部由环境变量覆盖)
|
||||||
|
# app-entry.sh — 本脚本
|
||||||
|
# 逻辑:启动 server → 每 10s 比对仓库里二进制的 md5 → 变了就杀进程重启。
|
||||||
|
# 进程崩溃同样会回到循环顶部自动拉起。
|
||||||
|
|
||||||
|
BIN=/repo/server
|
||||||
|
RUN=/tmp/server.run
|
||||||
|
|
||||||
|
echo "[entry] waiting for $BIN ..."
|
||||||
|
while [ ! -f "$BIN" ]; do sleep 2; done
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# 等 gitsync 的 checkout 落定:两次校验和一致才启动
|
||||||
|
SUM=$(md5sum "$BIN" | cut -d' ' -f1)
|
||||||
|
sleep 1
|
||||||
|
SUM2=$(md5sum "$BIN" | cut -d' ' -f1)
|
||||||
|
if [ "$SUM" != "$SUM2" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 跑 /tmp 副本而不是 /repo 原件,避免 git reset 覆盖运行中的可执行文件
|
||||||
|
cp "$BIN" "$RUN"
|
||||||
|
chmod +x "$RUN"
|
||||||
|
echo "[entry] starting server (md5 $SUM)"
|
||||||
|
"$RUN" -config /repo/config.yaml &
|
||||||
|
PID=$!
|
||||||
|
|
||||||
|
while kill -0 "$PID" 2>/dev/null; do
|
||||||
|
sleep 10
|
||||||
|
NEW=$(md5sum "$BIN" 2>/dev/null | cut -d' ' -f1)
|
||||||
|
if [ -n "$NEW" ] && [ "$NEW" != "$SUM" ]; then
|
||||||
|
echo "[entry] new binary detected, restarting"
|
||||||
|
kill "$PID"
|
||||||
|
wait "$PID" 2>/dev/null
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[entry] server exited, relaunching in 2s"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
44
ci-runner-entry.sh
Normal file
44
ci-runner-entry.sh
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Gitea Actions runner 容器入口(在 node:20-bookworm 镜像内执行)。
|
||||||
|
# NAS compose 的 ci-runner 服务启动时从公开产物仓 curl 本脚本执行——
|
||||||
|
# 改本文件 + 发版 + 重启 ci-runner 容器即可更新 runner 逻辑。
|
||||||
|
# /data 是持久卷:Go 工具链、模块/构建缓存、act_runner 注册状态都在里面,
|
||||||
|
# 容器重建不丢。需要环境变量 RUNNER_REG_TOKEN(仅首次注册时使用)。
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# runner v1.x 要求 go>=1.26;1.26 向下兼容编译 go.mod 1.25 的项目
|
||||||
|
GO_VERSION=1.26.4
|
||||||
|
# v1.x 起模块改名 gitea.com/gitea/runner(旧名 act_runner 装不上),二进制名也是 runner
|
||||||
|
RUNNER_VERSION=v1.0.8
|
||||||
|
|
||||||
|
export GOPATH=/data/gopath
|
||||||
|
export GOMODCACHE=/data/gomod
|
||||||
|
export GOCACHE=/data/gocache
|
||||||
|
export GOPROXY=https://goproxy.cn,direct
|
||||||
|
export PATH=/data/go/bin:$GOPATH/bin:$PATH
|
||||||
|
|
||||||
|
if [ ! -x /data/go/bin/go ]; then
|
||||||
|
echo "[ci-runner] downloading go $GO_VERSION ..."
|
||||||
|
curl -fsSL -o /tmp/go.tgz "https://mirrors.aliyun.com/golang/go${GO_VERSION}.linux-amd64.tar.gz"
|
||||||
|
tar -C /data -xzf /tmp/go.tgz
|
||||||
|
rm /tmp/go.tgz
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$GOPATH/bin/runner" ]; then
|
||||||
|
echo "[ci-runner] installing gitea runner $RUNNER_VERSION ..."
|
||||||
|
go install "gitea.com/gitea/runner@${RUNNER_VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /data/runner
|
||||||
|
cd /data/runner
|
||||||
|
if [ ! -f .runner ]; then
|
||||||
|
echo "[ci-runner] registering ..."
|
||||||
|
runner register --no-interactive \
|
||||||
|
--instance https://git.xindiegaming.top \
|
||||||
|
--token "$RUNNER_REG_TOKEN" \
|
||||||
|
--name nas-runner \
|
||||||
|
--labels nas:host
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[ci-runner] daemon up"
|
||||||
|
exec runner daemon
|
||||||
41
config.yaml
Normal file
41
config.yaml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
server:
|
||||||
|
port: 8090
|
||||||
|
log_level: info # debug, info, warn, error;留空默认 info
|
||||||
|
tls_cert: "" # TLS 证书路径(留空则以 HTTP 启动;生产由 Caddy 终结 TLS)
|
||||||
|
tls_key: ""
|
||||||
|
# timezone: "Asia/Shanghai" # 可选 IANA 时区,决定看板"按日"统计边界与前端时间显示;留空默认 Asia/Shanghai
|
||||||
|
|
||||||
|
# SQLite 数据文件。admin / game 分成两个文件(沿用原先 MySQL 双 schema 的划分):
|
||||||
|
# 玩家上报的数据独占 game 库,可以整个清空或拷走而不碰管理员账号。
|
||||||
|
#
|
||||||
|
# 2026-07 由 MySQL+Redis 换成 SQLite:服务从 NAS 迁到 512MB 的 VPS,
|
||||||
|
# 光 MySQL 就要 ~400MB 常驻内存,而全部数据量以 MB 计。
|
||||||
|
db:
|
||||||
|
admin_path: "data/admin.db" # admins
|
||||||
|
game_path: "data/game.db" # feedbacks / events / devices / daily_active / daily_stats / demo_markers
|
||||||
|
busy_timeout_ms: 5000 # 写锁等待毫秒数;与 WAL 一起吸收并发写入
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: "admin-secret-change-in-production"
|
||||||
|
expire_hours: 24
|
||||||
|
|
||||||
|
# 首次启动时若 admins 表为空,用以下信息创建超级管理员(纯密码登录,必须设置 password)
|
||||||
|
init_admin:
|
||||||
|
name: "超级管理员"
|
||||||
|
phone: "13800000000"
|
||||||
|
password: ""
|
||||||
|
|
||||||
|
# 玩家侧公开上报 API(/api/v1/events|heartbeat|feedback)
|
||||||
|
ingest:
|
||||||
|
app_key: "" # 客户端内置的 X-App-Key;留空 = 关闭上报路由(503)
|
||||||
|
upload_dir: "data/uploads" # 反馈截图落盘目录
|
||||||
|
max_screenshot_kb: 2048 # 单张截图上限
|
||||||
|
events_per_min: 10 # 按 IP 限流:事件批/分钟
|
||||||
|
feedback_per_10min: 5 # 按 IP 限流:反馈/10分钟
|
||||||
|
heartbeat_per_min: 2 # 按 IP 限流:心跳/分钟
|
||||||
|
events_retention_days: 90 # 原始事件保留天数(已汇总进 daily_stats 后批量清理;0=永久保留)
|
||||||
|
|
||||||
|
# 所有字段均可被环境变量覆盖(生产用 systemd EnvironmentFile 注入),
|
||||||
|
# 见 internal/config/config.go envBindings:
|
||||||
|
# SERVER_PORT TZ ADMIN_DB_PATH GAME_DB_PATH
|
||||||
|
# JWT_SECRET INIT_ADMIN_NAME/PHONE/PASSWORD INGEST_APP_KEY INGEST_UPLOAD_DIR
|
||||||
14
sgame-admin-sync.service
Normal file
14
sgame-admin-sync.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=SGame admin 发版拉取(检查产物仓,有新构建才重启服务)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# 需要 root:install 到 /opt 并 systemctl restart 主服务
|
||||||
|
User=root
|
||||||
|
ExecStart=/opt/sgame-admin/vps-sync.sh
|
||||||
|
# 拉取脚本内部已有 git 超时,这里再兜一层,杜绝 timer 任务堆积
|
||||||
|
TimeoutStartSec=180
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=sgame-admin-sync
|
||||||
13
sgame-admin-sync.timer
Normal file
13
sgame-admin-sync.timer
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=每分钟检查一次 sgame-admin 是否有新版本
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# 开机 2 分钟后首次检查,之后每分钟一次(对齐原 NAS gitsync 的 60s 节奏)
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=1min
|
||||||
|
Unit=sgame-admin-sync.service
|
||||||
|
# 单核机器,避免和别的定时任务撞在同一秒
|
||||||
|
RandomizedDelaySec=15s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
47
sgame-admin.service
Normal file
47
sgame-admin.service
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=SGame admin server (江湖录后台:玩家反馈 + 埋点 + 看板)
|
||||||
|
Documentation=https://git.xindiegaming.top/bia/sgame-admin-server
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=sgame
|
||||||
|
Group=sgame
|
||||||
|
WorkingDirectory=/opt/sgame-admin
|
||||||
|
ExecStart=/opt/sgame-admin/server -config /opt/sgame-admin/config.yaml
|
||||||
|
# 密钥(JWT_SECRET / INIT_ADMIN_PASSWORD / INGEST_APP_KEY)走这个文件,
|
||||||
|
# 权限 600、属主 sgame,不进任何仓库。
|
||||||
|
EnvironmentFile=/opt/sgame-admin/env
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
# —— 资源闸 ——
|
||||||
|
# VPS 总共 459MB,还跑着 Caddy / frps / v2ray / vaultwarden。
|
||||||
|
# 实测常驻 ~30MB,150M 留了 5 倍余量:真涨到这个数说明有泄漏,
|
||||||
|
# 宁可让它被 OOM kill 然后自动重启,也不能把整机拖进 swap 抖动。
|
||||||
|
#
|
||||||
|
# 目标机是 CentOS 7 = systemd 219 + cgroup v1,只认 MemoryLimit;
|
||||||
|
# 新版的 MemoryMax 在这里会被静默忽略,内存闸等于没写。
|
||||||
|
MemoryLimit=150M
|
||||||
|
# 单核机器,限制 CPU 配额避免某次重聚合把 Caddy 饿死
|
||||||
|
CPUQuota=80%
|
||||||
|
TasksMax=256
|
||||||
|
|
||||||
|
# —— 加固 ——
|
||||||
|
# 同样受 systemd 219 限制:ProtectSystem=strict、ReadWritePaths、
|
||||||
|
# ProtectKernelTunables、RestrictSUIDSGID 都是 232+ 才有的,写了只会被忽略
|
||||||
|
# 并刷 warning。full 已经把 /usr /boot /etc 挂成只读,而本服务只写
|
||||||
|
# /opt/sgame-admin/data,够用。
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=true
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=sgame-admin
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
84
vps-sync.sh
Normal file
84
vps-sync.sh
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# VPS 侧发版拉取:从产物仓取最新构建,二进制有变化才重启服务。
|
||||||
|
# 由 sgame-admin-sync.timer 每分钟触发(见 deploy/vps-README.md)。
|
||||||
|
#
|
||||||
|
# 关键约束:产物仓托管在 NAS 的 Gitea 上,而 NAS 每晚 23 点关机、偶尔断网。
|
||||||
|
# 因此拉取失败一律视为「本次没有新版本」——静默保持现状继续跑,
|
||||||
|
# 绝不因为拉不到而重启或停服。线上可用性不依赖 NAS 在线。
|
||||||
|
set -u
|
||||||
|
|
||||||
|
APP_DIR=/opt/sgame-admin
|
||||||
|
REPO_DIR="$APP_DIR/repo"
|
||||||
|
REPO_URL=https://git.xindiegaming.top/bia/sgame-admin-deploy.git
|
||||||
|
# git 操作的硬超时:NAS 不可达时 TCP 可能挂很久,绝不能让 timer 堆积
|
||||||
|
GIT_TIMEOUT=60
|
||||||
|
|
||||||
|
log() { echo "sgame-admin-sync: $*"; }
|
||||||
|
|
||||||
|
# 1) 取最新产物。任何失败都直接退出 0(= 没有新版本可发)
|
||||||
|
#
|
||||||
|
# 注意:目标机是 CentOS 7,git 1.8.3.1 不支持 `git -C <dir>`,只能子 shell 里 cd。
|
||||||
|
if [ -d "$REPO_DIR/.git" ]; then
|
||||||
|
(cd "$REPO_DIR" && timeout "$GIT_TIMEOUT" git fetch --depth 1 origin main >/dev/null 2>&1) || {
|
||||||
|
log "产物仓不可达(NAS 可能已关机),保持当前版本运行"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
(cd "$REPO_DIR" && timeout "$GIT_TIMEOUT" git reset --hard origin/main >/dev/null 2>&1) || {
|
||||||
|
log "reset 失败,保持当前版本运行"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
timeout "$GIT_TIMEOUT" git clone --depth 1 "$REPO_URL" "$REPO_DIR" >/dev/null 2>&1 || {
|
||||||
|
log "首次 clone 失败(NAS 可能已关机),稍后重试"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_BIN="$REPO_DIR/server"
|
||||||
|
CUR_BIN="$APP_DIR/server"
|
||||||
|
|
||||||
|
if [ ! -f "$NEW_BIN" ]; then
|
||||||
|
log "产物仓里没有 server 二进制,跳过"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2) 内容比对:只有真的变了才重启(timer 每分钟跑,不能每次都重启)
|
||||||
|
NEW_SUM=$(md5sum "$NEW_BIN" | awk '{print $1}')
|
||||||
|
CUR_SUM=$(md5sum "$CUR_BIN" 2>/dev/null | awk '{print $1}')
|
||||||
|
if [ "$NEW_SUM" = "$CUR_SUM" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "检测到新版本 ($CUR_SUM -> $NEW_SUM),开始更新"
|
||||||
|
|
||||||
|
# 3) 就地替换 + 重启。保留上一版以便手工回滚。
|
||||||
|
cp -f "$CUR_BIN" "$APP_DIR/server.prev" 2>/dev/null || true
|
||||||
|
install -m 0755 -o sgame -g sgame "$NEW_BIN" "$CUR_BIN" || {
|
||||||
|
log "替换二进制失败,保持当前版本运行"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# config.yaml 只在产物仓有更新时覆盖;密钥不在其中(走 EnvironmentFile)
|
||||||
|
if [ -f "$REPO_DIR/config.yaml" ]; then
|
||||||
|
install -m 0644 -o sgame -g sgame "$REPO_DIR/config.yaml" "$APP_DIR/config.yaml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl restart sgame-admin
|
||||||
|
|
||||||
|
# 4) 起不来就自动回滚,避免一次坏发版把线上撂倒一整夜
|
||||||
|
sleep 3
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
if curl -fsS --max-time 3 http://127.0.0.1:8090/health >/dev/null 2>&1; then
|
||||||
|
log "更新完成,健康检查通过"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
log "新版本健康检查失败,回滚到上一版"
|
||||||
|
if [ -f "$APP_DIR/server.prev" ]; then
|
||||||
|
install -m 0755 -o sgame -g sgame "$APP_DIR/server.prev" "$CUR_BIN"
|
||||||
|
systemctl restart sgame-admin
|
||||||
|
log "已回滚"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
Reference in New Issue
Block a user