从本地开发到一句话上线:AI + GitHub Actions + 宝塔自动部署

网站在本地改好了,接下来又是压缩文件、上传宝塔、解压覆盖、重启服务……

一次还好,每次更新都重复,确实有点烦。😵‍💫

所以,我给自己的项目接上了一条自动部署流程。配置好以后,只需要告诉 AI:

帮我提交代码,推送并触发部署,完成后检查网站是否正常。

AI 就能通过已授权的开发工具完成提交、推送、查看部署结果;GitHub Actions 负责构建并更新服务器。

这篇文章记录整个过程,也包括第一次部署失败时踩到的坑。🛠️

🔒 本文所有域名、目录、仓库名均使用示例,不包含真实服务器信息或密钥。示例适用于 Node.js + Vite + PM2 项目;其他项目需要调整构建命令、部署文件和健康检查地址。

这篇讨论的是带 Node.js 服务端的 Vite 应用。纯静态 Vite 或 Hexo 网站通常由 Nginx 直接提供构建产物,不需要 PM2;请先确认项目类型再套用流程。

🧩 一、整条流程怎么工作?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
👤 提出修改和上线需求
↓
🤖 AI 修改本地代码并验证
↓
📦 推送到 GitHub 私有仓库
↓
⚙️ GitHub Actions 安装依赖、构建
↓
🔐 SSH 上传到服务器
↓
🔄 更新文件并重启 PM2
↓
🩺 检查健康接口
↓
🌍 网站更新完成

这里几个工具各有分工:

工具 作用
AI 开发助手 修改代码、运行命令、推送和排查错误
GitHub 私有仓库 保存源码和部署配置
GitHub Actions 自动执行部署
云服务器 持续运行应用
宝塔 / Nginx 管理域名、HTTPS 和反向代理
PM2 管理 Node.js 进程

第一次需要把权限和流程配好,之后更新就省事多了。✨

🔍 二、先查清楚网站现在怎么运行

我的网站已经运行了一段时间,因此第一步不是重装,而是确认现有配置。

📍 在服务器终端执行:

1
2
3
node --version
npm --version
pm2 list

然后查看目标应用:

1
pm2 describe my-web-app

重点记录:

  • exec cwd:项目实际目录。
  • script path、script args:启动方式。
  • node.js version:实际使用的 Node.js 版本。
  • status:当前进程状态。

例如,这类项目可能通过以下命令启动:

1
npm run server

再检查网站目录:

1
2
3
cd /www/wwwroot/example.com
git status
git remote -v

如果出现:

1
fatal: not a git repository

说明服务器上的文件不是 Git 工作目录。之前通过压缩包上传的网站很常见,不代表网站有问题。

这次就采用 Actions 构建后上传文件 的方式,不要求服务器执行 git pull。

💡 还有一个容易误会的地方:网站出现在宝塔的“PHP 项目”列表里,不足以说明应用使用 PHP。要结合启动命令和反向代理配置判断实际运行方式。

🛟 三、先备份,再自动化

正式更新前,先给现有网站做一个备份。

📍 在服务器终端执行,先修改第一行目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
APP_DIR="/www/wwwroot/example.com"
BACKUP_DIR="/root/site-backups"

mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"

BACKUP_FILE="$BACKUP_DIR/site-before-$(date +%Y%m%d-%H%M%S).tar.gz"

tar \
--exclude='./node_modules' \
--exclude='./.cache' \
-czf "$BACKUP_FILE" \
-C "$APP_DIR" .

chmod 600 "$BACKUP_FILE"
printf 'Backup created: %s\n' "$BACKUP_FILE"

这个备份保留构建文件和配置,排除了体积较大的依赖目录。

⚠️ 备份可能包含服务器 .env,因此只留在服务器的受限目录里,不要提交到 GitHub。

同时明确更新边界:

  • .env 继续留在服务器。
  • 数据库、上传文件和运行时数据不属于发布包。
  • 只重启当前网站,不重启同服务器上的其他服务。

🔐 四、配置部署密钥和 Secrets

GitHub Actions 需要通过 SSH 连接服务器,因此要准备一把专用部署密钥。

📍 在可信的 Linux / Bash 终端执行:

1
2
3
4
ssh-keygen \
-t ed25519 \
-C "github-actions-deploy" \
-f "$HOME/.ssh/github_actions_deploy"

示例工作流没有配置私钥口令,生成时口令留空。生成的两个文件分别是:

1
2
github_actions_deploy       私钥:配置到 GitHub Secrets
github_actions_deploy.pub 公钥:安装到服务器

用自己的服务器地址替换后,安装公钥:

1
2
3
4
ssh-copy-id \
-i "$HOME/.ssh/github_actions_deploy.pub" \
-p 22 \
DEPLOY_USER@YOUR_SERVER_IP

这里的 DEPLOY_USER 必须有权限更新网站目录,并管理对应 PM2 进程。已有应用属于哪个用户,就需要考虑该用户的 PM2 环境;随意换用户可能看到一个空的进程列表。

接着,在 GitHub 仓库打开:

Settings → Secrets and variables → Actions → New repository secret

