跳转至

TimeTrace 一次性部署清单(手把手)

这份文档不会进 git.git/info/exclude 已配置)。 看完做完照着上面打勾,然后 rm DEPLOY-CHECKLIST.md 即可。

仅你自己在自己机器上操作;不要把里面的 token / 私钥贴给别人,包括我。


0. 一分钟科普 SSH 公私钥(看一遍就够了)

每个 SSH 用户都有一对 key:

  • 私钥 (private key):文件名常见 id_ed25519id_rsa永远不要给任何人看,不要传到公网,不要进 git。这是"你的身份"。
  • 公钥 (public key):文件名加 .pub 后缀,如 id_ed25519.pub可以随便贴在哪里,包括 GitHub Settings、远端的 ~/.ssh/authorized_keys、Stack Overflow。它只能用来验证带着对应私钥的人,不能反推私钥。

登录远端流程是这样的:

你本机:     有 私钥 id_ed25519
小主机:     ~/.ssh/authorized_keys 里有 你的 id_ed25519.pub

→ ssh 小主机
→ 远端发挑战:"你拿私钥签个数字给我看"
→ 本机用私钥签
→ 远端用授权过的公钥验签 → 通过 → 放你进去

"为什么要两把 key"(你的开发 key + CI 专用 key):

  • 开发 key(已存在的 ~/.ssh/id_ed25519 之类):你本人用,带口令保护
  • CI 专用 key(这次新生成的 timetrace_deploy):GitHub Actions 用,无口令(runner 自动跑不可能输密码)

CI key 单独一把的好处:

  1. 万一 GitHub Secrets 泄露 / 你想吊销,只 revoke 这一把,不影响你日常 SSH
  2. GitHub Actions 拿不到带口令的 key(unlock 需要你输密码)
  3. 审计清晰:"哪次部署用的哪把 key"一目了然

1. 本机准备(PowerShell)

1.1 确认 gh 已可用

打开新的 PowerShell 窗口(你之前装完 gh 没重启终端 → 旧窗口看不到):

gh --version
gh auth status

期望看到:

github.com
  ✓ Logged in to github.com account Vanilla-Yukirin

如果 gh: command not found:重启 PowerShell;还不行就 winget install --id GitHub.cli 再试。

1.2 看你的 ~/.ssh 里有哪些 key

ls $HOME\.ssh

期望看到至少一对开发 key,比如 id_ed25519 + id_ed25519.pub。 如果连开发 key 都没有:

ssh-keygen -t ed25519 -f $HOME\.ssh\id_ed25519 -C "Yuki dev key"
# 提示 passphrase 时建议输一个,回车两次也可(开发 key 无口令风险中等)

2. 用密码登录小主机一次(最后一次用密码)

你之前一直密码登录小主机,所以下面假设 ~/.ssh/config 里已经有 tt-rb4g 这类条目,可以直接:

ssh tt-rb4g
# 输密码登录
whoami        # 应该返回 "Yuki"(或你的小主机用户名)
exit

如果 ~/.ssh/config 里没配过 tt-rb4g,可以临时直连:

ssh Yuki@121.x.x.x -p <FRP 映射的小主机 SSH 端口>
# 第一次会问 "Are you sure you want to continue connecting (yes/no)?" → yes

记下三个数字(后面 GitHub Secrets 会用到):

名称 取值方法
跳板 IP tt-rb4g~/.ssh/config 里指向的那个 121.x.x.x;或 gci $HOME\.ssh\configHostName
跳板 SSH 端口 通常 22。如果你 FRP 改过就用你改的
FRP 映射到小主机的端口 你在 frpc.toml 里给小主机 22 端口配的 remote_port,例如 22001

如果忘了 FRP 端口,登录到云服务器上看 frps.tomlss -tlnp | grep frps 查找。


3. 把你的开发公钥装到小主机(一次性,告别密码)

Windows PowerShell 没有内置 ssh-copy-id。两种办法:

办法 A:Git Bash 里跑(推荐)

ssh-copy-id tt-rb4g
# 它会用密码登一次,然后追加你的本机公钥到小主机的 ~/.ssh/authorized_keys

办法 B:PowerShell 一行命令模拟

Get-Content $HOME\.ssh\id_ed25519.pub | ssh tt-rb4g "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

验证:

ssh tt-rb4g whoami     # 不再提示密码 → 成功

4. 生成 CI 专用 keypair(无口令)

ssh-keygen -t ed25519 -f $HOME\.ssh\timetrace_deploy -C "timetrace-deploy" -N '""'

参数说明: - -t ed25519:算法(比 RSA 更短更快更安全) - -f $HOME\.ssh\timetrace_deploy:输出文件名(会得到两个文件:timetrace_deploy 私钥 + timetrace_deploy.pub 公钥) - -C "timetrace-deploy":注释,方便日后辨认这把 key 是哪用的 - -N '""'空口令(CI runner 无法输密码)。PowerShell 语法注意是 '""',外单引号 + 内双引号

