Compare commits

...

2 Commits

Author SHA1 Message Date
wangjia 91d1355721 fix(ci): 部署脚本 —— 私钥清理 trap 前移 + rsync 空目录守卫 + 主机密钥 accept-new(评审 Critical/Important)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 00:17:37 +08:00
wangjia 9bac5c8dcb feat(ci): 官网 site-v* 构建+部署脚本(compile-site/deploy-site/lib-ssh/workflow)
- scripts/ci/compile-site.sh: node:20 容器内构建 web/website(Astro),
  SITE_URL 注入 canonical 域名 https://pangolin.yanmeiai.com。
- scripts/ci/lib-ssh.sh: 新增共享 setup_ssh/teardown_ssh(写临时私钥+
  known_hosts),供 site 与后续 server 部署复用;目标写死 IP 103.119.13.48
  (runner 无法解析用户本机 ~/.ssh/config 的 pangolin1 别名)。
- scripts/ci/deploy-site.sh: rsync dist/ 到 pangolin1
  /var/www/pangolin-site/,root 部署。
- .gitea/workflows/deploy-site.yml: site-v* tag + workflow_dispatch 触发,
  concurrency 组 deploy-site 防并发。
- .gitea/workflows/ci.yml: shellcheck 列表纳入三个新脚本。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-06 00:10:53 +08:00
5 changed files with 154 additions and 1 deletions
+4 -1
View File
@@ -47,7 +47,10 @@ jobs:
-S warning \
/mnt/scripts/ci/_env.sh \
/mnt/scripts/ci/lib-forgejo.sh \
/mnt/scripts/ci/notify.sh
/mnt/scripts/ci/notify.sh \
/mnt/scripts/ci/lib-ssh.sh \
/mnt/scripts/ci/compile-site.sh \
/mnt/scripts/ci/deploy-site.sh
# ── Job 2: OpenAPI Sync Check ────────────────────────────────────────────
openapi-check:
+29
View File
@@ -0,0 +1,29 @@
name: Deploy Site
on:
push:
tags:
- 'site-v[0-9]*.[0-9]*.[0-9]*'
workflow_dispatch:
concurrency:
group: deploy-site
cancel-in-progress: false
jobs:
deploy-site:
runs-on: nas
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Compile (Astro 官网, node:20 容器内构建)
run: |
docker run --rm -v "$PWD:/w" -w /w \
-e SITE_URL=https://pangolin.yanmeiai.com \
node:20 bash scripts/ci/compile-site.sh
- name: Deploy → pangolin1
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: bash scripts/ci/deploy-site.sh
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# compile-site.sh — build the Astro 官网 (web/website) as a static site.
# Output: web/website/dist/. Canonical domain is injected via SITE_URL (see
# web/website/astro.config.mjs) — must match the deploy target host name so
# canonical URLs / sitemap resolve correctly.
#
# Run inside a node:20 container by .gitea/workflows/deploy-site.yml; this
# script itself just runs npm and assumes it is invoked from the repo root.
set -euo pipefail
SITE_URL="${SITE_URL:-https://pangolin.yanmeiai.com}"
export SITE_URL
echo "==> compile-site: SITE_URL=${SITE_URL}"
cd web/website
npm ci
npm run build
echo "==> compile-site: done — dist/ contents:"
ls -lh dist/
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# deploy-site.sh — rsync the built Astro 官网 (web/website/dist/) to the
# pangolin1 VPS web root (/var/www/pangolin-site). Deploy user is root — the
# CI deploy key (secrets.DEPLOY_SSH_KEY) is authorized for root on the host.
# Static files only; nginx/caddy serves them directly, no service restart
# needed. Assumes it is invoked from the repo root, after compile-site.sh has
# produced web/website/dist/.
set -euo pipefail
# shellcheck source=scripts/ci/lib-ssh.sh
. scripts/ci/lib-ssh.sh
# setup_ssh registers the EXIT cleanup trap itself (before writing the key),
# so a mid-setup failure still cleans up — see lib-ssh.sh.
setup_ssh
# Guard against deploying a missing/empty build: `rsync --delete` against an
# empty source would wipe the live site.
if [ ! -d web/website/dist ]; then
echo "==> deploy-site: web/website/dist/ does not exist — refusing to deploy" >&2
exit 1
fi
if ! find web/website/dist -mindepth 1 -print -quit | grep -q .; then
echo "==> deploy-site: web/website/dist/ is empty — refusing to deploy" >&2
exit 1
fi
DEPLOY_TARGET="root@${DEPLOY_HOST}:/var/www/pangolin-site/"
echo "==> deploy-site: rsync web/website/dist/ -> ${DEPLOY_TARGET}"
rsync -az --delete -e "${RSYNC_SSH}" web/website/dist/ "${DEPLOY_TARGET}"
echo "==> deploy-site: done"
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# lib-ssh.sh — shared SSH deploy-key helpers for the pangolin deploy pipelines
# (site / server). `source` this from deploy-*.sh.
#
# Provides:
# setup_ssh -> registers the EXIT cleanup trap first, then writes
# $DEPLOY_SSH_KEY to a temp private key (mode 600),
# registers the deploy host in a dedicated known_hosts
# file, and exports SSH / RSYNC_SSH (ssh command strings)
# + SSH_KEY_FILE.
# teardown_ssh -> removes the temp private key + known_hosts file.
#
# Requires env: DEPLOY_SSH_KEY (PEM content of the deploy private key,
# authorized for root on the target host).
#
# Target host is hardcoded to the pangolin1 VPS IP (103.119.13.48): the CI
# runner has no access to the user's local ~/.ssh/config, so the `pangolin1`
# alias cannot be resolved there — the bare IP is used instead. Override with
# DEPLOY_HOST / DEPLOY_PORT if a caller needs to retarget.
#
# No command substitution ($()) is used anywhere, per repo bash conventions.
DEPLOY_HOST="${DEPLOY_HOST:-103.119.13.48}"
DEPLOY_PORT="${DEPLOY_PORT:-22}"
SSH_KEY_FILE="${SSH_KEY_FILE:-/tmp/pangolin_deploy_key.$$}"
SSH_KNOWN_HOSTS_FILE="${SSH_KNOWN_HOSTS_FILE:-/tmp/pangolin_deploy_known_hosts.$$}"
# setup_ssh — write the deploy key, register known_hosts, export SSH/RSYNC_SSH.
setup_ssh() {
# Register cleanup FIRST: if anything below fails mid-setup (e.g. a
# transient ssh-keyscan error under `set -e`), the private key file must
# still be removed on exit rather than leaking.
trap teardown_ssh EXIT
if [ -z "${DEPLOY_SSH_KEY:-}" ]; then
echo "==> setup_ssh: DEPLOY_SSH_KEY is empty" >&2
return 1
fi
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Pre-create the key file with restrictive perms *before* writing any key
# material into it, so there is no window at the default umask between
# file creation and chmod.
install -m 600 /dev/null "${SSH_KEY_FILE}"
# `printf '%s\n'` 末尾补一个换行:Forgejo/Gitea 存 secret 会去掉结尾换行,
# 而缺结尾换行的 OpenSSH 格式私钥会被判为 "invalid format" 拒绝加载,
# 退化成无密钥 → Permission denied。多补的换行对已含结尾换行的 PEM 无害。
printf '%s\n' "${DEPLOY_SSH_KEY}" > "${SSH_KEY_FILE}"
# Populate a dedicated known_hosts file via TOFU keyscan. This is
# belt-and-suspenders: `accept-new` below will pin the host key on first
# real connection regardless, so a transient keyscan failure must not
# abort the deploy.
ssh-keyscan -p "${DEPLOY_PORT}" -H "${DEPLOY_HOST}" >> "${SSH_KNOWN_HOSTS_FILE}" 2>/dev/null || true
SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}"
RSYNC_SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}"
export SSH RSYNC_SSH SSH_KEY_FILE SSH_KNOWN_HOSTS_FILE DEPLOY_HOST DEPLOY_PORT
echo "==> setup_ssh: key written to ${SSH_KEY_FILE}, known_hosts pinned (accept-new) for ${DEPLOY_HOST}:${DEPLOY_PORT}"
}
# teardown_ssh — remove the temp private key + known_hosts file.
teardown_ssh() {
rm -f "${SSH_KEY_FILE}" "${SSH_KNOWN_HOSTS_FILE}"
echo "==> teardown_ssh: removed ${SSH_KEY_FILE}"
}