164dc2f72c
新增 14~19 节:假 bridge 数 publish 请求(坏报告 0 次/好报告 1 次)、--verify-only 单跑核对、检出协议保留、合并调用链(假 Gitea 跑真实 merge_pr,13 分支)、生成器落齐新脚本与空策略文件。首轮 1 处失败暴露真实缺陷:无远端引用时 --not --remotes 退化成整部历史(把 Initial commit 算进来)⇒ 改为只核 HEAD 并写明。
343 lines
19 KiB
Bash
343 lines
19 KiB
Bash
#!/usr/bin/env bash
|
||
# 反向验收夹具:**只在隔离副本里跑**。它故意弄坏东西,断言门禁会红;再恢复,断言会绿。
|
||
#
|
||
# 为什么要有它:一个"永远返回 0 的校验器"比没有校验更糟 —— 它给人一种已经检查过的错觉
|
||
# (正本 §0-2)。所以每条判据都要有反例,而且反例要在**隔离副本**里做,
|
||
# 绝不往日常 main 上推故意失败的提交(正本 §10)。
|
||
#
|
||
# 用法(在副本里):
|
||
# cp -a <repo> /tmp/kg-neg && cd /tmp/kg-neg && CI_NEGATIVE_FIXTURE=1 bash scripts/ci_negative_fixtures.sh
|
||
#
|
||
# 两道保险,防止有人在本仓里直接跑:
|
||
# 1. 必须显式设 CI_NEGATIVE_FIXTURE=1
|
||
# 2. 本仓有 origin 指向真实 Gitea 时拒绝运行(副本里通常没有 origin)
|
||
set -uo pipefail
|
||
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$ROOT"
|
||
|
||
if [ "${CI_NEGATIVE_FIXTURE:-}" != "1" ]; then
|
||
echo "❌ 拒绝运行:本脚本会故意弄坏文件。先设 CI_NEGATIVE_FIXTURE=1,并确认自己在隔离副本里。" >&2
|
||
exit 2
|
||
fi
|
||
if git remote get-url origin >/dev/null 2>&1; then
|
||
echo "❌ 拒绝运行:这个工作区有 origin 远端($(git remote get-url origin))—— 反向夹具只在无远端的副本里跑。" >&2
|
||
exit 2
|
||
fi
|
||
|
||
PY="${PYTHON:-python3}"
|
||
pass=0; fail=0
|
||
say() { printf '\n\033[1m== %s ==\033[0m\n' "$*"; }
|
||
ok() { pass=$((pass+1)); echo " ✅ $1"; }
|
||
no() { fail=$((fail+1)); echo " ❌ $1"; }
|
||
|
||
# expect <期望退出码> <名字> -- <命令...>
|
||
expect() {
|
||
local want="$1" name="$2"; shift 3
|
||
local out; out="$("$@" 2>&1)"; local got=$?
|
||
if [ "$got" = "$want" ]; then ok "$name(退出码 $got,符合预期)"
|
||
else no "$name:期望退出码 $want,实际 $got"; echo "$out" | tail -12 | sed 's/^/ /'; fi
|
||
}
|
||
|
||
[ -d web/node_modules ] || { echo "先装依赖:npm --prefix web ci" >&2; exit 2; }
|
||
|
||
say "0. 基线:现在应当全绿"
|
||
expect 0 "identity(事件 SHA = 当前 HEAD)" -- env GITHUB_SHA="$(git rev-parse HEAD)" "$PY" scripts/ci_checks.py --only identity
|
||
expect 0 "format" -- "$PY" scripts/ci_checks.py --only format
|
||
expect 0 "security-secrets" -- "$PY" scripts/ci_checks.py --only security-secrets
|
||
|
||
say "1. 格式错误必须红(Go / 前端各一条)"
|
||
printf 'package main\nfunc main( ){ println("x") }\n' > zz_fixture_bad.go
|
||
git add zz_fixture_bad.go
|
||
expect 1 "Go 未格式化 ⇒ format 红" -- "$PY" scripts/ci_checks.py --only format
|
||
git rm -q --cached zz_fixture_bad.go; rm -f zz_fixture_bad.go
|
||
|
||
printf 'export function f( x:number ){return x*2}\n' > web/src/zzFixture.ts
|
||
git -C "$ROOT" add web/src/zzFixture.ts 2>/dev/null || git add web/src/zzFixture.ts
|
||
expect 1 "前端未格式化(基线外的新文件)⇒ format 红" -- "$PY" scripts/ci_checks.py --only format
|
||
git rm -q --cached web/src/zzFixture.ts; rm -f web/src/zzFixture.ts
|
||
expect 0 "还原后 format 恢复绿" -- "$PY" scripts/ci_checks.py --only format
|
||
|
||
say "2. 前端测试失败必须让门禁红"
|
||
cat > web/src/zzFixture.test.ts <<'TS'
|
||
import { describe, expect, it } from 'vitest'
|
||
describe('fixture', () => {
|
||
it('故意失败', () => { expect(1).toBe(2) })
|
||
})
|
||
TS
|
||
expect 1 "vitest 有用例失败 ⇒ tests-web 红" -- "$PY" scripts/ci_checks.py --only tests-web
|
||
rm -f web/src/zzFixture.test.ts
|
||
expect 0 "删除后 tests-web 恢复绿" -- "$PY" scripts/ci_checks.py --only tests-web
|
||
|
||
say "3. 版本与变更记录契约"
|
||
cp CHANGELOG.md /tmp/kg-changelog.bak
|
||
python3 - <<'PY'
|
||
import pathlib
|
||
p = pathlib.Path('CHANGELOG.md')
|
||
t = p.read_text(encoding='utf-8')
|
||
# 造一个"没有日期"的已发布版本标题(结构自检必须抓住)
|
||
old = '## [0.1.0] - 2026-10-10'
|
||
assert t.count(old) == 1, f'夹具锚点不唯一({t.count(old)} 处)—— 别静默跳过'
|
||
p.write_text(t.replace(old, '## [9.9.9]', 1), encoding='utf-8')
|
||
PY
|
||
expect 1 "变更记录里的已发布版本缺日期 ⇒ contracts 红" -- "$PY" scripts/ci_checks.py --only contracts
|
||
cp /tmp/kg-changelog.bak CHANGELOG.md
|
||
|
||
# 变异测试:把**契约那一处**的注入目标路径写错 ⇒ 版本用例会 SKIP ⇒ 契约必须红(跳过不算通过)
|
||
# ⚠️ 必须只改契约那一处:ci_checks.py 里还有一处同名的注入是给 `build-go` 用的,
|
||
# 改错地方就会出现"夹具没生效、但看起来通过了"的假结论(这一版夹具第一稿就是这么错的)。
|
||
cp scripts/ci_checks.py /tmp/kg-cichecks.bak
|
||
python3 - <<'PY'
|
||
import pathlib
|
||
p = pathlib.Path('scripts/ci_checks.py')
|
||
t = p.read_text(encoding='utf-8')
|
||
needle = '-ldflags=-X knowledge-governance/internal/version.Number={version}'
|
||
assert t.count(needle) == 1, f'契约注入点不唯一({t.count(needle)} 处),夹具失效'
|
||
p.write_text(t.replace(needle, '-ldflags=-X knowledge-governance/internal/version.Nope={version}', 1),
|
||
encoding='utf-8')
|
||
PY
|
||
"$PY" scripts/ci_checks.py --only contracts > /tmp/kg-skip.log 2>&1; rc=$?
|
||
if [ "$rc" -ne 0 ] && grep -q 'SKIP' /tmp/kg-skip.log; then
|
||
ok "注入路径写错 ⇒ 版本用例被 SKIP ⇒ contracts 红(确认走的是 SKIP 分支)"
|
||
else
|
||
no "注入路径写错没有判红,或没走到 SKIP 分支(rc=$rc)"; tail -8 /tmp/kg-skip.log | sed 's/^/ /'
|
||
fi
|
||
cp /tmp/kg-cichecks.bak scripts/ci_checks.py
|
||
expect 0 "还原后 contracts 恢复绿" -- "$PY" scripts/ci_checks.py --only contracts
|
||
|
||
say "4. 依赖锁与例外文件"
|
||
mv web/package-lock.json /tmp/kg-lock.bak
|
||
expect 1 "缺 package-lock.json ⇒ deps-install 红(不退化无约束安装)" -- "$PY" scripts/ci_checks.py --only deps-install
|
||
mv /tmp/kg-lock.bak web/package-lock.json
|
||
mv security/dependency-exceptions.json /tmp/kg-exc.bak
|
||
expect 1 "例外登记表缺失 ⇒ security-deps 红(不静默放行)" -- "$PY" scripts/ci_checks.py --only security-deps
|
||
mv /tmp/kg-exc.bak security/dependency-exceptions.json
|
||
|
||
say "5. 合成密钥夹具(无真实秘密)必须被拦住"
|
||
python3 - <<'PY'
|
||
import pathlib
|
||
# 值在运行期拼出来:夹具文件里不留真命中文本
|
||
ak = "AKIA" + "Q7ZP2M4XR9TLB6VW"
|
||
pathlib.Path('zz_fixture_secret.txt').write_text(f"aws_access_key = \"{ak}\"\n", encoding='utf-8')
|
||
PY
|
||
git add zz_fixture_secret.txt
|
||
expect 1 "合成 AWS AK ⇒ security-secrets 红" -- "$PY" scripts/ci_checks.py --only security-secrets
|
||
git rm -q --cached zz_fixture_secret.txt; rm -f zz_fixture_secret.txt
|
||
|
||
say "6. 检出身份(事件 SHA 错配必须红)"
|
||
expect 1 "事件 SHA 与 HEAD 不一致 ⇒ identity 红" -- env GITHUB_SHA="0000000000000000000000000000000000000000" "$PY" scripts/ci_checks.py --only identity
|
||
expect 0 "事件 SHA 与 HEAD 一致 ⇒ identity 绿" -- env GITHUB_SHA="$(git rev-parse HEAD)" "$PY" scripts/ci_checks.py --only identity
|
||
|
||
say "7. 必需检查未运行不得报全绿(全量跑一次,故意让 format 红)"
|
||
printf 'package main\nfunc main( ){ println("x") }\n' > zz_fixture_bad.go
|
||
git add zz_fixture_bad.go
|
||
"$PY" scripts/ci_checks.py > /tmp/kg-neg-gate.log 2>&1; rc=$?
|
||
if [ "$rc" -ne 0 ]; then ok "全量门禁非 0(退出码 $rc)"; else no "全量门禁竟然返回 0"; fi
|
||
python3 - <<'PY' && ok "报告里后续必需项被记为 not_run,且总体为 failure" || no "报告的 not_run / overall 断言不成立"
|
||
import json, sys
|
||
d = json.load(open('ci-report.json'))
|
||
by = {c['id']: c['status'] for c in d['checks']}
|
||
assert by.get('format') == 'failure', by
|
||
later = [by.get(k) for k in ('tests-go', 'tests-web', 'contracts', 'security-deps')]
|
||
assert all(x == 'not_run' for x in later if x is not None), by
|
||
assert d['overall_status'] == 'failure', d['overall_status']
|
||
assert any('not_run' in p or '未运行' in p for p in d['problems']), d['problems']
|
||
PY
|
||
git rm -q --cached zz_fixture_bad.go; rm -f zz_fixture_bad.go
|
||
|
||
say "8. 上报:通知失败不改变质量结果;汇总不可用必须非 0"
|
||
python3 - "$(git rev-parse HEAD)" <<'PYPY'
|
||
import json, sys, pathlib
|
||
sys.path.insert(0, 'scripts')
|
||
from ci_checks import CHECK_MANIFEST, SUCCESS
|
||
sha = sys.argv[1]
|
||
# 夹具报告必须是**合规**的(真清单 + 完整 SHA + run_mode=full):上报脚本现在会独立核对,
|
||
# 不核对就会把"能跑通"误当成"结论可信"。
|
||
checks = [{"id": cid, "status": SUCCESS, "reason": "ok", "duration_s": 1, "required_in": list(req)}
|
||
for cid, req in CHECK_MANIFEST.items()]
|
||
rep = {"schema": "ci-report/1", "profile": "ci", "run_mode": "full", "commit_sha": sha,
|
||
"event_sha": sha, "overall_status": SUCCESS, "policy_revision": "fixture",
|
||
"manifest": {cid: list(req) for cid, req in CHECK_MANIFEST.items()},
|
||
"checks": checks, "duration_s": 2, "problems": []}
|
||
pathlib.Path("/tmp/kg-report-ok.json").write_text(json.dumps(rep, ensure_ascii=False), encoding="utf-8")
|
||
PYPY
|
||
CI_BRIDGE_RESOLVED="http://127.0.0.1:1" "$PY" .gitea/scripts/ci_report.py --report /tmp/kg-report-ok.json > /tmp/kg-notify.log 2>&1
|
||
rc=$?
|
||
if [ "$rc" = 0 ]; then ok "通知不可达时退出码仍是 0(质量结论不变)"; else no "通知失败把退出码带成了 $rc"; sed 's/^/ /' /tmp/kg-notify.log | tail -5; fi
|
||
if grep -q '"notification_status": "failed' /tmp/kg-report-ok.json; then ok "通知失败被单独标注(notification_status=failed)"; else no "没看到 notification_status 被标注"; fi
|
||
expect 1 "汇总报告缺失 ⇒ 上报脚本非 0(不能报绿)" -- "$PY" .gitea/scripts/ci_report.py --report /tmp/kg-does-not-exist.json
|
||
|
||
say "9. 必需/可选测试的区分(必需用例被跳过必须红)"
|
||
cp scripts/test-scope.json /tmp/kg-scope.bak
|
||
python3 - <<'PYPY'
|
||
import json, pathlib
|
||
p = pathlib.Path('scripts/test-scope.json')
|
||
d = json.loads(p.read_text(encoding='utf-8'))
|
||
# 删掉"整个 postgres 包可跳过"这条声明:该包里的 57 个跳过随即变成"必需用例被跳过"
|
||
d['optional'] = [e for e in d['optional']
|
||
if e.get('package') != 'knowledge-governance/internal/platform/postgres']
|
||
p.write_text(json.dumps(d, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
PYPY
|
||
expect 1 "未声明的必需用例被跳过 ⇒ tests-go 红" -- "$PY" scripts/ci_checks.py --only tests-go
|
||
cp /tmp/kg-scope.bak scripts/test-scope.json
|
||
expect 0 "恢复声明后 tests-go 回到绿" -- "$PY" scripts/ci_checks.py --only tests-go
|
||
mv scripts/test-scope.json /tmp/kg-scope2.bak
|
||
expect 1 "测试范围声明文件缺失 ⇒ tests-go 红(不许默认放行)" -- "$PY" scripts/ci_checks.py --only tests-go
|
||
mv /tmp/kg-scope2.bak scripts/test-scope.json
|
||
|
||
say "10. 前端 pending/todo 必须红"
|
||
printf "import { describe, it } from 'vitest'\ndescribe('fixture', () => { it.todo('故意待办') })\n" > web/src/zzPending.test.ts
|
||
expect 1 "有 todo 待跑用例 ⇒ tests-web 红" -- "$PY" scripts/ci_checks.py --only tests-web
|
||
rm -f web/src/zzPending.test.ts
|
||
expect 0 "删除后恢复绿" -- "$PY" scripts/ci_checks.py --only tests-web
|
||
|
||
say "11. 上报前必须独立核对报告(不能只看 overall_status)"
|
||
python3 - "$(git rev-parse HEAD)" <<'PYPY'
|
||
import json, sys, pathlib
|
||
sys.path.insert(0, 'scripts')
|
||
from ci_checks import CHECK_MANIFEST, SUCCESS
|
||
sha = sys.argv[1]
|
||
# 合规夹具必须用**真清单**造:报告里的清单与实现不一致时,上报脚本本来就应该拦下来
|
||
checks = [{"id": cid, "status": SUCCESS, "reason": "ok", "duration_s": 1,
|
||
"required_in": list(req)} for cid, req in CHECK_MANIFEST.items()]
|
||
base = {"schema": "ci-report/1", "profile": "ci", "run_mode": "full", "commit_sha": sha,
|
||
"event_sha": sha, "overall_status": SUCCESS,
|
||
"manifest": {cid: list(req) for cid, req in CHECK_MANIFEST.items()}, "checks": checks}
|
||
cases = {
|
||
"ok": base,
|
||
"short-sha": {**base, "commit_sha": sha[:12]},
|
||
"missing-check": {**base, "checks": [c for c in checks if c["id"] != "tests-go"]},
|
||
"dup-check": {**base, "checks": checks + [checks[0]]},
|
||
"unknown-check": {**base, "checks": checks + [{"id": "zz", "status": SUCCESS}]},
|
||
"profile-lie": {**base, "profile": "local"},
|
||
"manifest-drift": {**base, "manifest": {k: list(v) for k, v in CHECK_MANIFEST.items() if k != "format"}},
|
||
}
|
||
for name, rep in cases.items():
|
||
pathlib.Path(f"/tmp/kg-rep-{name}.json").write_text(json.dumps(rep, ensure_ascii=False), encoding="utf-8")
|
||
PYPY
|
||
expect 0 "合规报告 ⇒ 上报脚本 0" -- env GITHUB_ACTIONS=1 GITHUB_SHA="$(git rev-parse HEAD)" "$PY" .gitea/scripts/ci_report.py --no-post --report /tmp/kg-rep-ok.json
|
||
for c in short-sha missing-check dup-check unknown-check profile-lie manifest-drift; do
|
||
expect 1 "报告缺陷 $c ⇒ 上报脚本非 0(不能报绿)" -- env GITHUB_ACTIONS=1 GITHUB_SHA="$(git rev-parse HEAD)" "$PY" .gitea/scripts/ci_report.py --no-post --report /tmp/kg-rep-$c.json
|
||
done
|
||
|
||
say "12. 提交闸门用「本次范围」,不用「最近 N 条」"
|
||
T="$(mktemp -d)"
|
||
(
|
||
cd "$T" || exit 1
|
||
git init -q -b main
|
||
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
||
mkdir -p "$T/no-hooks"
|
||
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0="$T/no-hooks"
|
||
git commit -q --allow-empty -F - <<'M'
|
||
feat(a): 第一条合规
|
||
M
|
||
git commit -q --allow-empty -F - <<'M'
|
||
update stuff
|
||
M
|
||
BAD="$(git rev-parse HEAD)"
|
||
git commit -q --allow-empty -F - <<'M'
|
||
fix(b): 第三条合规
|
||
M
|
||
"$PY" "$ROOT/.gitea/scripts/ci_gate.py" --range "$BAD..HEAD" > /tmp/kg-range1.log 2>&1
|
||
echo "clean=$?"
|
||
"$PY" "$ROOT/.gitea/scripts/ci_gate.py" --range "HEAD~2..HEAD" > /tmp/kg-range2.log 2>&1
|
||
echo "dirty=$?"
|
||
) > /tmp/kg-range.log 2>&1
|
||
grep -q '^clean=0$' /tmp/kg-range.log && ok "干净范围通过(历史里有违规也不误伤 ⇒ 不是固定最近 N 条)" || { no "干净范围竟然判红"; sed 's/^/ /' /tmp/kg-range.log; }
|
||
grep -q '^dirty=1$' /tmp/kg-range.log && ok "范围含违规提交 ⇒ 判红" || { no "含违规的范围没有判红"; sed 's/^/ /' /tmp/kg-range.log; }
|
||
rm -rf "$T"
|
||
|
||
say "13. 通用模板/生成器:调用同一门禁入口、默认不生成 CD"
|
||
STD="${ENG_STANDARDS_DIR:-$HOME/eng-standards}"
|
||
GEN="$(mktemp -d)"
|
||
if [ -x "$STD/scripts/new-project.sh" ]; then
|
||
bash "$STD/scripts/new-project.sh" "$GEN/svc" Weknora/svc runner-x > /tmp/kg-gen.log 2>&1
|
||
if [ -f "$GEN/svc/.gitea/workflows/deploy-production.yml" ]; then no "生成器默认生成了 CD 工作流"; else ok "生成器默认不生成 CD 工作流"; fi
|
||
[ -f "$GEN/svc/scripts/ci_checks.py" ] && ok "生成器带了权威门禁实现" || no "生成器没带门禁实现"
|
||
[ -f "$GEN/svc/scripts/verify.sh" ] && ok "生成器带了门禁入口 shim" || no "生成器没带门禁入口"
|
||
if grep -q 'gofmt -l\|npm run build\|go vet' "$GEN/svc/.gitea/workflows/build.yml"; then
|
||
no "模板工作流里还留着内联的语言检查(第二套逻辑)"
|
||
else ok "模板工作流只调用门禁入口(没有内联第二套)"; fi
|
||
if grep -q '^ deploy' "$GEN/svc/.gitea/workflows/build.yml"; then no "build.yml 里还有 deploy 作业"; else ok "build.yml 里没有 deploy 作业"; fi
|
||
else
|
||
no "找不到规范仓的生成器(设 ENG_STANDARDS_DIR 指向它)"
|
||
fi
|
||
rm -rf "$GEN"
|
||
|
||
say "14. 第二轮修正:预览发布顺序(真实 ci_preview.py + 假 bridge 数请求)"
|
||
if python3 scripts/ci_preview_negative_test.py >/tmp/kg-preview-neg.log 2>&1; then
|
||
ok "预览:坏报告一个 publish 请求都不发、合规报告正好发 1 次(10 分支)"
|
||
else
|
||
no "预览发布负例失败,详见 /tmp/kg-preview-neg.log"
|
||
fi
|
||
|
||
say "15. 第二轮修正:提交范围取法(11 分支)+ 显示范围异常"
|
||
if python3 .gitea/scripts/ci_gate.py --self-test >/tmp/kg-scope.log 2>&1; then
|
||
ok "提交范围:截断/空范围/新分支/手工运行/shallow 全部分支符合预期"
|
||
else
|
||
no "提交范围自检失败,详见 /tmp/kg-scope.log"
|
||
fi
|
||
|
||
say "16. 第二轮修正:报告核对可单独调用(--verify-only)"
|
||
RD="$(mktemp -d)"
|
||
python3 - "$RD/rep.json" <<'PY'
|
||
import json, sys
|
||
sys.path.insert(0, "scripts")
|
||
import ci_checks
|
||
good = {"schema": "ci-report/1", "profile": "ci", "run_mode": "full",
|
||
"commit_sha": "a" * 40, "event_sha": "a" * 40,
|
||
"manifest": {c: list(r) for c, r in ci_checks.CHECK_MANIFEST.items()},
|
||
"checks": [{"id": c, "status": "success", "reason": ""} for c in ci_checks.CHECK_MANIFEST],
|
||
"overall_status": "success"}
|
||
json.dump(good, open(sys.argv[1], "w"))
|
||
json.dump({**good, "checks": [c for c in good["checks"] if c["id"] != "tests-go"]}, open(sys.argv[1] + ".missing", "w"))
|
||
PY
|
||
GITHUB_ACTIONS=true GITHUB_SHA=$(printf 'a%.0s' {1..40}) python3 .gitea/scripts/ci_report.py --verify-only --report "$RD/rep.json" >/dev/null 2>&1 \
|
||
&& ok "合规报告:--verify-only 退出 0(可采信)" || no "合规报告竟然核不过"
|
||
GITHUB_ACTIONS=true GITHUB_SHA=$(printf 'a%.0s' {1..40}) python3 .gitea/scripts/ci_report.py --verify-only --report "$RD/rep.json.missing" >/dev/null 2>&1 \
|
||
&& no "缺项报告:--verify-only 竟然退出 0" || ok "缺项报告:--verify-only 非 0(不可采信 ⇒ 预览不会发)"
|
||
rm -rf "$RD"
|
||
|
||
say "17. 第二轮修正:检出脚本的协议保留"
|
||
if [ "$(GITHUB_SERVER_URL='https://gitea.zheromiku.top' GITHUB_REPOSITORY='Weknora/knowledge-governance' bash .gitea/scripts/ci_checkout.sh --print-remote 2>/dev/null)" = "https://gitea.zheromiku.top/Weknora/knowledge-governance.git" ]; then
|
||
ok "https 服务端 ⇒ remote 仍是 https(没有降级)"
|
||
else
|
||
no "https 被降级或拼接不对"
|
||
fi
|
||
if GITHUB_SERVER_URL='ftp://nope' GITHUB_REPOSITORY='Weknora/knowledge-governance' bash .gitea/scripts/ci_checkout.sh --print-remote >/dev/null 2>&1; then
|
||
no "未知协议竟然被接受"
|
||
else
|
||
ok "未知协议被拒绝"
|
||
fi
|
||
|
||
say "18. 第二轮修正:合并只认明确策略(真实 merge_pr + 假 Gitea)"
|
||
if [ -f "$STD/scripts/adopt-existing.py" ]; then
|
||
if python3 "$STD/scripts/adopt-existing.py" --self-test-chain >/tmp/kg-merge-chain.log 2>&1; then
|
||
ok "合并调用链:13 分支里只有'策略齐+必需 context 全 success+完整 SHA+base/state 对'才发 merge"
|
||
else
|
||
no "合并调用链负例失败,详见 /tmp/kg-merge-chain.log"
|
||
fi
|
||
grep -q "required-contexts" "$STD/scripts/adopt-existing.py" \
|
||
&& ok "接入器只从明确策略取必需 context(不再用观察到的状态)" \
|
||
|| no "接入器里找不到策略来源"
|
||
else
|
||
no "找不到规范仓的接入器"
|
||
fi
|
||
|
||
say "19. 第二轮修正:生成器带齐新脚本与策略文件"
|
||
if [ -x "$STD/scripts/new-project.sh" ]; then
|
||
GEN2="$(mktemp -d)"
|
||
bash "$STD/scripts/new-project.sh" "$GEN2/svc" Weknora/svc2 runner-y >/tmp/kg-gen2.log 2>&1
|
||
for f in .gitea/scripts/ci_preview.py .gitea/scripts/ci_checkout.sh .gitea/required-contexts.txt scripts/check_secrets.py scripts/test-scope.json; do
|
||
[ -f "$GEN2/svc/$f" ] && ok "生成器落了 $f" || no "生成器没落 $f"
|
||
done
|
||
if grep -qE '^[[:space:]]*build / build' "$GEN2/svc/.gitea/required-contexts.txt"; then
|
||
no "生成器预填了猜出来的 context(会把没跑过必需检查的 PR 放过去)"
|
||
else
|
||
ok "策略文件故意留空(填之前合并会被拒)"
|
||
fi
|
||
rm -rf "$GEN2"
|
||
fi
|
||
|
||
printf '\n\033[1m════ 反向验收汇总 ════\033[0m\n ✅ 通过 %d | ❌ 失败 %d\n' "$pass" "$fail"
|
||
[ "$fail" -eq 0 ] || exit 1 |