Compare commits

...

8 Commits

Author SHA1 Message Date
wangjia ba5f71b716 ci: runs-on nas → ubuntu-latest(匹配 nas runner 实际 label)
Deploy Site / deploy-site (push) Failing after 1m36s
nas 上的 forgejo-act-runner 注册 label 是 ubuntu-latest(非 nas),导致所有
runs-on: nas 的工作流无匹配 runner。改为 ubuntu-latest 对齐 runner 现状。
(备选:给 runner 加 nas 标签保约定,未采用。)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:33:27 +08:00
wangjia 7c26050cdb docs(spec): 官网部署改 Cloudflare Pages(架构变更说明)
Deploy Site / deploy-site (push) Has been cancelled
节点 :443 被 VPN 占用 + CF 免费套餐改回源端口需 Enterprise → 官网改 CF Pages 托管
(纯静态/全程 HTTPS/CSP 生效/无 :443 冲突),已上线 pangolin.yanmeiai.com。
deploy-site.sh 用 wrangler,需 CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:08:20 +08:00
wangjia 673914783d refactor(ci): 官网部署改用 Cloudflare Pages(替代 rsync 到 pangolin1)
节点 :443 被 sing-box(VPN 数据面)占用,CF 免费套餐回源只能打 :80/:443、改端口需
Enterprise —— 故官网改由 CF Pages 托管(项目 pangolin-site,自定义域 pangolin.yanmeiai.com),
纯静态、全程 HTTPS、_headers/CSP 自动生效、不落 VPS、彻底无 :443 冲突。
deploy-site.sh 改为 wrangler pages deploy(需 CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID
Gitea secret);deploy-site.yml deploy 步骤在 node:20 容器内跑 wrangler。lib-ssh 仍供服务端部署用。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 09:59:58 +08:00
wangjia 443d4e3a11 feat(ci): test.sh(go test / flutter test)
Add scripts/ci/test.sh <server|client>: server runs `go test ./...`
inside golang:1.25 (mirrors ci.yml's go-server job docker invocation +
gomod/gobuild caches + GOPROXY from _env.sh); client runs `flutter test`
inside ghcr.io/cirruslabs/flutter:stable (mirrors ci.yml's flutter-client
job's pub-cache mount + test dirs). Wired into deploy-server.yml's Test
step already added in Task 4. Registers the script in ci.yml's shellcheck
file list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-06 00:37:48 +08:00
wangjia 0825170044 fix(ci): 服务端流水线注入加固(tag 校验/JSON 转义/workflow env)+ gitignore server/out
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-06 00:33:47 +08:00
wangjia 4fb3fe3fee feat(ci): 服务端 server-v* 编译+release+部署(备份/迁移/回滚,复用 lib-ssh/lib-forgejo)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 00:24:18 +08:00
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
14 changed files with 420 additions and 22 deletions
+18 -11
View File
@@ -21,7 +21,7 @@ jobs:
# ── Job 1: Lint (shellcheck) ─────────────────────────────────────────────
lint:
name: Lint — shellcheck
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -47,12 +47,19 @@ 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 \
/mnt/scripts/ci/compile-backend.sh \
/mnt/scripts/ci/release-server.sh \
/mnt/scripts/ci/deploy-server.sh \
/mnt/scripts/ci/test.sh
# ── Job 2: OpenAPI Sync Check ────────────────────────────────────────────
openapi-check:
name: OpenAPI Sync Check
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -69,7 +76,7 @@ jobs:
# ── Job 3: Redline Word Scan (脱敏) ──────────────────────────────────────
redline-scan:
name: Redline Scan — 脱敏 (UI 文案)
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -80,7 +87,7 @@ jobs:
# ── Job 4: Flutter 客户端(分析 + 单测/组件测试)────────────────────────
flutter-client:
name: Flutter — analyze + test
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -103,7 +110,7 @@ jobs:
# 规则与豁免见 ci/scan-portable-sql.sh 头注 + docs/dev-conventions.html 支柱 3。
portable-sql-scan:
name: Portable SQL — 可移植性 (mysql/sqlite)
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -116,7 +123,7 @@ jobs:
# (改了 design/colors_and_type.css 没重生成,或手改了生成物)。
codegen-drift:
name: Codegen Drift — token 生成物未漂移
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -133,7 +140,7 @@ jobs:
# -tags integration(需 docker 起 mysql/redis),见 go-integration job。
go-server:
name: Go — build + test
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -159,7 +166,7 @@ jobs:
# 详见 scripts/e2e-smoke.sh + server/test/e2e/。
e2e-smoke:
name: E2E Smoke — L4 进程级端到端
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -183,7 +190,7 @@ jobs:
# · -p 1 串行:一次只起一个 mysql 容器,避免并发把 Docker Desktop 压垮/端口资源争用。
go-integration:
name: Go — integration (mysql/redis testcontainers)
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -198,7 +205,7 @@ jobs:
# tablet/desktop-stats golden 与 stats-overhaul 工作区耦合,待其合并后并入本 job。
golden:
name: Golden — 视觉回归 (components + auth)
runs-on: nas
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
+42
View File
@@ -0,0 +1,42 @@
name: Deploy Server
on:
push:
tags:
- 'server-v[0-9]*.[0-9]*.[0-9]*'
workflow_dispatch:
concurrency:
group: deploy-server
cancel-in-progress: false
jobs:
deploy-server:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Compile (Go 控制面, golang:1.25 容器内构建)
run: |
mkdir -p "$HOME/.cache/pangolin-ci/gomod" "$HOME/.cache/pangolin-ci/gobuild"
docker run --rm -v "$PWD:/w" -w /w \
-v "$HOME/.cache/pangolin-ci/gomod:/go/pkg/mod" \
-v "$HOME/.cache/pangolin-ci/gobuild:/root/.cache/go-build" \
golang:1.25 bash scripts/ci/compile-backend.sh
- name: Test (go test)
run: bash scripts/ci/test.sh server
- name: Release → Forgejo
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
TAG: ${{ gitea.ref_name }}
run: bash scripts/ci/release-server.sh "$TAG"
- name: Deploy → pangolin1
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
TAG: ${{ gitea.ref_name }}
run: bash scripts/ci/deploy-server.sh "$TAG"
+33
View File
@@ -0,0 +1,33 @@
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: ubuntu-latest
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 → Cloudflare Pages
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
docker run --rm -v "$PWD:/w" -w /w \
-e CLOUDFLARE_API_TOKEN -e CLOUDFLARE_ACCOUNT_ID \
node:20 bash scripts/ci/deploy-site.sh
+1
View File
@@ -48,6 +48,7 @@ app/kernel/.build/
# Go 编译产物(mock server 等)
server/mockserver
server/pangolin-server
server/out/
# Flutter / Dart 构建产物与本地配置
client/android/local.properties
+5 -3
View File
@@ -111,10 +111,12 @@ pangolin.yanmeiai.com)与多端产物。现状:仅 <code>ci.yml</code> 校
</ol>
</div>
<h2>4. 官网部署</h2>
<h2>4. 官网部署 —— Cloudflare Pages</h2>
<div class="card" style="border-left:3px solid var(--warn)">
<b>架构变更(2026-07-06 实施):</b>原计划 rsync 到 pangolin1 的 nginx。但节点 <code>:443</code> 被 sing-box(VPN 数据面)占用,而 CF 免费套餐 proxied 回源只能打 :80/:443、改回源端口需 Enterprise —— 同机同 IP 上官网 HTTPS 与 VPN 无法共存。<b>故官网改由 Cloudflare Pages 托管</b>:纯静态、全程 HTTPS、<code>_headers</code>/CSP 原生生效、不落 VPS,从根上无 :443 冲突,也不拖累 VPN 机器。<b>已上线</b> <code>https://pangolin.yanmeiai.com</code>
</div>
<p>Astro <code>npm ci && npm run build</code><code>SITE_URL=https://pangolin.yanmeiai.com</code>)→ <code>dist/</code>
rsync 到服务器 web 根 → nginx/Caddy 静态托管。<b>DNS</b>CF 加记录
<code>pangolin.yanmeiai.com</code> → 服务器(首次部署时做,记 baize)。</p>
<code>npx wrangler pages deploy</code> 发布到 Pages 项目 <b>pangolin-site</b>(自定义域 <code>pangolin.yanmeiai.com</code>,CNAME → <code>pangolin-site.pages.dev</code>,proxied)。需 secret <code>CLOUDFLARE_API_TOKEN</code>(带 Account&gt;Pages&gt;Edit+ <code>CLOUDFLARE_ACCOUNT_ID</code>deploy 步骤在 <code>node:20</code> 容器内跑 wrangler。灾备:产物仍纯静态,可另 rsync 到镜像。</p>
<h2>5. 下载链接闭环(30A</h2>
<p><code>web/website/src/config/site.ts</code><code>downloads:{ android, macos, windows }</code>,值为 Gitea release
@@ -43,7 +43,7 @@ pangolin 现有 CI 仅 `.gitea/workflows/ci.yml`(nas,只校验无部署)+ `web/w
- `test.sh <server|client>` —— `go test` / `flutter test`
- `notify.sh` —— 成功/失败 Telegram 通知(可选,复用节点监控 bot)
- `compile-site.sh` / `compile-backend.sh` / `compile-android.sh` / `compile-macos.sh` / `compile-windows.sh`
- `deploy-site.sh` / `deploy-server.sh`(ssh pangolin1)
- `deploy-site.sh`(wrangler → CF Pages)/ `deploy-server.sh`(ssh pangolin1,复用 lib-ssh)
- `release-<x>.sh` —— 建 release + 挂产物
> 每个 `compile-*` 内部封装该端已验证的构建命令(如 Android 走
@@ -74,11 +74,18 @@ pangolin 现有 CI 仅 `.gitea/workflows/ci.yml`(nas,只校验无部署)+ `web/w
5. `install` 新二进制到 `/usr/local/bin`(旧的备份为 `.bak-<tag>`)
6. `systemctl start pangolin-server` + `/healthz` 健康检查;agent 随连接自恢复
### 4.4 官网部署(deploy-site.sh)
### 4.4 官网部署(deploy-site.sh)—— Cloudflare Pages
Astro `npm ci && npm run build`(`SITE_URL=https://pangolin.yanmeiai.com`)→ `dist/` rsync 到
服务器 web 根 → 由 nginx/Caddy 静态托管 `pangolin.yanmeiai.com`。**DNS**:CF 加 A/CNAME
`pangolin.yanmeiai.com` → 服务器(首次部署时做,记 baize)。
> **架构变更(2026-07-06 实施):** 原计划 rsync 到 pangolin1 的 nginx。但节点 :443 被 sing-box
> (VPN 数据面)占用,而 CF 免费套餐 proxied 回源只能打 :80/:443、改回源端口需 Enterprise ——
> 无法在同机同 IP 上让官网 HTTPS 与 VPN 共存。**故官网改由 Cloudflare Pages 托管**:纯静态、
> 全程 HTTPS、`_headers`/CSP 原生生效、不落 VPS,从根上无 :443 冲突,也不拖累 VPN 机器。
Astro `npm ci && npm run build`(`SITE_URL=https://pangolin.yanmeiai.com`)→ `dist/`
`npx wrangler pages deploy` 发布到 CF Pages 项目 **`pangolin-site`**(自定义域
`pangolin.yanmeiai.com`,CNAME → `pangolin-site.pages.dev`,proxied)。
需 secret:`CLOUDFLARE_API_TOKEN`(带 Account>Pages>Edit)+ `CLOUDFLARE_ACCOUNT_ID`(账户级)。
deploy 步骤在 `node:20` 容器内跑 wrangler。**灾备**:构建产物仍是纯静态,可另 rsync 到任意镜像。
## 5. 下载链接闭环(30A)
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# compile-backend.sh — cross-compile the pangolin Go control-plane binaries
# (server / agent / migrate) for the pangolin1 deploy target. CGO disabled:
# modernc.org/sqlite is pure Go, no cgo toolchain needed on the runner.
# Output: server/out/{pangolin-server,pangolin-agent,pangolin-migrate}.
#
# Run inside a golang:1.25 container by .gitea/workflows/deploy-server.yml;
# this script itself just runs `go build` and assumes it is invoked from the
# repo root.
set -euo pipefail
# shellcheck source=scripts/ci/_env.sh
. scripts/ci/_env.sh
cd server
mkdir -p out
echo "==> compile-backend: building pangolin-server"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/pangolin-server ./cmd/server
echo "==> compile-backend: building pangolin-agent"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/pangolin-agent ./cmd/agent
echo "==> compile-backend: building pangolin-migrate"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/pangolin-migrate ./cmd/migrate
echo "==> compile-backend: done — out/ contents:"
ls -lh out/
+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/
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# deploy-server.sh <tag> — deploy the pangolin control-plane binaries
# (pangolin-server / pangolin-agent / pangolin-migrate) to pangolin1, in the
# exact manual sequence used for F3/F4: stop → wal checkpoint → backup db →
# migrate (rollback db + restart old binary on failure) → swap binaries →
# start → healthcheck. Assumes compile-backend.sh has already produced
# server/out/{pangolin-server,pangolin-agent,pangolin-migrate}.
#
# Usage: scripts/ci/deploy-server.sh <tag> (e.g. server-v1.2.3)
# Requires env: DEPLOY_SSH_KEY (see lib-ssh.sh).
set -euo pipefail
# shellcheck source=scripts/ci/lib-ssh.sh
. scripts/ci/lib-ssh.sh
DB=/var/lib/pangolin/pangolin.db
BIN=/usr/local/bin
TAG="${1:?usage: deploy-server.sh <tag>}"
# Refuse anything that isn't a strict server-vX.Y.Z[-suffix] tag before it can
# reach the remote heredoc / backup paths below (command-injection guard).
# An anchored regex is used instead of a `case` glob: a trailing `*` in a
# case pattern matches ANY trailing characters (including shell metachars
# like `; rm -rf /`), which would defeat the point of this check.
if ! [[ "$TAG" =~ ^server-v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
echo "deploy-server: refusing unexpected tag '$TAG'" >&2
exit 1
fi
# setup_ssh registers the EXIT cleanup trap itself (before writing the key),
# so a mid-setup failure still cleans up — see lib-ssh.sh. It exports
# SSH_KEY_FILE / DEPLOY_PORT / SSH_KNOWN_HOSTS_FILE / DEPLOY_HOST used below
# to build SCP (mirroring the SSH/RSYNC_SSH command-string convention).
setup_ssh
SCP="scp -i ${SSH_KEY_FILE} -P ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}"
echo "==> deploy-server: tag=${TAG} host=${DEPLOY_HOST}"
echo "==> deploy-server: uploading binaries to ${DEPLOY_HOST}:/tmp/"
$SCP server/out/pangolin-server server/out/pangolin-agent server/out/pangolin-migrate "root@${DEPLOY_HOST}:/tmp/"
$SSH "root@${DEPLOY_HOST}" "bash -s" <<REMOTE
set -euo pipefail
systemctl stop pangolin-server
runuser -u pangolin -- sqlite3 "$DB" 'PRAGMA wal_checkpoint(TRUNCATE);'
cp -p "$DB" "$DB.bak-pre-$TAG"
if ! runuser -u pangolin -- env DB_DRIVER=sqlite DB_DSN=$DB /tmp/pangolin-migrate up; then
echo "!! migrate 失败,回滚"; cp -p "$DB.bak-pre-$TAG" "$DB"; systemctl start pangolin-server; exit 1
fi
cp -p "$BIN/pangolin-server" "$BIN/pangolin-server.bak-$TAG" || true
install -m755 /tmp/pangolin-server "$BIN/pangolin-server"
install -m755 /tmp/pangolin-agent "$BIN/pangolin-agent"
install -m755 /tmp/pangolin-migrate "$BIN/pangolin-migrate"
systemctl start pangolin-server
systemctl is-active pangolin-server
REMOTE
curl -fsS -m 10 --retry 5 --retry-connrefused "http://${DEPLOY_HOST}:8080/healthz" >/dev/null && echo "healthz OK"
echo "==> deploy-server: done"
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# deploy-site.sh — 部署构建好的 Astro 官网 (web/website/dist/) 到 Cloudflare Pages。
#
# 官网托管在 CF Pages(项目 pangolin-site,自定义域 pangolin.yanmeiai.com),纯静态、
# 全程 HTTPS、CSP(_headers)自动生效,不落在 VPS 上 —— 故与节点 :443(sing-box)无冲突。
#
# 需环境变量:
# CLOUDFLARE_API_TOKEN 带 Account > Cloudflare Pages > Edit 权限的 CF token
# CLOUDFLARE_ACCOUNT_ID CF 账户 ID
# 由 compile-site.sh 先产出 web/website/dist/;从 repo 根调用。
set -euo pipefail
if [ ! -d web/website/dist ]; then
echo "==> deploy-site: web/website/dist/ 不存在 — 拒绝部署" >&2
exit 1
fi
if ! find web/website/dist -mindepth 1 -print -quit | grep -q .; then
echo "==> deploy-site: web/website/dist/ 为空 — 拒绝部署" >&2
exit 1
fi
: "${CLOUDFLARE_API_TOKEN:?deploy-site: CLOUDFLARE_API_TOKEN 未设(需带 Pages:Edit)}"
: "${CLOUDFLARE_ACCOUNT_ID:?deploy-site: CLOUDFLARE_ACCOUNT_ID 未设}"
echo "==> deploy-site: wrangler pages deploy → project pangolin-site (branch main)"
npx --yes wrangler@4 pages deploy web/website/dist \
--project-name=pangolin-site --branch=main --commit-dirty=true
echo "==> deploy-site: done"
+21 -3
View File
@@ -59,18 +59,36 @@ forgejo_release_ensure() {
rm -f "$get_body_file"
echo "==> forgejo: creating release ${tag}"
local create_code_file create_body_file create_code
local create_code_file create_body_file create_code create_req_file
create_code_file="/tmp/forgejo_create_code.$$"
create_body_file="/tmp/forgejo_create_body.$$.json"
create_req_file="/tmp/forgejo_create_req.$$.json"
# Build the JSON request body via python3's json.dumps rather than raw
# string interpolation, so a tag/title containing `"` / `\` / control
# characters can't break out of the JSON structure (json-injection guard).
# Values are piped in NUL-separated on stdin — never interpolated into the
# python source — and no $() command substitution is used.
printf '%s\0%s\0' "$tag" "$title" | python3 -c '
import json
import sys
raw = sys.stdin.buffer.read()
tag, title = (part.decode() for part in raw.split(b"\0")[:2])
json.dump(
{"tag_name": tag, "name": title, "draft": False, "prerelease": False},
sys.stdout,
)
' > "$create_req_file"
curl "${FORGEJO_CURL_TLS[@]}" -s -o "$create_body_file" -w '%{http_code}' \
-X POST "${FORGEJO_URL}/api/v1/repos/${FORGEJO_REPO}/releases" \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${tag}\",\"name\":\"${title}\",\"draft\":false,\"prerelease\":false}" \
--data @"$create_req_file" \
> "$create_code_file"
read -r create_code < "$create_code_file"
rm -f "$create_code_file"
rm -f "$create_code_file" "$create_req_file"
if [ "$create_code" -lt 200 ] || [ "$create_code" -ge 300 ]; then
echo "==> forgejo: release create FAILED (HTTP ${create_code})" >&2
+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}"
}
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# release-server.sh — ensure the Forgejo release for a server-v* tag exists
# and upload the three compiled binaries as release assets. Assumes
# compile-backend.sh has already produced
# server/out/{pangolin-server,pangolin-agent,pangolin-migrate}.
#
# Usage: scripts/ci/release-server.sh <tag> (e.g. server-v1.2.3)
# Requires env: FORGEJO_URL, FORGEJO_TOKEN (see lib-forgejo.sh).
set -euo pipefail
TAG="${1:?usage: release-server.sh <tag>}"
# shellcheck source=scripts/ci/_env.sh
. scripts/ci/_env.sh
# shellcheck source=scripts/ci/lib-forgejo.sh
. scripts/ci/lib-forgejo.sh
# No command substitution ($()): ver_from_tag prints to stdout, captured via
# a temp file + `read`, same no-substitution pattern as lib-forgejo.sh.
ver_file="/tmp/release_server_ver.$$"
ver_from_tag server "$TAG" > "$ver_file"
VER=""
read -r VER < "$ver_file"
rm -f "$ver_file"
echo "==> release-server: tag=${TAG} ver=${VER}"
forgejo_release_ensure "$TAG" "server ${VER}"
forgejo_upload_asset "$TAG" server/out/pangolin-server
forgejo_upload_asset "$TAG" server/out/pangolin-agent
forgejo_upload_asset "$TAG" server/out/pangolin-migrate
echo "==> release-server: done"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# test.sh <server|client> — run the test suite for one side of the repo.
# Invoked directly on the nas runner (see deploy-server.yml's "Test" step),
# so this script itself owns the docker invocation — Go/Flutter are not on
# the host.
#
# server: `go test ./...` inside golang:1.25, mirroring the go-server job's
# docker invocation in .gitea/workflows/ci.yml (mounts + gomod/gobuild
# caches), plus GOPROXY from _env.sh so it doesn't hit proxy.golang.org.
# client: `flutter test` inside ghcr.io/cirruslabs/flutter:stable, mirroring
# the flutter-client job's docker invocation in .gitea/workflows/ci.yml
# (pub-cache volume, same test dirs); analyze/coverage stay in that job.
#
# Run from the repo root (relative paths below assume this).
set -euo pipefail
# shellcheck source=scripts/ci/_env.sh
. scripts/ci/_env.sh
TARGET="${1:-}"
case "$TARGET" in
server)
mkdir -p "$HOME/.cache/pangolin-ci/gomod" "$HOME/.cache/pangolin-ci/gobuild"
echo "==> test: go test ./... (golang:1.25 容器)"
docker run --rm \
-v "$PWD/server:/app" -w /app \
-v "$HOME/.cache/pangolin-ci/gomod:/go/pkg/mod" \
-v "$HOME/.cache/pangolin-ci/gobuild:/root/.cache/go-build" \
-e GOPROXY="$GOPROXY" \
golang:1.25 \
go test ./...
;;
client)
mkdir -p "$HOME/.cache/pangolin-ci/pubcache"
echo "==> test: flutter test (ghcr.io/cirruslabs/flutter:stable 容器)"
docker run --rm \
-v "$PWD/client:/app" -w /app \
-v "$HOME/.cache/pangolin-ci/pubcache:/root/.pub-cache" \
ghcr.io/cirruslabs/flutter:stable \
bash -c "flutter pub get && flutter test test/unit test/widget test/contract"
;;
*)
echo "usage: scripts/ci/test.sh <server|client>" >&2
exit 1
;;
esac
echo "==> test: ${TARGET} 通过"