ci: 部署到腾讯云服务器并启用 Gitea Actions 自动发布
CI / verify (push) Canceled after 1m37s
CI / 部署到服务器 (push) Skipped

- ci.yml 增加 deploy job:verify 全绿后 rsync 到 /opt/kanban/dist 并做线上自检
- runner 与站点同机(host 模式),部署直接写本地目录,不依赖 SSH 密钥与 Secrets
- 站点目录是容器 bind mount,必须原地同步;manual deploy.sh 同步改为 rsync 语义
- 新增 deploy/:容器版 nginx 配置、runner 配置与 systemd 单元、服务器初始化脚本
- README 更新实际部署架构(1Panel + Nginx Proxy Manager 反代)与 runner 说明
This commit is contained in:
chenshoulin
2026-10-19 20:59:38 +08:00
parent 777815704d
commit f9bf1b3eb3
8 changed files with 446 additions and 18 deletions
+50 -5
View File
@@ -8,6 +8,10 @@ on:
permissions:
contents: read
# 站点目录(宿主路径)。Nginx 容器挂载的就是这个目录,rsync 原地同步后立即生效。
env:
DEPLOY_PATH: /opt/kanban/dist
jobs:
verify:
runs-on: ubuntu-latest
@@ -17,11 +21,10 @@ jobs:
- name: 检出代码
uses: actions/checkout@v4
- name: 准备 Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
# 这是自托管 runner(host 模式),宿主机已装 Node 22,
# 不再用 setup-node 下载一份到 tool cache,省时间和磁盘。
- name: 环境确认
run: node -v && npm -v
- name: 安装依赖
run: npm ci
@@ -37,3 +40,45 @@ jobs:
- name: 生产构建
run: npm run build
# 只在 master / main 的 push、且 verify 全绿之后部署。
# runner 与站点同机,直接写本地目录,不需要 SSH 密钥和任何 Secrets。
deploy:
name: 部署到服务器
needs: verify
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装依赖
run: npm ci
- name: 生产构建
run: npm run build
# 站点目录是容器的 bind mount,不能整目录 mv(挂载跟 inode,容器会看不到新内容),
# 必须原地同步:rsync 覆盖变化文件并删掉上一版残留的 hash 资源。
- name: 同步到站点目录
run: |
set -euo pipefail
rsync -a --delete dist/ "$DEPLOY_PATH"/
echo "已同步到 $DEPLOY_PATH"
- name: 线上自检
run: |
set -euo pipefail
for i in 1 2 3 4 5; do
curl -fsS -o /dev/null http://127.0.0.1:8081/ && break
echo "第 $i 次探测失败,1 秒后重试"
sleep 1
done
curl -fsS -o /dev/null http://127.0.0.1:8081/
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8081/today)
echo "根路径 200,/today $code"
[ "$code" = "200" ] || { echo "SPA 回落异常:/today 返回 $code"; exit 1; }
curl -fsS http://127.0.0.1:8081/sw.js >/dev/null
echo "自检通过"
+84 -13
View File
@@ -169,19 +169,61 @@ npm run build
# 将 dist/ 上传或指向静态服务器
```
Nginx 示例:
### 部署到云服务器(当前实际架构)
服务器由 1Panel 管理,80/443 已被 **Nginx Proxy Manager** 占用,所以站点不装宿主机 Nginx,
而是跑一个 `nginx:alpine` 容器,由 NPM 反代:
```nginx
server {
listen 80;
root /var/www/kanban/dist;
index index.html;
location / {
# 必须配置:/today 是真实路径,刷新时文件不存在,需要回落到 index.html
try_files $uri $uri/ /index.html;
}
}
```
浏览器 → NPM(80/443, 证书) → kanban 容器(:80) → /opt/kanban/dist
↑
CI runner 构建后 rsync 到这里(原地同步)
```
| 文件 | 用途 |
| -------------------------------------- | ---------------------------------------------------------------- |
| `deploy/nginx.docker.conf` | 容器内站点配置:SPA 回落、`sw.js` 不缓存、`/assets` 长缓存、gzip |
| `deploy/gitea-runner.config.yaml` | runner 配置:host 模式、`ubuntu-latest` 标签 |
| `deploy/gitea-runner.service` | runner 的 systemd 单元 |
| `deploy/deploy.sh` | 不走 CI 时的手动发布脚本 |
| `deploy/nginx.conf` + `server-init.sh` | 备选路径:80 端口空闲、直接用宿主机 Nginx 的机器 |
**容器启动命令**(已在服务器上运行,重建时照抄):
```bash
docker run -d --name kanban --restart unless-stopped \
--network 1panel-network \
-p 127.0.0.1:8081:80 \
-v /opt/kanban/dist:/usr/share/nginx/html:ro \
-v /opt/kanban/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
nginx:alpine
```
**反向代理**:NPM 后台 → Hosts → Proxy Hosts → Add
| 字段 | 值 |
| ---------------- | -------------------- |
| Domain Names | `kanban.shoulin.net` |
| Scheme | `http` |
| Forward Hostname | `172.17.0.1` |
| Forward Port | `8081` |
其余站点都走 `172.17.0.1:端口`,这里保持一致(用容器名 `kanban:80` 实测也可通,但容器重建后 IP 可能变)。
SSL 页签挂证书并开启 Force SSL,域名解析到服务器 IP 即可。
**手动发布**(CI 不可用时):
```bash
SERVER=root@132.232.203.88 WEB_ROOT=/opt/kanban/dist ./deploy/deploy.sh
```
> 注意:`/opt/kanban/dist` 是容器挂载目录,**不能整目录 `mv`**——bind mount 跟的是 inode,
> 换目录后容器仍指向旧内容。`deploy.sh` 与 CI 都用 rsync 原地同步,就是为了避开这个坑。
> **HTTPS 不是可选项**:Service Worker 只在 HTTPS 或 localhost 下注册成功。用 IP 走 HTTP 访问时,
> 应用能打开,但「安装到桌面 / 离线可用 / 版本更新提示」全部失效。证书在 NPM 里配置。
### 其它静态托管
Netlify / Vercel 等平台,在 `dist/` 同级放一个 `_redirects` 或配置即可:
@@ -223,9 +265,38 @@ scripts/ # 图标生成等辅助脚本
.gitea/workflows/ # CI(lint / typecheck / test / build)
```
## 持续集成
## 持续集成与自动部署
仓库内置 `.gitea/workflows/ci.yml`:推送与合并请求会依次执行 `npm run lint`、`npm run typecheck`、`npm run test`、`npm run build`。
仓库内置 `.gitea/workflows/ci.yml`,分两个 job:
| Job | 触发条件 | 做什么 |
| -------- | ------------------------------------------ | --------------------------------------------------- |
| `verify` | push 与合并请求 | `npm ci` → `lint` → `typecheck` → `test` → `build` |
| `deploy` | push 到 `master` / `main` 且 `verify` 通过 | 重新构建 → `rsync` 到 `/opt/kanban/dist` → 线上自检 |
合并请求只跑校验、不部署。部署用 `rsync -a --delete` 原地同步,失败时旧文件仍在,不会把站点弄空;
同步完会自检根路径、`/today` 回落与 `sw.js`,任一异常就让 job 变红。
### Runner(自托管,已部署)
服务器 `132.232.203.88` 上的 `tencent-sh-1`,host 模式,systemd 常驻:
```bash
systemctl status gitea-runner
journalctl -u gitea-runner -n 50 --no-pager
```
配置见 `deploy/gitea-runner.config.yaml` 与 `deploy/gitea-runner.service`,几个要点:
- 标签 `ubuntu-latest:host`,任务直接在宿主机执行,不再套一层容器
- `DEFAULT_ACTIONS_URL=https://gitea.com` —— 这台机器访问 github.com 不通,action 必须换源
- 宿主机 Node 22 在 `/opt/node22`,软链到 `/usr/local/bin`;npm 源指向 `registry.npmmirror.com`
- runner 与站点同机,部署直接写本地目录,**不需要配置任何 Secrets**
### 为什么不用 SSH 部署
runner 与站点同一台机器,绕一圈 SSH 只是多一个故障点、多一份私钥要管。
真要跨机部署,用 `deploy/deploy.sh` 的 `SERVER=... WEB_ROOT=...` 形式即可。
E2E 需要真实浏览器,默认不放进 CI(要求 runner 预装 Chrome);请在本地或用带浏览器的 runner 执行 `npm run test:e2e`。
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# 一键发布到云服务器:本地构建 → 上传 → 原地同步
#
# 用法:
# SERVER=root@132.232.203.88 ./deploy/deploy.sh
# SERVER=root@1.2.3.4 WEB_ROOT=/var/www/kanban SSH_PORT=2222 ./deploy/deploy.sh
#
# 环境变量:
# SERVER 远端 SSH 目标(必填),如 root@1.2.3.4
# WEB_ROOT 远端站点目录,默认 /opt/kanban/dist(容器挂载的就是这里)
# SSH_PORT SSH 端口,默认 22
# SKIP_BUILD 设为 1 时跳过本地构建,直接发布现有 dist/
#
# 注意:站点目录是容器的 bind mount,必须「原地同步」。
# 整目录 mv 之后容器仍指向旧 inode,看不到新文件 —— 所以这里只用 rsync / tar 解包原地覆盖。
set -euo pipefail
# 无论从哪里调用,都切到仓库根目录
cd "$(dirname "$0")/.."
SERVER="${SERVER:-}"
WEB_ROOT="${WEB_ROOT:-/opt/kanban/dist}"
SSH_PORT="${SSH_PORT:-22}"
SKIP_BUILD="${SKIP_BUILD:-0}"
if [ -z "$SERVER" ]; then
echo "缺少 SERVER。示例:SERVER=root@1.2.3.4 $0" >&2
exit 1
fi
if [ "$SKIP_BUILD" != "1" ]; then
echo "==> 本地构建"
npm run build
fi
if [ ! -f dist/index.html ]; then
echo "dist/index.html 不存在,构建可能失败了" >&2
exit 1
fi
echo "==> 远端准备目录 $WEB_ROOT"
ssh -p "$SSH_PORT" "$SERVER" "mkdir -p '$WEB_ROOT'"
if command -v rsync >/dev/null 2>&1; then
echo "==> rsync 原地增量同步"
rsync -az --delete -e "ssh -p $SSH_PORT" dist/ "$SERVER:$WEB_ROOT/"
else
# Windows / Git Bash 一般没有 rsync:上传到远端临时目录,再在远端原地同步
echo "==> tar 上传后远端原地同步(本地未检测到 rsync)"
STAMP=$(date +%Y%m%d%H%M%S)
STAGE="/tmp/kanban-release-$STAMP"
ssh -p "$SSH_PORT" "$SERVER" "rm -rf '$STAGE' && mkdir -p '$STAGE'"
tar -czf - -C dist . | ssh -p "$SSH_PORT" "$SERVER" "tar -xzf - -C '$STAGE'"
ssh -p "$SSH_PORT" "$SERVER" "
set -e
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete '$STAGE/' '$WEB_ROOT/'
else
# 没有 rsync 时退化为「清空 + 解包」,会有毫秒级空窗
find '$WEB_ROOT' -mindepth 1 -delete
cp -a '$STAGE/.' '$WEB_ROOT/'
fi
rm -rf '$STAGE'
"
fi
echo "==> 发布完成:$SERVER:$WEB_ROOT"
echo " 验证:curl -I http://<域名或 IP>/today # 期望 200,而不是 404"
+21
View File
@@ -0,0 +1,21 @@
# Gitea Runner 配置(服务端 /opt/act_runner/config.yaml)
#
# 宿主模式(host):任务直接在本机执行,不额外起容器。
# 本机已装 Node 22,所以 ubuntu-latest 标签映射到 host 而不是 docker 镜像。
# action 源通过 systemd 的 DEFAULT_ACTIONS_URL 指向 gitea.com(本机访问 github.com 不通)。
log:
level: info
runner:
file: .runner
capacity: 1
timeout: 3h
labels:
- 'ubuntu-latest:host'
fetch_timeout: 5s
fetch_interval: 2s
# 单机部署不需要 runner 自带缓存服务,关掉避免额外端口
cache:
enabled: false
+20
View File
@@ -0,0 +1,20 @@
[Unit]
Description=Gitea Actions Runner (tencent-sh-1)
Documentation=https://docs.gitea.com/usage/actions/act-runner
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/act_runner
Environment=HOME=/root
# 本机访问 github.com 不通,action 改从 gitea.com 拉取
Environment=DEFAULT_ACTIONS_URL=https://gitea.com
ExecStart=/usr/local/bin/gitea-runner daemon
Restart=always
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
+74
View File
@@ -0,0 +1,74 @@
# 看板 · 个人任务管理 —— Nginx 站点配置
#
# 用法(Ubuntu / Debian):
# sudo cp deploy/nginx.conf /etc/nginx/conf.d/kanban.conf
# sudo vim /etc/nginx/conf.d/kanban.conf # 改 server_name
# sudo nginx -t && sudo systemctl reload nginx
#
# CentOS / RHEL 把文件放到 /etc/nginx/conf.d/kanban.conf 同样可用。
server {
listen 80;
listen [::]:80;
server_name kanban.example.com; # ← 改成你的域名或服务器 IP
# deploy.sh 默认就发布到这个目录
root /var/www/kanban;
index index.html;
charset utf-8;
access_log /var/log/nginx/kanban.access.log;
error_log /var/log/nginx/kanban.error.log;
# ---------- 缓存策略(PWA 能否正常更新,全看这三条) ----------
# 构建产物文件名带 hash,可以长期强缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Service Worker 绝对不能缓存,否则发布后客户端永远停在旧版本
location = /sw.js {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# index.html 引用着带 hash 的资源名,同样不能缓存
location = /index.html {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location = /manifest.webmanifest {
default_type application/manifest+json;
expires off;
add_header Cache-Control "no-cache";
}
# ---------- 前端路由 ----------
# /today 是真实路径,刷新时磁盘上没有对应文件,必须回落到 index.html。
# 少了这条,直接访问或刷新 /today 会 404。
location / {
try_files $uri $uri/ /index.html;
}
# ---------- 传输压缩 ----------
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
application/javascript
application/json
application/manifest+json
image/svg+xml;
# ---------- 配好 HTTPS 之后 ----------
# 1. 把上面的 listen 改成 443 ssl,并补上证书配置(或用 certbot 自动改)
# 2. 取消下面这行注释,让 80 端口跳转到 443
# return 301 https://$host$request_uri;
}
+53
View File
@@ -0,0 +1,53 @@
# 看板 · 容器内站点配置
#
# 由 kanban 容器挂载到 /etc/nginx/conf.d/default.conf。
# 站点根目录是容器内的 /usr/share/nginx/html,对应宿主机 /opt/kanban/dist。
# 宿主机不需要装 Nginx —— 80/443 由已有的 Nginx Proxy Manager 接管,反代到本容器。
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
charset utf-8;
# 构建产物带 hash,长期强缓存(只加一条 Cache-Control,避免与 expires 叠加)
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# Service Worker 与入口 HTML 不能缓存,否则发布后客户端停在旧版本
location = /sw.js {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location = /index.html {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location = /manifest.webmanifest {
default_type application/manifest+json;
expires off;
add_header Cache-Control "no-cache";
}
# 前端路由:/today 刷新时磁盘上没有该文件,必须回落
location / {
try_files $uri $uri/ /index.html;
}
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
application/javascript
application/json
application/manifest+json
image/svg+xml;
}
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# 目标服务器初始化(只需执行一次)
#
# 安装 Nginx、建站点目录、写入 deploy/nginx.conf 对应的站点配置并 reload。
# 不会动 Nginx 里已有的其它站点。
#
# 用法(本机执行):
# scp deploy/server-init.sh deploy/nginx.conf root@<服务器IP>:/tmp/kanban-deploy/
# ssh root@<服务器IP> 'bash /tmp/kanban-deploy/server-init.sh kanban.example.com'
#
# 参数:
# $1 域名或 IP,写进 server_name;省略则沿用 nginx.conf 里的值
# 环境变量:
# WEB_ROOT 站点目录,默认 /var/www/kanban(要和 CI 里的 DEPLOY_PATH 一致)
set -euo pipefail
DOMAIN="${1:-}"
WEB_ROOT="${WEB_ROOT:-/var/www/kanban}"
CONF_DEST=/etc/nginx/conf.d/kanban.conf
CONF_SRC="$(cd "$(dirname "$0")" && pwd)/nginx.conf"
if [ "$(id -u)" -ne 0 ]; then
echo "请用 root 执行(sudo bash $0 ${DOMAIN:-})" >&2
exit 1
fi
if [ ! -f "$CONF_SRC" ]; then
echo "找不到 $CONF_SRC —— server-init.sh 和 nginx.conf 需要放在同一目录" >&2
exit 1
fi
echo "==> 安装 Nginx"
if command -v nginx >/dev/null 2>&1; then
echo " 已安装:$(nginx -v 2>&1)"
elif command -v apt-get >/dev/null 2>&1; then
apt-get update -qq
apt-get install -y nginx
elif command -v dnf >/dev/null 2>&1; then
dnf install -y nginx
else
yum install -y nginx
fi
echo "==> 创建站点目录 $WEB_ROOT"
mkdir -p "$WEB_ROOT"
echo "==> 写入站点配置 $CONF_DEST"
if [ -n "$DOMAIN" ]; then
# 只替换 server_name 那一行,其余配置保持与仓库一致
sed -E "s/^([[:space:]]*)server_name[^;]*;/\1server_name ${DOMAIN};/" "$CONF_SRC" > "$CONF_DEST"
else
cp "$CONF_SRC" "$CONF_DEST"
fi
nginx -t
systemctl enable nginx >/dev/null 2>&1 || true
systemctl reload nginx 2>/dev/null || systemctl restart nginx
if systemctl is-active --quiet firewalld 2>/dev/null; then
echo "==> firewalld 放行 80/443"
firewall-cmd --permanent --add-service=http >/dev/null
firewall-cmd --permanent --add-service=https >/dev/null
firewall-cmd --reload >/dev/null
fi
echo
echo "完成。站点根目录:$WEB_ROOT"
echo "server_name:${DOMAIN:-沿用 nginx.conf 默认值}"
echo
echo "还需要做两件事:"
echo " 1. 腾讯云控制台 → 防火墙/安全组,放行 80(上 HTTPS 后再放行 443)"
echo " 2. 把 CI 部署用的公钥追加到 ~/.ssh/authorized_keys"