生成后看一眼:

ls $HOME\.ssh\timetrace_deploy*
# timetrace_deploy        ← 私钥,绝对不要传出去
# timetrace_deploy.pub    ← 公钥,可以贴

5. 把 CI 公钥装到小主机的 authorized_keys

Get-Content $HOME\.ssh\timetrace_deploy.pub | ssh tt-rb4g "cat >> ~/.ssh/authorized_keys"

验证 CI key 也能登:

ssh -i $HOME\.ssh\timetrace_deploy tt-rb4g whoami     # 应返回 Yuki,不提示密码

如果 ~/.ssh/configtt-rb4g 锁定了 IdentityFile 到你的开发 key,-i 会被忽略。改成直连测:

ssh -i $HOME\.ssh\timetrace_deploy -p <FRP端口> Yuki@121.x.x.x whoami

6. 生成 known_hosts(让 CI 不弹"信任此主机吗")

CI runner 每次跑都是干净环境,没有缓存的 host key。我们提前抓两个 host 的 SSH 指纹,作为 secret 喂进去。

在本机 PowerShell:

# 跳板的 host key(121.x.x.x 是真实 IP,22 是 SSH 端口)
ssh-keyscan -p 22 121.x.x.x > $HOME\timetrace_known_hosts.txt

# 通过跳板看小主机的 host key(注意 127.0.0.1 + FRP 端口)
# 这条会在跳板上执行 ssh-keyscan
ssh tt-rb4g "ssh-keyscan -p <FRP端口> 127.0.0.1" >> $HOME\timetrace_known_hosts.txt

cat $HOME\timetrace_known_hosts.txt

期望看到 2-6 行(每台主机可能有 rsa/ecdsa/ed25519 多种算法的 key):

121.x.x.x ssh-ed25519 AAAAC3...
121.x.x.x ssh-rsa AAAAB3...
[127.0.0.1]:22001 ssh-ed25519 AAAAC3...

保留这个文件,第 7 步会用。


7. 在 GitHub 仓库配 6 个 Secrets

打开浏览器:

https://github.com/Vanilla-Yukirin/TimeTrace/settings/secrets/actions

或者用 gh CLI 一条条加(推荐,下面给精确命令)。

7.1 DEPLOY_SSH_KEY — CI 私钥全文

gh secret set DEPLOY_SSH_KEY --body (Get-Content $HOME\.ssh\timetrace_deploy -Raw)

Web 方式:复制 Get-Content $HOME\.ssh\timetrace_deploy -Raw 输出,包括头尾的 -----BEGIN OPENSSH PRIVATE KEY----- / -----END OPENSSH PRIVATE KEY-----,粘进去。

7.2 DEPLOY_KNOWN_HOSTS — 第 6 步生成的文件全文

gh secret set DEPLOY_KNOWN_HOSTS --body (Get-Content $HOME\timetrace_known_hosts.txt -Raw)

7.3 DEPLOY_JUMP_USER_HOSTuser@跳板IP

gh secret set DEPLOY_JUMP_USER_HOST --body "Yuki@121.x.x.x"

Yuki 换成你的云服务器实际用户名(可能是 root / ubuntu / yuki,登一次 ssh tt-rb4g whoami 确认); 121.x.x.x 换成你云服务器真实 IP。

7.4 DEPLOY_JUMP_PORT — 跳板 SSH 端口

gh secret set DEPLOY_JUMP_PORT --body "22"

通常 22。如果你云服务器把 sshd 改到别的端口(比如 22022),填那个。

7.5 DEPLOY_TARGET_USER_HOST — 从跳板视角看小主机

gh secret set DEPLOY_TARGET_USER_HOST --body "Yuki@127.0.0.1"

这里 Yuki小主机用户名,不是云服务器用户名。 127.0.0.1 是因为 FRP 在跳板的 loopback 监听,从跳板视角看就是本机。

7.6 DEPLOY_TARGET_PORT — 跳板上 FRP 监听端口

gh secret set DEPLOY_TARGET_PORT --body "22001"

替换成你 frpc.tomlremote_port 配的值。

7.7 验证

gh secret list

应该看到这 6 个,每个右侧显示更新时间。Secret 值不可读回(GH 安全设计),上面命令只能"存"和"列"。


8. 小主机一次性配 systemd lingering

让 server 在你登出后继续跑(默认 user systemd 在用户登出时停服务):

ssh tt-rb4g "loginctl enable-linger Yuki"

Yuki 换成小主机的真实用户名。

验证:

ssh tt-rb4g "loginctl show-user Yuki | grep Linger"
# 期望: Linger=yes

9. 首次部署 — 手动触发 + 紧盯日志

9.1 触发 workflow

gh workflow run deploy.yml --ref main

第一次部署前确认你想部署的代码已经在 main 分支。当前在 feature/refactor-split 分支,需要先 merge / --ref feature/refactor-split 测试:

