Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: CI

# PR 门禁。只保两件事(详见 Makefile 的 ci-syntax):
# G1 生成的代码能通过类型检查
# G2 生成的代码没有重复声明
#
# 本仓**没有 lint job**,是有意为之,不是漏配:
# · `npm run tslint` 当前 4017 条报错(生成侧 3943 + 手写侧 74),全部是风格问题,
# 无一影响运行。决定性证据:同一批代码 `tsc --noEmit` exit 0、输出 0 行。
# · 加一个恒红的非阻断 job,只会在每个 PR 上留个红叉、制造「CI 坏了」的误解,
# 反而降低门禁的可信度。
# · 生成代码的正确性由 ci-syntax 的 `tsc --noEmit` 承担;风格问题不影响 SDK 能否使用。
# 要恢复 lint 门禁,前提是先把那 4017 条清零(改模板或改 tslint 配置),不是直接加 job。
#
# 分支保护只需把 `ci-gate` 配成 required check,其余 job 增删都不影响它。
#
# 原 workflow.yml 的 node 10/12/14 矩阵已去掉,不再在 PR 上跑:
# 那是「声明的下界还能不能装」的问题,与本次改动是否正确无关,
# 不该让每个 PR 都为它等三份构建。要验的时候手工跑一次即可。

on:
pull_request:
branches: [master]
push:
branches: [master]

permissions:
contents: read

jobs:
ci-syntax:
name: ci-syntax
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
# 用 npm install 而非 npm ci:仓里的 package-lock.json 是 lockfileVersion 1
# (npm 6 时代格式),且其 root version 停在 0.2.25、package.json 已是 0.2.26,
# 本就不同步。npm ci 要求 lock 与 package.json 严格一致,拿它当门禁第一步,
# 等于让一个与本次改动无关的陈旧文件决定 PR 红绿。
# npm install 是原 workflow.yml 一直在用、且已在 node 22/24 上实测通过的路径。
- run: npm install
- run: make ci-syntax

test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
- run: npm install
- run: make test-cov
# codecov 是可观测性,不是门禁:它挂了不该把 PR 拦死。
# 原 workflow.yml 是 fail_ci_if_error: true,等于把第三方服务的可用性
# 接进了合并条件,这里摘掉,并再加一层 continue-on-error。
- name: Upload coverage
continue-on-error: true
uses: codecov/codecov-action@v7
with:
fail_ci_if_error: false

ci-gate:
name: ci-gate
needs: [ci-syntax, test]
if: always()
runs-on: ubuntu-latest
steps:
- name: Aggregate
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" \
|| "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "ci-gate: 有 job 失败或被取消"
exit 1
fi
echo "ci-gate: 全部通过"
34 changes: 0 additions & 34 deletions .github/workflows/workflow.yml

This file was deleted.

12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ test:
test-cov:
npm run coverage

# CI 门禁:生成代码必须能通过类型检查、且无重复声明。
# G1 `tsc --noEmit` 覆盖 tsconfig include 的 src/**/*(含 services/ 下全部生成代码)。
# 这个仓此前完全没有类型检查入口——`npm run build` 会写 lib/,不适合放进门禁。
# G2 `tsc` 对同一文件内重复的 `export interface Foo` 走 declaration merging,
# 静默通过、不报错,所以 G1 抓不到重复声明,必须另加显式检测。
# (对照 go:重复声明是编译错误,G1 天然覆盖,故 go 侧无需此闸。)
# 代码风格(tslint / prettier)不进门禁,理由见 .github/workflows/ci.yml。
.PHONY: ci-syntax
ci-syntax:
npx tsc --noEmit
@bash $(CURDIR)/scripts/dup-check.sh ts src

lint:
npm run tslint
npm run prettier:check
Expand Down
61 changes: 61 additions & 0 deletions scripts/dup-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# G2 重复闸 —— php / js 生成代码重复顶层声明检测。
#
# 背景(实测,非推断):
# · php:`php -l` 能抓重复 `use`(编译期符号表冲突),但对同一文件内两个
# `class Foo` 一律 exit 0 —— 那是运行期错误,lint 阶段看不到。
# · js/ts:`tsc --noEmit` 对重复的 `export interface Foo` 走 declaration merging,
# 静默通过;只有同一 interface 内重复成员才报 TS2300。
# 故两者都需要显式的重复声明检测。
#
# 用法:dup-check.sh php <目录>
# dup-check.sh ts <目录>
# 退出码:0 = 无重复;1 = 发现重复(逐条打印 文件:声明名 出现次数)

set -uo pipefail

lang="${1:?用法: dup-check.sh <php|ts> <目录>}"
root="${2:?用法: dup-check.sh <php|ts> <目录>}"

case "$lang" in
php)
ext='*.php'
# 顶层 class / interface / trait 声明;兼容 final / abstract 前缀
pattern='^[[:space:]]*(final[[:space:]]+|abstract[[:space:]]+)?(class|interface|trait)[[:space:]]+[A-Za-z0-9_]+'
;;
ts)
ext='*.ts'
# 导出的顶层 interface / class / type / enum 声明
pattern='^export[[:space:]]+(interface|class|type|enum)[[:space:]]+[A-Za-z0-9_]+'
;;
*)
echo "不支持的语言: $lang(仅 php / ts)" >&2
exit 2
;;
esac

findings=0
scanned=0

while IFS= read -r f; do
scanned=$((scanned + 1))
# 抽出声明名(每行最后一个标识符),排序后取重复项
dups=$(grep -oE "$pattern" "$f" 2>/dev/null | awk '{print $NF}' | sort | uniq -d)
if [ -n "$dups" ]; then
while IFS= read -r name; do
[ -z "$name" ] && continue
n=$(grep -cE "$pattern[[:space:]]*\$|${pattern}([[:space:]]|\{)" "$f" 2>/dev/null || true)
lines=$(grep -nE "$pattern" "$f" | awk -v want="$name" '$NF==want || $0 ~ ("[[:space:]]"want"([[:space:]]|\\{|$)") {split($0,a,":"); printf "%s,", a[1]}')
echo " ${f}:${lines%,} 重复声明 '${name}'"
findings=$((findings + 1))
done <<< "$dups"
fi
done < <(find "$root" -name "$ext" -type f)

if [ "$findings" -gt 0 ]; then
echo "G2 重复闸失败:发现 $findings 处(扫描 $scanned 个文件)"
exit 1
fi

echo "G2 重复闸通过:$scanned 个文件无重复顶层声明"
exit 0