Skip to content

# erlang_pay:纯 Erlang 第三方支付库 #12

# erlang_pay:纯 Erlang 第三方支付库

# erlang_pay:纯 Erlang 第三方支付库 #12

Workflow file for this run

# imboy 质量门 — Erlang lint + Contract + Secrets
#
# 关联:.claude/plans/quality-loop.md v1.3 T4.1(v1.1 §301: 三仓各一份;v1.3 §490: erlang 用 make 链)
# 与 backend-ci.yml 互补:本 workflow 仅跑质量门(lint/secrets/contract),不跑 build/eunit/dialyze
# Ratchet 上限(debt-baseline-20260509.md):elvis 8824 / gitleaks 0(2026-06-10 收紧,历史 9 条经 .gitleaksignore 豁免)/ redocly 4 errors+9 warnings
# 创建:2026-05-09 / iteration 74 / T4.1
name: imboy Quality Gate
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
workflow_dispatch:
permissions:
contents: read
jobs:
# ----------------------------------------------------------------
# 1) Erlang lint — elvis 5.0.3 风格检查(不跑 dialyze/xref,那是业务 CI)
# ----------------------------------------------------------------
erlang-lint:
name: Erlang lint (elvis)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Erlang/OTP
uses: erlef/setup-beam@v1
with:
otp-version: '28'
- name: Install elvis
run: |
# elvis 5.0.3 from brew formula source (escript build via rebar3)
set -euo pipefail
mkdir -p ~/bin
curl -fsSL https://github.com/inaka/elvis/releases/download/5.0.3/elvis -o ~/bin/elvis || \
(echo "fallback: build from source"; \
git clone --depth 1 -b 5.0.3 https://github.com/inaka/elvis /tmp/elvis && \
cd /tmp/elvis && rebar3 escriptize && cp _build/default/bin/elvis ~/bin/elvis)
chmod +x ~/bin/elvis
echo "$HOME/bin" >> "$GITHUB_PATH"
~/bin/elvis --version || true
- name: Run elvis (ratchet — 当前 baseline 8824 violations / 239 FAIL files)
run: |
set -euo pipefail
make lint-erlang 2>&1 | tee elvis-output.txt
# 去 ANSI 色码再统计(elvis 默认 colored output)
sed -E $'s/\x1b\\[[0-9;]*m//g' elvis-output.txt > elvis-clean.txt
# 用 awk 匹配(不依赖 BSD/GNU grep 的 [[:space:]] 行为差异)
# violations: " - At line ..." 起头(4+ 空格 + dash + space + "At" + " line")
violations=$(awk '/^[[:space:]]+- At line/{c++} END{print c+0}' elvis-clean.txt)
# FAIL 文件: "# path [FAIL]" 起头
fail_files=$(awk '/^# .* \[FAIL\]/{c++} END{print c+0}' elvis-clean.txt)
echo "elvis_violations=$violations / fail_files=$fail_files"
if [ "$violations" -gt 8824 ]; then
echo "::error::Elvis violations $violations exceed baseline 8824 (ratchet)"
exit 1
fi
if [ "$fail_files" -gt 239 ]; then
echo "::error::Elvis FAIL files $fail_files exceed baseline 239 (ratchet)"
exit 1
fi
# ----------------------------------------------------------------
# 2) Contract — OpenAPI lint(imboy 是契约真源)
# ----------------------------------------------------------------
contract-lint:
name: Contract lint (redocly)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install Redocly CLI
run: npm i -g @redocly/cli@latest
- name: Lint OpenAPI (ratchet — 4 errors / 9 warnings baseline)
run: |
set -euo pipefail
if ! redocly lint api/openapi.yaml; then
echo "::warning::Redocly lint failed — see baseline ratchet (4 errors / 9 warnings)"
errors=$(redocly lint api/openapi.yaml 2>&1 | grep -oE "[0-9]+ errors" | head -1 | awk '{print $1}')
errors=${errors:-0}
if [ "$errors" -gt 4 ]; then
echo "::error::OpenAPI errors $errors exceed baseline 4 (ratchet)"
exit 1
fi
fi
# T3.2 split (2026-05-16):multi-file $ref bundle 验证
# 失败信号:跨文件 $ref 解析错误 / 文件缺失 / YAML 锚点污染(codegen 杀手)
- name: Bundle OpenAPI multi-file → single file
run: |
set -euo pipefail
redocly bundle api/openapi.yaml -o api/openapi.bundled.yaml
# 防御:bundle 后不应出现 YAML alias 锚点 (*ref_N / &ref_N),
# 否则下游 openapi-generator (dart/ts) 会生成异名类型。
if grep -nE "(^|[[:space:]])\*ref_|&ref_" api/openapi.bundled.yaml; then
echo "::error::Bundle contains YAML aliases — component filename must match PascalCase key"
exit 1
fi
echo "::notice::Bundle clean — $(wc -l < api/openapi.bundled.yaml) lines"
- name: Upload bundled OpenAPI artifact
uses: actions/upload-artifact@v4
with:
name: openapi-bundled
path: api/openapi.bundled.yaml
retention-days: 14
# ----------------------------------------------------------------
# 3) oasdiff — OpenAPI breaking change 检测(仅 PR)
# v1.1 §331: oasdiff job 仅放在 imboy(契约真源仓)
# v1.3 T4.2: PR 提交时对比 base vs head 的 openapi.yaml,breaking 即 fail
# ----------------------------------------------------------------
oasdiff-breaking:
name: OpenAPI breaking change check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout PR head
uses: actions/checkout@v4
with:
path: head
- name: Checkout PR base
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
- name: Install oasdiff
run: |
set -euo pipefail
curl -fsSL https://github.com/oasdiff/oasdiff/releases/download/v1.15.2/oasdiff_1.15.2_linux_amd64.tar.gz \
| tar -xz -C /tmp oasdiff
/tmp/oasdiff --version
# T3.2 split (2026-05-16):multi-file 适配 — oasdiff 不会自动解析跨文件 $ref,
# 必须先用 redocly bundle 把 base/head 各自合并为单文件再做 diff,否则
# 跨文件 $ref 会被当作字符串比较,导致漏报或误报。
- name: Setup Node + Redocly for bundle
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install Redocly CLI
run: npm i -g @redocly/cli@latest
- name: Bundle base & head OpenAPI
run: |
set -euo pipefail
# base 仓在 phase 1 之前可能没有 api/components/,bundle 失败时降级
# 为直接使用 base/api/openapi.yaml;head 总是新结构,必须 bundle 成功。
if [ -d base/api/components ]; then
redocly bundle base/api/openapi.yaml -o /tmp/oas.base.yaml
else
echo "::notice::Base lacks api/components/ — falling back to raw openapi.yaml"
cp base/api/openapi.yaml /tmp/oas.base.yaml
fi
redocly bundle head/api/openapi.yaml -o /tmp/oas.head.yaml
- name: Diff base vs head openapi.yaml (changelog summary)
run: |
set -euo pipefail
/tmp/oasdiff diff /tmp/oas.base.yaml /tmp/oas.head.yaml --format markup || true
- name: Breaking changes (fail PR if any)
run: |
set -euo pipefail
if /tmp/oasdiff breaking /tmp/oas.base.yaml /tmp/oas.head.yaml --fail-on ERR; then
echo "::notice::No breaking changes detected"
else
echo "::error::Breaking change detected in api/openapi.yaml"
echo "::error::请在 PR 描述中说明影响范围 + 升 major version + 通知 imboyapp/admin 客户端"
exit 1
fi
# ----------------------------------------------------------------
# 4) Secrets — gitleaks
# ----------------------------------------------------------------
secrets-scan:
name: Secrets scan (gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # gitleaks 需要历史
# 历史 9 条泄露已逐条定性并经 .gitleaksignore 精确豁免(指纹级),
# 详见 docs/operations/security-leak-assessment-2026-06.md;新增泄露即红。
- name: Run gitleaks (ratchet — baseline 0)
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
config-path: .gitleaks.toml
- name: Verify no new leaks
run: |
set -euo pipefail
gitleaks detect --no-git --config .gitleaks.toml --redact --no-banner \
--report-format json --report-path /tmp/leaks.json || true
count=$(jq '. | length' /tmp/leaks.json 2>/dev/null || echo 0)
echo "leaks=$count"
if [ "$count" -gt 0 ]; then
echo "::error::Gitleaks found $count leaks, baseline is 0 (see docs/operations/security-leak-assessment-2026-06.md)"
exit 1
fi
# ----------------------------------------------------------------
# 5) Architecture gates — 层边界 + 文件大小 + 静默吞错检查
# 2026-06-03: 源自 verified-audit-final.md P1-5
# ----------------------------------------------------------------
architecture-gates:
name: Architecture gates (layer / size / silent-catch)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check layer boundaries
run: |
set -euo pipefail
echo "=== API → Repo (must be 0) ==="
api_repo=$(grep -rn "_repo:" src/api/ --include="*.erl" | grep -v "%%" | wc -l || echo 0)
echo "API→Repo violations: $api_repo"
if [ "$api_repo" -gt 0 ]; then
echo "::error::API layer must not call Repo directly ($api_repo violations)"
exit 1
fi
echo "=== Repo → Logic (must be 0) ==="
repo_logic=$(grep -rn "_logic:" src/repo/ --include="*.erl" | grep -v "%%" | wc -l || echo 0)
echo "Repo→Logic violations: $repo_logic"
if [ "$repo_logic" -gt 0 ]; then
echo "::error::Repo layer must not call Logic directly ($repo_logic violations)"
exit 1
fi
- name: Check file size (hand-written .erl must be ≤ 800 lines)
run: |
set -euo pipefail
# Baseline: current oversized files (2026-06-03)
# adm_group_handler 2218, imboy_policy 2185, channel_handler 999,
# imboy_router 908, websocket_handler 847, adm_channel_handler 817
oversized=$(find src -name "*.erl" ! -name "imboy_pb.erl" -exec wc -l {} + \
| awk '$1 > 800 && !/total/ {print}' || true)
if [ -n "$oversized" ]; then
count=$(echo "$oversized" | wc -l | tr -d ' ')
echo "::warning::$count files exceed 800 lines (baseline 6 files, ratchet)"
echo "$oversized"
# Fail only if NEW oversized files appear (baseline = 6)
if [ "$count" -gt 6 ]; then
echo "::error::New oversized files detected (baseline 6, found $count)"
exit 1
fi
fi
- name: Check silent error swallowing (catch _:_ must not appear)
run: |
set -euo pipefail
silent=$(grep -rn "catch _:_ ->" src/ --include="*.erl" | grep -v "%%" | wc -l || echo 0)
echo "Silent catch _:_ count: $silent"
if [ "$silent" -gt 0 ]; then
echo "::error::Found $silent silent catch _:_ — use catch _:Err + logger:warning instead"
grep -rn "catch _:_ ->" src/ --include="*.erl" | grep -v "%%"
exit 1
fi
api-ds-ratchet:
name: API→DS 层违规不得增长
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 检查 API→DS 违规数(上限 131)
run: |
COUNT=$(grep -rn "_ds:" src/api/ --include="*.erl" \
| grep -v "%%\|auth_ds:\|config_ds:" | wc -l | tr -d ' ')
echo "当前 API→DS 违规: $COUNT (上限 131)"
if [ "$COUNT" -gt "131" ]; then
echo "FAIL: 违规增加 $COUNT > 131"
exit 1
fi