name: ci-pangolin # 触发条件:deploy/ 或 design/ 或 client/ 或 server/ 或 ci/ 变更时,以及所有向 main 的 PR on: push: branches: [main] paths: - 'deploy/**' - 'design/**' - 'client/**' - 'server/**' - 'ci/**' - 'scripts/ci/**' - '.gitea/workflows/ci.yml' pull_request: branches: [main] workflow_dispatch: # runner 分配: # · runs-on: ubuntu-latest —— 在 catthehacker 容器里跑,仅用于纯 bash 扫描 # (redline/cleartext/portable-sql);容器内**不能**嵌套 `docker run`(DinD 挂载 # 失败,$PWD 在宿主不存在),故套 docker 的 job 不能用它。 # · runs-on: nas —— host 模式(mac-pangolin-2 直接在宿主跑),`docker run` 是宿主 # 真 docker(非嵌套),可正常拉/跑 node/golang/flutter/python/shellcheck 镜像。 # golden 保留 ghcr.io/cirruslabs/flutter Linux 容器 → 与入库基线渲染一致。 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 - name: shellcheck CI 脚本(scripts/ci) run: | docker run --rm \ -v "$PWD/scripts/ci:/mnt/scripts/ci:ro" \ koalaman/shellcheck:stable \ -S warning \ /mnt/scripts/ci/_env.sh \ /mnt/scripts/ci/lib-forgejo.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 \ /mnt/scripts/ci/backup-db.sh \ /mnt/scripts/ci/compile-android.sh \ /mnt/scripts/ci/compile-windows.sh \ /mnt/scripts/ci/compile-macos.sh \ /mnt/scripts/ci/compile-ios.sh \ /mnt/scripts/ci/release-client.sh \ /mnt/scripts/ci/deploy-client.sh - name: shellcheck CI 脚本(ci/) run: | docker run --rm \ -v "$PWD/ci:/mnt/ci:ro" \ koalaman/shellcheck:stable \ -S warning \ /mnt/ci/scan-cleartext.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: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: scan UI text resources for prohibited words run: bash ci/scan-redline.sh # ── Job 3b: Cleartext Scan (Android 禁全局明文,#25 控制面已 https) ────── cleartext-scan: name: Cleartext Scan — Android 禁明文 runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: scan Android manifest for global cleartext run: bash ci/scan-cleartext.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: ubuntu-latest 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 ds-flow: name: DS-flow — 原型/跨端同源/代码色单源闸 runs-on: nas steps: - name: Checkout uses: actions/checkout@v4 - name: 原型校验(check-ds) run: docker run --rm -v "$PWD:/repo" -w /repo node:20 node design/prototype/tools/check-ds.mjs - name: 跨端同源(check-l1-sync) run: docker run --rm -v "$PWD:/repo" -w /repo node:20 node tools/check-l1-sync.mjs - name: Flutter 颜色单源(check_ds_code --strict) run: docker run --rm -v "$PWD/client:/app" -w /app node:20 node tool/check_ds_code.mjs --strict # ── 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 "bash scripts/e2e-smoke.sh" # 注:openssl/curl/python3 已在 golang:1.25 镜像内,无需 apt 安装 # (原 apt-get 会走 Docker Desktop 代理→本机 clash 死口,徒增网络脆性)。 # ── 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/desktop/tablet) 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"