添加:

Secret 名称 内容
SERVER_HOST 服务器地址
SERVER_PORT SSH 端口
SERVER_USER 部署用户
SERVER_SSH_KEY 专用部署私钥的完整内容
SERVER_FINGERPRINT 经可信渠道确认的 SSH 主机 SHA256 指纹

在服务器控制台执行 ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256,将确认过的 SHA256:... 值保存为 SERVER_FINGERPRINT。这是服务器主机指纹,与部署公钥不同;两处 SSH Action 都应填写。配置方法见 SSH Action 官方说明 和 SCP Action 参数。

私钥只填写到 Secrets,不放在代码、聊天记录或截图里。🔒

📦 五、确认仓库不会带上个人数据

提交前,检查忽略规则,至少排除:

1
2
3
4
5
6
7
8
node_modules/
dist/
.env
.env.*
!.env.example
.cache/
*.zip
*.tar.gz

这是基础示例;项目如果有上传目录、数据库或下载目录,也应分别排除。

📍 在本地项目的 Bash 终端执行:

1
2
3
git status --short
git ls-files '.env' '.env.*'
git diff --stat

注意:.gitignore 不会自动取消已经跟踪的文件。如果真实密钥曾经进入提交,删除当前文件也不能清除历史泄露,需要更换密钥并处理历史。

我的另一个实际插曲是:部署完才发现仓库是公开的。😅

如果不准备开源,建议一开始就创建私有仓库。网站公开访问与源码是否公开,是两回事。

⚙️ 六、添加 GitHub Actions 工作流

在项目里创建:

1
.github/workflows/deploy.yml

下面是整理后的示例。使用前调整:

  • 触发分支 main。
  • 服务器目录。
  • PM2 应用名称。
  • 上传文件列表。
  • 健康接口和端口。

项目必须存在对应的 build 脚本,并提交一致的 package-lock.json。示例保留原稿使用的 Action 版本标签,实际接入时应检查维护状态与运行器兼容性。示例沿用服务器现有运行环境,Actions 的 Node.js 22 不会自动升级服务器上的 Node.js。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
name: Deploy website

on:
workflow_dispatch:
push:
branches:
- main

concurrency:
group: website-production
cancel-in-progress: false

permissions:
contents: read

jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Upload release
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
fingerprint: ${{ secrets.SERVER_FINGERPRINT }}
source: "dist,server,package.json,package-lock.json"
target: "/tmp/website-release-${{ github.run_id }}-${{ github.run_attempt }}"
overwrite: true

- name: Update application
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
fingerprint: ${{ secrets.SERVER_FINGERPRINT }}
command_timeout: 15m
script: |
set -eu
umask 077

APP_DIR="/www/wwwroot/example.com"
RELEASE_DIR="/tmp/website-release-${{ github.run_id }}-${{ github.run_attempt }}"
BACKUP_DIR="$HOME/site-backups"
APP_NAME="my-web-app"
HEALTH_URL="http://127.0.0.1:5175/api/health"

test -d "$APP_DIR"
test -d "$RELEASE_DIR/dist"
test -d "$RELEASE_DIR/server"
test -f "$RELEASE_DIR/package.json"
test -f "$RELEASE_DIR/package-lock.json"

mkdir -p "$BACKUP_DIR"

tar \
--exclude='./node_modules' \
--exclude='./.cache' \
-czf "$BACKUP_DIR/site-$(date +%Y%m%d-%H%M%S).tar.gz" \
-C "$APP_DIR" .

cp -a "$RELEASE_DIR/dist" "$APP_DIR/"
cp -a "$RELEASE_DIR/server" "$APP_DIR/"
cp "$RELEASE_DIR/package.json" "$APP_DIR/"
cp "$RELEASE_DIR/package-lock.json" "$APP_DIR/"

cd "$APP_DIR"
pm2 stop "$APP_NAME"
npm ci --no-audit --no-fund
pm2 restart "$APP_NAME" --update-env

READY=0
for attempt in $(seq 1 30); do
if curl \
--fail \
--silent \
--show-error \
--connect-timeout 2 \
--max-time 5 \
"$HEALTH_URL"; then
READY=1
break
fi
sleep 2
done

if [ "$READY" -ne 1 ]; then
echo "Health check failed."
exit 1
fi

服务端改用 npm ci,让依赖严格遵循锁文件;锁文件不一致时会失败,而不会现场修改锁文件。它会移除旧 node_modules,因此示例先停止目标应用再安装。此处保留完整依赖,运行时依赖整理清楚后才考虑 --omit=dev。参见 npm ci 官方文档。

SSH 非交互会话也要能找到 node、npm 和 pm2。宝塔或 nvm 管理的 Node.js 可能需要显式设置 PATH,并使用现有进程所属用户的 PM2 环境。

📌 这是一套基础覆盖部署方案,停机时间包含依赖安装和重启,也不会自动删除服务器上已经废弃的旧文件。它还没有实现原子切换或自动回滚。

