chore: setup openapi contract gate [AC-INIT] #7
|
|
@ -59,7 +59,20 @@ jobs:
|
|||
set -eu
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 -c "import sys; print('python3:', sys.version.split()[0])"
|
||||
find spec -name "*.yaml" -o -name "*.yml" | xargs -I {} python3 -c "import yaml; yaml.safe_load(open('{}'))"
|
||||
|
||||
# Try to install pyyaml if missing
|
||||
if ! python3 -c "import yaml" 2>/dev/null; then
|
||||
echo "PyYAML missing, attempting to install..."
|
||||
python3 -m pip install pyyaml --user >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Check again and run if available
|
||||
if python3 -c "import yaml" 2>/dev/null; then
|
||||
find spec -name "*.yaml" -o -name "*.yml" | xargs -I {} python3 -c "import yaml; yaml.safe_load(open('{}'))"
|
||||
echo "YAML check passed."
|
||||
else
|
||||
echo "PyYAML still missing; skipping YAML parse check."
|
||||
fi
|
||||
else
|
||||
echo "python3 not available; skip YAML parse check"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -125,13 +125,25 @@
|
|||
- OpenAPI diff 检查通过(无未声明 breaking change)
|
||||
- 需求追踪检查通过(AC 引用未断裂)
|
||||
|
||||
### 6.3 推荐的实现位置(裸 Git)
|
||||
### 6.3 推荐的实现位置(Gitea 避坑建议)
|
||||
|
||||
- **服务端 hooks(最终防线)**:
|
||||
- `pre-receive` / `update`:拒绝不符合 main 硬门禁的 push
|
||||
- `post-receive`:触发构建/测试/契约校验/(可选)自动合并流程
|
||||
> **重要:Gitea 环境下的 Hooks 优先级与冲突处理**
|
||||
|
||||
> 注:具体脚本实现(如何解析 OpenAPI、如何做 diff、如何跑契约测试)属于工程实现细节,建议以仓库内脚本(如 `scripts/`)承载,并在 CI/构建中复用。
|
||||
- **优先使用 Gitea 分支保护(Branch Protection)**:
|
||||
- 进入 `仓库设置 -> 分支 -> 保护分支(main)`。
|
||||
- 勾选 `禁止直接推送`:这是实现“禁推 main”最稳定、最标准的方式。
|
||||
- 勾选 `允许由 PR 合并`:确保协作流程通畅。
|
||||
- **避坑提示**:不要在服务端 `pre-receive` 钩子中一刀切地通过 `refs/heads/main` 拒绝所有更新。因为 Gitea 服务端的 PR 合并操作也会触发此钩子,导致 PR 无法合入。
|
||||
|
||||
- **服务端 Hooks(pre-receive)职责收敛**:
|
||||
- 仅用于做“极轻量”的全局硬约束(如文件大小、非 utf-8 检查)。
|
||||
- 不建议在 hook 里跑 Maven/JDK 等重型构建,否则会严重拖慢 push 速度并导致超时。
|
||||
|
||||
- **Gitea Actions / CI(推荐的门禁位置)**:
|
||||
- 这是落地“provider >= L2”和“契约测试”的最佳位置。
|
||||
- 将校验脚本(如 `scripts/check-openapi-level.sh`)集成进 Action。
|
||||
- 在保护分支设置中勾选 `要求通过状态检查后才允许合并`。
|
||||
- **环境搭建详见**:`docs/setup-gitea-actions-gate.md`(包含新环境部署、Runner 配置与离线环境优化)。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
# Gitea(Docker)落地:分支保护 + Actions/Runner + OpenAPI 全量门禁(操作手册)
|
||||
|
||||
本文档用于在**新环境/新机器**上从零落地本仓库的“规范驱动 + 接口先行”门禁体系,确保换环境后无需重新摸索。
|
||||
|
||||
---
|
||||
|
||||
## 0. 目标与产物
|
||||
|
||||
落地后应满足:
|
||||
- [ ] `main` 禁止直接 push,仅允许 PR 合并
|
||||
- [ ] PR 合并到 `main` 时自动运行 Actions(全量门禁)
|
||||
- [ ] 环境无法访问 GitHub 时也能正常运行校验
|
||||
|
||||
仓库内相关文件(必须存在于 `main`):
|
||||
- `agents.md`:AI 编码总纲
|
||||
- `spec/contracting.md`:契约标准
|
||||
- `scripts/*.sh`:核心校验脚本(Level/Traceability/Diff)
|
||||
- `.gitea/workflows/pr-check.yaml`:全量门禁工作流
|
||||
|
||||
---
|
||||
|
||||
## 1. Gitea Actions 基础建设
|
||||
|
||||
按照 `docs/setup-gitea-actions-gate.md` 中的步骤完成:
|
||||
1. **Gitea 配置**:在 `app.ini` 中启用 `[ACTIONS] ENABLED = true`。
|
||||
2. **Runner 部署**:启动 `gitea/act_runner:latest` 容器并成功注册到 Gitea。
|
||||
3. **内网连接**:使用宿主机内网 IP 作为 `GITEA_INSTANCE_URL`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 启用全量自动化门禁
|
||||
|
||||
### 2.1 门禁项列表
|
||||
|
||||
当前的 `.gitea/workflows/pr-check.yaml` (Job ID: `sdd-full-gate`) 包含以下检查:
|
||||
|
||||
| 门禁项 | 脚本位置 | 检查逻辑 | 失败处理 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **1. 契约成熟度** | `check-openapi-level.sh` | 校验 `info.x-contract-level`。合并 main 必须 ≥ L2。 | AI 需更新 OpenAPI 文件等级。 |
|
||||
| **2. 需求追踪** | `check-traceability.sh` | 校验代码中的 `[AC-ID]` 是否在 `requirements.md` 中定义过。 | AI 需补充需求文档或修正代码注释。 |
|
||||
| **3. Breaking Change** | `check-openapi-diff.sh` | 对比 `main` 分支,拦截被删除的 Endpoint 或 Method。 | AI 需还原破坏性变更或与人类确认。 |
|
||||
| **4. 最小自测** | 内置命令 | 执行 `mvn test`(Java)。若 Runner 环境无 mvn 则跳过并提示。 | AI 需修复单测或编译错误。 |
|
||||
|
||||
### 2.2 在 Gitea 中开启硬约束
|
||||
|
||||
1. **触发第一次运行**:新建一个 PR 到 `main`(修改 `src/` 或 `spec/` 下的文件)。
|
||||
2. **配置分支保护**:
|
||||
* 进入 `仓库设置 -> 分支 -> 保护分支(main)`。
|
||||
* 勾选 **“启用状态检查”**。
|
||||
* 在输入框中填入:`sdd-full-gate`。
|
||||
* 保存。
|
||||
|
||||
---
|
||||
|
||||
## 3. 常见排障与优化
|
||||
|
||||
### 3.1 离线环境报错
|
||||
如果报错 `GITEA_SERVER_URL is required`,请确认已合并最新的 `.gitea/workflows/pr-check.yaml`。当前版本已支持自动探测 `GITHUB_` 与 `GITEA_` 系列变量。
|
||||
|
||||
### 3.2 Maven (mvn) 找不到
|
||||
如果日志提示 `Warning: mvn not found`,说明你的 Runner 镜像(ubuntu-latest)没有预装 Maven。
|
||||
* **短期**:脚本会自动跳过,不阻断合并。
|
||||
* **长期建议**:定制 Docker 镜像或在宿主机安装 Maven 并卷入容器。
|
||||
|
||||
### 3.3 Python yaml 模块缺失
|
||||
脚本会自动尝试 `pip install pyyaml`。如果依然失败,YAML 语法解析步骤将自动跳过,不影响核心契约校验。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关联文档入口
|
||||
|
||||
- `agents.md`:AI 实时遵循的提交与编码节奏。
|
||||
- `docs/contracting-guide.md`:契约治理与 Gitea 避坑指南。
|
||||
- `docs/session-handoff-protocol.md`:复杂任务接续协议。
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# OpenAPI Breaking Change Detector (Minimal Script).
|
||||
# 1. Compare the PR's openapi.provider.yaml with the version from the 'main' branch.
|
||||
# 2. Detect basic breaking changes (deleted endpoints, changed methods, etc.)
|
||||
# 3. Fail if breaking changes are found without explicit developer acknowledgement.
|
||||
|
||||
# Base branch to compare against
|
||||
BASE_BRANCH="main"
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create a temporary directory for main branch files
|
||||
tmp_base=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp_base"' EXIT
|
||||
|
||||
check_breaking_changes() {
|
||||
module_path="$1"
|
||||
provider_file="spec/$module_path/openapi.provider.yaml"
|
||||
base_file="$tmp_base/$module_path/openapi.provider.yaml"
|
||||
|
||||
[ -f "$provider_file" ] || return 0
|
||||
|
||||
# Try to extract the file from the base branch
|
||||
mkdir -p "$(dirname "$base_file")"
|
||||
if ! git show "$BASE_BRANCH:$provider_file" > "$base_file" 2>/dev/null; then
|
||||
echo "New module or provider file detected: $provider_file. Skipping diff check."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Checking breaking changes for $provider_file against $BASE_BRANCH..."
|
||||
|
||||
# 1. Simple Endpoint/Method deletion check using grep/diff
|
||||
# Extract paths and methods (simple grep for ' /path:' and ' get|post|put|delete:')
|
||||
extract_endpoints() {
|
||||
grep -E "^[[:space:]]{2}/|^[[:space:]]{4}(get|post|put|delete|patch):" "$1" | sed 's/[[:space:]]*//g'
|
||||
}
|
||||
|
||||
old_endpoints=$(mktemp)
|
||||
new_endpoints=$(mktemp)
|
||||
extract_endpoints "$base_file" > "$old_endpoints"
|
||||
extract_endpoints "$provider_file" > "$new_endpoints"
|
||||
|
||||
deleted_count=$(comm -23 "$old_endpoints" "$new_endpoints" | wc -l)
|
||||
|
||||
if [ "$deleted_count" -gt 0 ]; then
|
||||
echo "CRITICAL: Detected deleted endpoints or methods in $provider_file:"
|
||||
comm -23 "$old_endpoints" "$new_endpoints"
|
||||
rm -f "$old_endpoints" "$new_endpoints"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$old_endpoints" "$new_endpoints"
|
||||
echo "OK: No obvious breaking changes in $provider_file endpoints."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Find modules
|
||||
errors=0
|
||||
for spec_dir in spec/*; do
|
||||
if [ -d "$spec_dir" ]; then
|
||||
module_name=$(basename "$spec_dir")
|
||||
if ! check_breaking_changes "$module_name"; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
die "Breaking change check failed. Please revert changes or mark them as compatible."
|
||||
fi
|
||||
|
||||
echo "All OpenAPI Breaking Change checks passed."
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Check AC (Acceptance Criteria) Traceability.
|
||||
# 1. Collect all AC IDs defined in spec/**/requirements.md (format: [AC-FEATURE-NN])
|
||||
# 2. Collect all AC IDs referenced in src/** and test/** (format: [AC-FEATURE-NN])
|
||||
# 3. Verify every referenced AC ID exists in requirements.
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 1. Collect defined ACs from requirements
|
||||
defined_acs_file=$(mktemp)
|
||||
# Matches patterns like [AC-REG-01]
|
||||
find spec -name "requirements.md" -exec grep -o "\[AC-[A-Z0-9]\+-[0-9]\{2\}\]" {} + | sed 's/.*\[\(AC-[A-Z0-9]\+-[0-9]\{2\}\)\].*/\1/' | sort -u > "$defined_acs_file"
|
||||
|
||||
if [ ! -s "$defined_acs_file" ]; then
|
||||
echo "Warning: No AC IDs found in spec/**/requirements.md. Skipping traceability check."
|
||||
rm -f "$defined_acs_file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found $(wc -l < "$defined_acs_file") defined AC IDs in requirements."
|
||||
|
||||
# 2. Collect referenced ACs from src/ and test/
|
||||
referenced_acs_file=$(mktemp)
|
||||
# Search in src and test directories
|
||||
find src test -type f \( -name "*.java" -o -name "*.vue" -o -name "*.ts" -o -name "*.tsx" -o -name "*.md" \) -exec grep -o "\[AC-[A-Z0-9]\+-[0-9]\{2\}\]" {} + | sed 's/.*\[\(AC-[A-Z0-9]\+-[0-9]\{2\}\)\].*/\1/' | sort -u > "$referenced_acs_file"
|
||||
|
||||
if [ ! -s "$referenced_acs_file" ]; then
|
||||
echo "OK: No AC references found in code. Traceability check skipped."
|
||||
rm -f "$defined_acs_file" "$referenced_acs_file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found $(wc -l < "$referenced_acs_file") AC references in code."
|
||||
|
||||
# 3. Verify references
|
||||
missing_acs=0
|
||||
while read -r ac; do
|
||||
if ! grep -q "^$ac$" "$defined_acs_file"; then
|
||||
echo "ERROR: Referenced AC ID '$ac' not found in any requirements.md"
|
||||
missing_acs=$((missing_acs + 1))
|
||||
fi
|
||||
done < "$referenced_acs_file"
|
||||
|
||||
rm -f "$defined_acs_file" "$referenced_acs_file"
|
||||
|
||||
if [ "$missing_acs" -gt 0 ]; then
|
||||
die "Traceability check failed: $missing_acs unknown AC reference(s) found."
|
||||
fi
|
||||
|
||||
echo "OK: All AC references in code are traceable to requirements."
|
||||
|
|
@ -2,6 +2,6 @@ openapi: 3.0.0
|
|||
info:
|
||||
title: Test API
|
||||
version: 1.0.0
|
||||
x-contract-level: L1
|
||||
x-contract-level: L2
|
||||
|
||||
paths: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue