name: ci-pangolin # 触发条件:deploy/ 或 design/ 或 client/ 或 server/ 或 ci/ 变更时,以及所有向 main 的 PR on: push: branches: [main] paths: - 'deploy/**' - 'design/**' - 'client/**' - 'server/**' - 'ci/**' - '.gitea/workflows/ci.yml' pull_request: branches: [main] workflow_dispatch: jobs: # ── Job 1: Lint (shellcheck) ───────────────────────────────────────────── lint: name: Lint — shellcheck runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: shellcheck 节点脚本(bootstrap + single-node) # 注:koalaman/shellcheck 镜像 ENTRYPOINT 已是 shellcheck,命令里别再写一遍 # (否则变 `shellcheck shellcheck …`,把 "shellcheck" 当文件→openBinaryFile 报错)。 run: | docker run --rm \ -v "$PWD/deploy:/mnt/deploy:ro" \ koalaman/shellcheck:stable \ -S warning \ /mnt/deploy/bootstrap/init.sh \ /mnt/deploy/bootstrap/monitor/pangolin-monitor.sh \ /mnt/deploy/bootstrap/monitor/deadman-watch.sh \ /mnt/deploy/single-node/deploy.sh # ── Job 2: OpenAPI Sync Check ──────────────────────────────────────────── openapi-check: name: OpenAPI Sync Check runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 # openapi-spec-validator 是纯 Python、无外部依赖,校验 OAS 3.0 结构合法性。 - name: lint design/server/openapi.yaml run: | docker run --rm \ -v "$PWD/design/server:/spec:ro" \ python:3.12-alpine \ sh -c "pip install openapi-spec-validator --quiet && \ python -m openapi_spec_validator /spec/openapi.yaml" # ── Job 3: Redline Word Scan (脱敏) ────────────────────────────────────── redline-scan: name: Redline Scan — 脱敏 (UI 文案) runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: scan UI text resources for prohibited words run: bash ci/scan-redline.sh # ── Job 4: Flutter 客户端(分析 + 单测/组件测试)──────────────────────── flutter-client: name: Flutter — analyze + test runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: flutter analyze + test + coverage run: | mkdir -p "$HOME/.cache/pangolin-ci/pubcache" 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 analyze --no-fatal-infos && flutter test --coverage test/unit test/widget test/contract" # 覆盖率闸(host 侧解析 lcov):防断崖,低于阈值即失败。 COV=$(awk -F: '/^LF:/{f+=$2} /^LH:/{h+=$2} END{if(f>0)printf "%.1f",h/f*100}' client/coverage/lcov.info) echo "flutter 行覆盖 ${COV}%" awk -v c="$COV" 'BEGIN{ if(c+0 < 28){ print "❌ flutter 覆盖率 "c"% < 阈值 28%"; exit 1 } print "✅ flutter 覆盖率 "c"% ≥ 28%" }' # ── Job 5: Portable SQL Scan (支柱 3:双库可移植)───────────────────────── # server Go 运行时查询不得含 MySQL 专属构造(同一份 SQL 要跑 mysql 与 sqlite)。 # 规则与豁免见 ci/scan-portable-sql.sh 头注 + docs/dev-conventions.html 支柱 3。 portable-sql-scan: name: Portable SQL — 可移植性 (mysql/sqlite) runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: scan server Go runtime SQL for MySQL-specific constructs run: bash ci/scan-portable-sql.sh # ── Job 6: Codegen Drift (支柱 2:token 单源不漂移)────────────────────── # 重新生成 client/lib/pangolin_tokens.gen.dart,与已提交版本不一致即失败 # (改了 design/colors_and_type.css 没重生成,或手改了生成物)。 codegen-drift: name: Codegen Drift — token 生成物未漂移 runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: regenerate flutter tokens & assert no drift run: | docker run --rm \ -v "$PWD:/repo" -w /repo \ node:20 \ bash ci/check-codegen-drift.sh # ── Job 7: Go 服务端(build + test:含契约快照 + sqlite 真库,跳过 integration)── # 此前 server 测试未进 CI;契约快照(支柱 2)等需在此守门。integration 测试走 # -tags integration(需 docker 起 mysql/redis),见 go-integration job。 go-server: name: Go — build + test runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: go build + vet + test + coverage run: | # 宿主持久缓存(host-mode runner):省掉每次 `go: downloading` 全量拉依赖。 mkdir -p "$HOME/.cache/pangolin-ci/gomod" "$HOME/.cache/pangolin-ci/gobuild" 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" \ golang:1.25 \ bash -c "go build ./... && go vet ./... && go test -coverprofile=cover.out ./... && go tool cover -func=cover.out > coverage.txt" tail -1 server/coverage.txt # 覆盖率闸(host 侧解析,避开容器引号转义):防断崖,低于阈值即失败。 COV=$(grep '^total:' server/coverage.txt | grep -oE '[0-9]+\.[0-9]+') awk -v c="$COV" 'BEGIN{ if(c+0 < 30){ print "❌ Go 覆盖率 "c"% < 阈值 30%"; exit 1 } print "✅ Go 覆盖率 "c"% ≥ 30%" }' # ── Job 8: E2E Smoke (L4 进程级端到端,含 gRPC 全链路)─────────────────── # 真起打包的 server 二进制(sqlite 临时库 + 内嵌 miniredis,无需 docker-in-docker) # → enroll(mTLS)→ ReportUsage 注入用量 → /v1/usage 断言统计真入库真读出。 # 详见 scripts/e2e-smoke.sh + server/test/e2e/。 e2e-smoke: name: E2E Smoke — L4 进程级端到端 runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: 进程级端到端冒烟 (server 二进制 + miniredis + gRPC 全链路) run: | mkdir -p "$HOME/.cache/pangolin-ci/gomod" "$HOME/.cache/pangolin-ci/gobuild" docker run --rm \ -v "$PWD:/repo" -w /repo \ -v "$HOME/.cache/pangolin-ci/gomod:/go/pkg/mod" \ -v "$HOME/.cache/pangolin-ci/gobuild:/root/.cache/go-build" \ golang:1.25 \ bash -c "apt-get update -qq && apt-get install -y -qq openssl curl python3 >/dev/null 2>&1 && bash scripts/e2e-smoke.sh" # ── Job 10: Go 集成测试 (L2:真 mysql8/redis 经 testcontainers)────────── # 跨库可移植(支柱 3)+ 按租户流量记账(usage)+ 配额(devices)+ 兑换(codes)+ # 节点生命周期(nodes)+ migrate/时区(store)的真库行为,sqlite 单测覆盖不到。 # 走 -tags integration,testcontainers 自起 mysql:8/redis 容器。 # · 在 host-mode runner 的宿主机直接跑 go(不套 golang 容器):testcontainers 要真 # docker,DooD 套在容器里在 Docker Desktop(mac)上网络不通。依赖宿主 go + docker。 # · -p 1 串行:一次只起一个 mysql 容器,避免并发把 Docker Desktop 压垮/端口资源争用。 go-integration: name: Go — integration (mysql/redis testcontainers) runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: go test -tags integration (宿主 docker + testcontainers) run: | cd server go test -tags integration -count=1 -p 1 ./... # ── Job 9: Golden 视觉回归 (L3 子集:components + auth)────────────────── # Linux 权威基线(scripts/update-goldens.sh 生成;mac 渲染不一致故钉死 Linux 容器)。 # tablet/desktop-stats golden 与 stats-overhaul 工作区耦合,待其合并后并入本 job。 golden: name: Golden — 视觉回归 (components + auth) runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: flutter test golden (Linux 权威基线) run: | mkdir -p "$HOME/.cache/pangolin-ci/pubcache" 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/golden/components_golden_test.dart test/golden/auth_redesign_golden_test.dart"