依赖安装或健康检查失败时,工作流只会报错,服务可能保持停止或异常状态。首次采用前应演练恢复:保留失败版本,恢复备份里的程序与锁文件,重新安装依赖,再启动同一个 PM2 应用并检查接口。备份排除了依赖,不是直接解压就能完成回滚;数据库还需单独备份。

cp -a 会合并目录,已废弃的旧文件仍可能存在;并且覆盖期间现有服务可能读到新旧混合内容。需要可靠切换时,应使用独立发布目录,而不是把这个示例当成零停机方案。临时发布目录和备份也需要按明确的保留策略定期清理。

如果服务端还依赖其他配置、静态资源或本地模块,需要补充上传列表,不能只照搬文件名。

🚀 七、提交并触发部署

📍 在本地项目 Bash 终端执行:

1
2
git status --short
git diff

确认内容后,提交部署文件:

1
2
3
4
git add .github/workflows/deploy.yml
git diff --cached
git commit -m "ci: add automatic deployment"
git push origin main

如果实际使用其他分支,工作流和推送命令要一起调整。

随后打开 GitHub 仓库的 Actions,查看运行过程:

1
2
3
4
5
Checkout ✅
Install dependencies ✅
Build ✅
Upload release ✅
Update application ✅

也可以让已经授权的 GitHub CLI 查看:

1
gh run list --limit 5

查看某次运行:

1
gh run view RUN_ID

查看失败日志:

1
gh run view RUN_ID --log-failed

💥 八、真实踩坑:PM2 online,为什么部署还失败?

第一次运行时,构建、上传和 PM2 重启都成功了,但健康检查失败:

1
curl: (7) Failed to connect to 127.0.0.1 port 5175

原来的脚本是:

1
2
3
pm2 restart my-web-app --update-env
sleep 3
curl --fail http://127.0.0.1:5175/api/health

问题在于:PM2 已经拉起进程,不代表应用已经开始监听端口。 ⏳

因此把固定等待改为多次重试。后一次部署中,第一次连接仍然失败,稍后就返回了:

1
{"ok":true}

这次部署最终通过。✅

不过,连接失败也可能来自启动异常、端口配置错误等原因。如果持续失败,应查看服务器日志,而不是无限延长等待:

1
2
pm2 describe my-web-app
pm2 logs my-web-app --nostream --lines 80

日志应在可信环境里查看,分享前检查是否包含敏感信息。

🩺 九、怎样才算真的上线成功?

不能只看“代码推送成功”。还需要确认:

  1. GitHub Actions 完成且成功。
  2. 健康接口返回预期内容。
  3. 网站首页可以打开。
  4. 实际页面出现本次更新。

📍 在能访问网站的 Bash 终端执行:

1
2
curl --fail --show-error \
https://example.com/api/health

预期:

1
{"ok":true}

工作流中的 curl --fail 只检查 HTTP 错误,尚未断言 JSON 中的 ok。如果接口可能返回 HTTP 200 但 ok: false,应添加 JSON 断言。只有再核对本次页面变化或提交版本,才能确认没有命中旧服务。

检查首页:

1
2
3
4
curl --silent --show-error \
--output /dev/null \
--write-out 'HTTP %{http_code}\n' \
https://example.com/

预期:

1
HTTP 200

健康接口通过代表基础服务可用,股票接口、图片分析等业务功能仍需要单独检查。🔎

🤖 十、以后怎么让 AI 帮我上线?

完成首次配置,并让 AI 开发工具获得本地项目和 GitHub 的授权后,可以使用这段提示词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
请将当前项目更新部署到服务器。

先阅读 PROJECT_STATUS.md,检查 Git 状态和本次变更。
不要覆盖、删除或重置我的未提交文件。

运行项目要求的构建和必要测试。
检查提交范围,不要提交 .env、私钥、Token、Cookies、
数据库、下载文件和其他个人数据。

验证通过后,提交本次更新并推送到已配置的部署分支,
触发 GitHub Actions。

等待部署完成。失败时读取真实日志,定位并修复问题,
不要通过删除测试或跳过验证来伪造成功。

部署后检查网站首页、健康接口和本次修改。
更新 PROJECT_STATUS.md。

最后报告提交版本、部署运行链接、验证结果和未解决的问题。

这句话背后真正发挥作用的是:可执行的部署脚本、预先配置的权限,以及明确的验证标准。 🧠

AI 可以把这些步骤串起来,还能在失败时继续查看日志、修改流程、重新验证。

🌱 十一、后续还可以继续完善什么?

这次已经打通了从本地推送到服务器更新的流程。下一步可以继续增加:

  • 🧪 部署前自动运行项目测试。
  • ↩️ 健康检查失败后自动回滚。
  • 📦 每次发布使用独立目录,通过软链接切换版本。
  • 🔐 进一步限制专用部署账户权限,并将第三方 Actions 固定到审查过的提交 SHA。
  • 🔔 推送部署成功或失败通知。
  • 🏷️ 在健康接口返回版本号,确认线上确实是最新提交。

现在,再修改网站时,不必每次打开宝塔传压缩包了。告诉 AI 更新并部署,随后查看真实运行结果,就能完成一次可追踪的上线。🚀