gh workflow run deploy.yml --ref feature/refactor-split

9.2 看跑得怎么样

gh run list --workflow=deploy.yml --limit 3
gh run watch        # 实时跟最近一个 run

期望流程:

  1. Checkout — 1s
  2. Set up SSH key — 1s
  3. Trust jumphost + target host keys — <1s
  4. Deploy via SSH (jump → FRP → target) — 这是关键步,~20s ~ 数分钟(首次要 git clone + uv sync 全量下载)
  5. Summary — <1s

9.3 失败诊断速查表

报错关键字 原因 处理
Permission denied (publickey) DEPLOY_SSH_KEY 没装上小主机的 authorized_keys 重做第 5 步
Host key verification failed DEPLOY_KNOWN_HOSTS 漏了某台主机或抓错端口 重做第 6 步,确保两台都抓到
Bad configuration option: proxyjumpport 你拉了 e82cba9 之前的旧 deploy.yml 拉最新代码
Could not resolve hostname DEPLOY_JUMP_USER_HOST 写错 IP 改 secret
Connection refused 在 127.0.0.1:22XXX FRP 在跳板没在跑 / 端口写错 登跳板 ss -tlnp 看 frps 监听的端口对不对
跑到 bash -s 后才挂 SSH 通了,是 deploy.sh 内部错。看 Action 日志最下面 deploy.sh 自己 echo 的"FATAL" 行

9.4 小主机这边看部署效果

ssh tt-rb4g "systemctl --user status timetrace-server"
# 期望 active (running)

ssh tt-rb4g "curl -s http://127.0.0.1:8765/healthz"
# 期望 {"status":"ok"} 或类似

ssh tt-rb4g "journalctl --user -u timetrace-server -n 20 --no-pager"
# 看启动日志,里面有 "auth.token_generated" 一行带 tt_live_... 这就是首启 token

记下首启 token:复制 tt_live_... 这一串 —— 这是默认设备的 bearer token,下一步配 client.toml 用。


10. 配置一台 client 接进来(本机 Windows)

# 仍在本机:
uv run timetrace-client init

按提示:

  • Server URL:写你能从本机访问到 server 的 URL。两种典型:
  • 本机和小主机不在一个网段:起 ssh -L 8765:127.0.0.1:8765 tt-rb4g(保持终端开着)→ Server URL 填 http://127.0.0.1:8765
  • 直接公网暴露(不推荐):需要先在小主机前面挂 Caddy + TLS
  • Auth token:粘贴第 9.4 拿到的 tt_live_...
  • 其余按需

然后:

uv run timetrace-client print-config    # 确认值都对
uv run timetrace-client                 # 正式起采集

托盘出现 TimeTrace 图标 → 切几个窗口 → 看 server 端日志:

ssh tt-rb4g "journalctl --user -u timetrace-server -f"
# 切换窗口时这边应该有 ingest 相关日志滚动

11. 善后

跑通之后:

# 清理 known_hosts 临时文件
Remove-Item $HOME\timetrace_known_hosts.txt

# 删除这份 checklist(不需要保留)
Remove-Item DEPLOY-CHECKLIST.md

保留这些不要删: - $HOME\.ssh\timetrace_deploy + .pub(CI key,下次轮换 deploy 时还要用) - $HOME\.ssh\id_ed25519 + .pub(你日常 SSH 用)


12. 常用后续命令速记

# 部署最新 main
gh workflow run deploy.yml

# 部署某个分支
gh workflow run deploy.yml --ref feature/some-branch

# 跟踪最近一次部署
gh run watch

# 看历史
gh run list --workflow=deploy.yml --limit 10

# 部署失败要看完整日志
gh run view --log

# 在小主机加一个新设备的 token
ssh tt-rb4g "cd ~/TimeTrace && uv run timetrace-server tokens add Mac-Laptop"

# 列所有 token
ssh tt-rb4g "cd ~/TimeTrace && uv run timetrace-server tokens list"

# 吊销
ssh tt-rb4g "cd ~/TimeTrace && uv run timetrace-server tokens revoke Mac-Laptop"
ssh tt-rb4g "systemctl --user restart timetrace-server"   # 改完 token 后重启

# 看前端 / OpenAPI(按需 SSH 隧道,看完关掉)
ssh -L 5173:127.0.0.1:5173 tt-rb4g     # 前端
ssh -L 8765:127.0.0.1:8765 tt-rb4g     # API

13. 出了大问题怎么回滚

# 找上一个跑过的 commit
ssh tt-rb4g "cd ~/TimeTrace && git log --oneline -5"

# 临时回滚(不走 CI,直接 ssh 改)
ssh tt-rb4g "cd ~/TimeTrace && git reset --hard <好的-commit> && systemctl --user restart timetrace-server"

# 或者:通过 deploy.yml 指定 ref 回滚
gh workflow run deploy.yml --ref <好的-commit-或-tag>

完成所有步骤后即可删除本文件。任何 step 卡住截图给我看。