From 2eb644173e50069259966140c257e69e9fc31c03 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 22:08:31 +0800 Subject: [PATCH 01/12] feat: test openapi gate with L1 level [AC-TEST] --- spec/test/openapi.provider.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 spec/test/openapi.provider.yaml diff --git a/spec/test/openapi.provider.yaml b/spec/test/openapi.provider.yaml new file mode 100644 index 0000000..ce7d5f5 --- /dev/null +++ b/spec/test/openapi.provider.yaml @@ -0,0 +1,6 @@ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 + x-contract-level: L1 +paths: {} From fc8197f6d506924b4afa55035e50253b78ebbefc Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 22:16:37 +0800 Subject: [PATCH 02/12] chore: setup openapi contract gate [AC-INIT] --- .gitea/workflows/pr-check.yaml | 29 ++++++++++ scripts/check-openapi-level.sh | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 .gitea/workflows/pr-check.yaml create mode 100644 scripts/check-openapi-level.sh diff --git a/.gitea/workflows/pr-check.yaml b/.gitea/workflows/pr-check.yaml new file mode 100644 index 0000000..cde342d --- /dev/null +++ b/.gitea/workflows/pr-check.yaml @@ -0,0 +1,29 @@ +name: PR Check (SDD Contract Gate) + +on: + pull_request: + branches: [ main ] + paths: + - 'spec/**/openapi.provider.yaml' + - 'spec/**/openapi.deps.yaml' + - 'src/**' + +jobs: + contract-level-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run OpenAPI Contract Level Check + env: + # For PRs targeting main, enforce provider >= L2 + REQUIRE_PROVIDER_L2: "1" + run: | + chmod +x scripts/check-openapi-level.sh + ./scripts/check-openapi-level.sh + + - name: YAML Lint (Optional) + run: | + # Simple check if yq or other linter is not available + find spec -name "*.yaml" -o -name "*.yml" | xargs -I {} python3 -c "import yaml, sys; yaml.safe_load(open('{}'))" || echo "YAML check skipped or failed" diff --git a/scripts/check-openapi-level.sh b/scripts/check-openapi-level.sh new file mode 100644 index 0000000..56ccbde --- /dev/null +++ b/scripts/check-openapi-level.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env sh +set -eu + +# Check OpenAPI contract levels for multi-module layout. +# - For PRs targeting main: require provider contract level >= L2. +# - Always require info.x-contract-level exists and is L0-L3. +# +# Expected locations: +# - spec//openapi.provider.yaml +# - spec//openapi.deps.yaml (optional) + +require_provider_l2="${REQUIRE_PROVIDER_L2:-0}" + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +level_rank() { + case "$1" in + L0) echo 0;; + L1) echo 1;; + L2) echo 2;; + L3) echo 3;; + *) echo -1;; + esac +} + +extract_level() { + # Extracts the first occurrence of info.x-contract-level: L? + # Accepts patterns like: + # x-contract-level: L2 + # x-contract-level: "L2" + # under info: + file="$1" + + awk ' + BEGIN{in_info=0; level=""} + { + # detect "info:" at any indentation + if ($0 ~ /^[[:space:]]*info:[[:space:]]*$/) { in_info=1; info_indent=match($0,/[^ ]/)-1; next } + if (in_info==1) { + # if indentation decreases or new top-level key begins, leave info block + cur_indent=match($0,/[^ ]/)-1; + if (cur_indent <= info_indent && $0 ~ /^[^[:space:]]/ ) { in_info=0 } + } + if (in_info==1 && level=="" && $0 ~ /^[[:space:]]*x-contract-level:[[:space:]]*/) { + line=$0 + sub(/^[[:space:]]*x-contract-level:[[:space:]]*/,"",line) + gsub(/"|\047/,"",line) + # strip comments + sub(/[[:space:]]*#.*/,"",line) + gsub(/[[:space:]]+/,"",line) + level=line + } + } + END{print level} + ' "$file" +} + +check_file_level() { + file="$1" + kind="$2" # provider|deps + + [ -f "$file" ] || die "Missing OpenAPI file: $file" + + level="$(extract_level "$file" | tr -d '\r')" + [ -n "$level" ] || die "$file: missing info.x-contract-level (expected under info: x-contract-level: L0|L1|L2|L3)" + + rank="$(level_rank "$level")" + [ "$rank" -ge 0 ] || die "$file: invalid x-contract-level '$level' (expected L0|L1|L2|L3)" + + if [ "$kind" = "provider" ] && [ "$require_provider_l2" = "1" ]; then + if [ "$rank" -lt 2 ]; then + die "$file: provider contract-level must be >= L2 for merge-to-main (current: $level)" + fi + fi + + echo "OK: $file level=$level" +} + +# Find all provider openapi files under spec/* +provider_files="$(find spec -mindepth 2 -maxdepth 2 -type f -name 'openapi.provider.yaml' 2>/dev/null || true)" +[ -n "$provider_files" ] || die "No provider OpenAPI found. Expected at least one spec//openapi.provider.yaml" + +# Provider files always must have a valid level; for main merges, require >= L2 +for f in $provider_files; do + check_file_level "$f" provider + +done + +# Deps files are optional, but if present must have valid level +for d in $(find spec -mindepth 2 -maxdepth 2 -type f -name 'openapi.deps.yaml' 2>/dev/null || true); do + check_file_level "$d" deps + +done + +echo "All OpenAPI contract level checks passed." From cca754acd49c8c441885504bba2fcd3320e74bd3 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 22:21:39 +0800 Subject: [PATCH 03/12] chore: setup openapi contract gate [AC-INIT] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a9feac..a55bf24 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # ai-robot - +测试改动 ai机器人 \ No newline at end of file From 6eb0dd275448726c5251f892110102cf7422c7a6 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 22:27:43 +0800 Subject: [PATCH 04/12] chore: setup openapi contract gate [AC-INIT] --- spec/test/openapi.provider.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/test/openapi.provider.yaml b/spec/test/openapi.provider.yaml index ce7d5f5..2a26810 100644 --- a/spec/test/openapi.provider.yaml +++ b/spec/test/openapi.provider.yaml @@ -3,4 +3,5 @@ info: title: Test API version: 1.0.0 x-contract-level: L1 + paths: {} From 465c049d0fd2953d9cbb0eceda5c55e3180dd211 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 23:03:31 +0800 Subject: [PATCH 05/12] chore: setup openapi contract gate [AC-INIT] --- .gitea/workflows/pr-check.yaml | 37 +++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/pr-check.yaml b/.gitea/workflows/pr-check.yaml index cde342d..ddc2fbc 100644 --- a/.gitea/workflows/pr-check.yaml +++ b/.gitea/workflows/pr-check.yaml @@ -4,26 +4,49 @@ on: pull_request: branches: [ main ] paths: - - 'spec/**/openapi.provider.yaml' - - 'spec/**/openapi.deps.yaml' + - '.gitea/workflows/**' + - 'scripts/**' + - 'spec/**' - 'src/**' jobs: contract-level-check: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Checkout code (no GitHub dependency) + shell: sh + run: | + set -eu + + # Expect Gitea-provided env vars in the runner + : "${GITEA_SERVER_URL:?GITEA_SERVER_URL is required}" + : "${GITEA_REPOSITORY:?GITEA_REPOSITORY is required}" + : "${GITEA_SHA:?GITEA_SHA is required}" + + echo "GITEA_SERVER_URL=$GITEA_SERVER_URL" + echo "GITEA_REPOSITORY=$GITEA_REPOSITORY" + echo "GITEA_SHA=$GITEA_SHA" + + git clone "$GITEA_SERVER_URL/$GITEA_REPOSITORY.git" . + git checkout "$GITEA_SHA" - name: Run OpenAPI Contract Level Check env: # For PRs targeting main, enforce provider >= L2 REQUIRE_PROVIDER_L2: "1" + shell: sh run: | + set -eu chmod +x scripts/check-openapi-level.sh ./scripts/check-openapi-level.sh - - name: YAML Lint (Optional) + - name: YAML Parse Check (Optional) + shell: sh run: | - # Simple check if yq or other linter is not available - find spec -name "*.yaml" -o -name "*.yml" | xargs -I {} python3 -c "import yaml, sys; yaml.safe_load(open('{}'))" || echo "YAML check skipped or failed" + 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('{}'))" + else + echo "python3 not available; skip YAML parse check" + fi From 3e33cbd71070731c26836ea9c07172edd2526397 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 23:10:20 +0800 Subject: [PATCH 06/12] chore: setup openapi contract gate [AC-INIT] --- .gitea/workflows/pr-check.yaml | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/pr-check.yaml b/.gitea/workflows/pr-check.yaml index ddc2fbc..e1250a1 100644 --- a/.gitea/workflows/pr-check.yaml +++ b/.gitea/workflows/pr-check.yaml @@ -18,17 +18,30 @@ jobs: run: | set -eu - # Expect Gitea-provided env vars in the runner - : "${GITEA_SERVER_URL:?GITEA_SERVER_URL is required}" - : "${GITEA_REPOSITORY:?GITEA_REPOSITORY is required}" - : "${GITEA_SHA:?GITEA_SHA is required}" + # Try GITHUB_ vars first (Gitea usually provides these for compatibility) + # Fallback to GITEA_ vars if GITHUB_ ones are empty + SERVER_URL="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}" + REPO_NAME="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}" + COMMIT_SHA="${GITHUB_SHA:-${GITEA_SHA:-}}" - echo "GITEA_SERVER_URL=$GITEA_SERVER_URL" - echo "GITEA_REPOSITORY=$GITEA_REPOSITORY" - echo "GITEA_SHA=$GITEA_SHA" + # If still empty, try to detect from environment + if [ -z "$SERVER_URL" ] || [ -z "$REPO_NAME" ] || [ -z "$COMMIT_SHA" ]; then + echo "Warning: Some standard env vars are missing. Printing environment for debugging (excluding secrets)..." + env | grep -E "GITEA|GITHUB|CI" | sort + fi - git clone "$GITEA_SERVER_URL/$GITEA_REPOSITORY.git" . - git checkout "$GITEA_SHA" + # Final validation + : "${SERVER_URL:?Could not determine SERVER_URL}" + : "${REPO_NAME:?Could not determine REPO_NAME}" + : "${COMMIT_SHA:?Could not determine COMMIT_SHA}" + + echo "Using SERVER_URL=$SERVER_URL" + echo "Using REPO_NAME=$REPO_NAME" + echo "Using COMMIT_SHA=$COMMIT_SHA" + + # Clone using the determined URL (assuming no auth required for internal runner or handled by runner) + git clone "$SERVER_URL/$REPO_NAME.git" . + git checkout "$COMMIT_SHA" - name: Run OpenAPI Contract Level Check env: From 132f1ffecbfe4135f1aa05302153661b7e1feb43 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 23:11:58 +0800 Subject: [PATCH 07/12] chore: setup openapi contract gate [AC-INIT] --- spec/test/openapi.provider.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/test/openapi.provider.yaml b/spec/test/openapi.provider.yaml index 2a26810..f030fed 100644 --- a/spec/test/openapi.provider.yaml +++ b/spec/test/openapi.provider.yaml @@ -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: {} From c035d4b7b4141c8dbbc1577b29fbe9db09dfcf66 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 23:16:03 +0800 Subject: [PATCH 08/12] chore: setup openapi contract gate [AC-INIT] --- .gitea/workflows/pr-check.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/pr-check.yaml b/.gitea/workflows/pr-check.yaml index e1250a1..0d7bcbf 100644 --- a/.gitea/workflows/pr-check.yaml +++ b/.gitea/workflows/pr-check.yaml @@ -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 From a21444634cbd2aee62983a39fcbd700d4b028dee Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 23:37:13 +0800 Subject: [PATCH 09/12] chore: setup openapi contract gate [AC-INIT] --- docs/contracting-guide.md | 22 ++++++--- docs/setup-gitea-actions-gate.md | 74 ++++++++++++++++++++++++++++++ scripts/check-openapi-diff.sh | 77 ++++++++++++++++++++++++++++++++ scripts/check-traceability.sh | 55 +++++++++++++++++++++++ 4 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 docs/setup-gitea-actions-gate.md create mode 100644 scripts/check-openapi-diff.sh create mode 100644 scripts/check-traceability.sh diff --git a/docs/contracting-guide.md b/docs/contracting-guide.md index 75cd4b5..8ac7b97 100644 --- a/docs/contracting-guide.md +++ b/docs/contracting-guide.md @@ -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 配置与离线环境优化)。 --- diff --git a/docs/setup-gitea-actions-gate.md b/docs/setup-gitea-actions-gate.md new file mode 100644 index 0000000..85afa3b --- /dev/null +++ b/docs/setup-gitea-actions-gate.md @@ -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`:复杂任务接续协议。 diff --git a/scripts/check-openapi-diff.sh b/scripts/check-openapi-diff.sh new file mode 100644 index 0000000..3a01c99 --- /dev/null +++ b/scripts/check-openapi-diff.sh @@ -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." diff --git a/scripts/check-traceability.sh b/scripts/check-traceability.sh new file mode 100644 index 0000000..ee24eb5 --- /dev/null +++ b/scripts/check-traceability.sh @@ -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." From 5133d574a43d71e39317743531705386f1579fbe Mon Sep 17 00:00:00 2001 From: MerCry Date: Tue, 24 Feb 2026 10:43:10 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=20=E9=80=82=E9=85=8Dgitea[?= =?UTF-8?q?AC-INIT]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/pr-check.yaml | 71 +++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/.gitea/workflows/pr-check.yaml b/.gitea/workflows/pr-check.yaml index 0d7bcbf..f5fe2b3 100644 --- a/.gitea/workflows/pr-check.yaml +++ b/.gitea/workflows/pr-check.yaml @@ -1,4 +1,4 @@ -name: PR Check (SDD Contract Gate) +name: PR Check (SDD Full Gate) on: pull_request: @@ -8,71 +8,80 @@ on: - 'scripts/**' - 'spec/**' - 'src/**' + - 'test/**' jobs: - contract-level-check: + sdd-full-gate: runs-on: ubuntu-latest steps: - name: Checkout code (no GitHub dependency) shell: sh run: | set -eu - - # Try GITHUB_ vars first (Gitea usually provides these for compatibility) - # Fallback to GITEA_ vars if GITHUB_ ones are empty SERVER_URL="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}" REPO_NAME="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}" COMMIT_SHA="${GITHUB_SHA:-${GITEA_SHA:-}}" - - # If still empty, try to detect from environment - if [ -z "$SERVER_URL" ] || [ -z "$REPO_NAME" ] || [ -z "$COMMIT_SHA" ]; then - echo "Warning: Some standard env vars are missing. Printing environment for debugging (excluding secrets)..." - env | grep -E "GITEA|GITHUB|CI" | sort + + # Try to get URL from git remote if env vars are missing + if [ -z "$SERVER_URL" ] || [ -z "$REPO_NAME" ]; then + echo "Warning: Env vars missing, attempting to detect from remote..." fi - # Final validation - : "${SERVER_URL:?Could not determine SERVER_URL}" - : "${REPO_NAME:?Could not determine REPO_NAME}" - : "${COMMIT_SHA:?Could not determine COMMIT_SHA}" - echo "Using SERVER_URL=$SERVER_URL" echo "Using REPO_NAME=$REPO_NAME" echo "Using COMMIT_SHA=$COMMIT_SHA" - # Clone using the determined URL (assuming no auth required for internal runner or handled by runner) git clone "$SERVER_URL/$REPO_NAME.git" . + git fetch origin main:main git checkout "$COMMIT_SHA" - - name: Run OpenAPI Contract Level Check + - name: 1. OpenAPI Contract Level Check env: - # For PRs targeting main, enforce provider >= L2 REQUIRE_PROVIDER_L2: "1" shell: sh run: | - set -eu - chmod +x scripts/check-openapi-level.sh + chmod +x scripts/*.sh ./scripts/check-openapi-level.sh + - name: 2. AC Traceability Check + shell: sh + run: ./scripts/check-traceability.sh + + - name: 3. OpenAPI Breaking Change Check + shell: sh + run: ./scripts/check-openapi-diff.sh + + - name: 4. Minimum Self-Test (mvn test) + shell: sh + run: | + # 针对 Java Spring 项目运行最小单测 (方案 B: 不存在则提示跳过) + if command -v mvn >/dev/null 2>&1; then + # 处理本地 jar 依赖:如果 lib 目录下存在 jar 包,先安装到本地仓库 + if [ -f "lib/commons-codec-1.9.jar" ]; then + echo "Installing local jar: lib/commons-codec-1.9.jar" + mvn -q install:install-file \ + -Dfile=lib/commons-codec-1.9.jar \ + -DgroupId=commons-codec \ + -DartifactId=commons-codec \ + -Dversion=1.9 \ + -Dpackaging=jar \ + -DgeneratePom=true + fi + + mvn -q -DskipTests=false test + else + echo "Warning: mvn not found, skipping unit tests. Please ensure Runner has JDK/Maven for full enforcement." + fi + - name: YAML Parse Check (Optional) shell: sh run: | - set -eu if command -v python3 >/dev/null 2>&1; then - python3 -c "import sys; print('python3:', sys.version.split()[0])" - - # 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 From d929f952cf9c4e505f38d0aec3efbf5f32162818 Mon Sep 17 00:00:00 2001 From: MerCry Date: Tue, 24 Feb 2026 10:50:39 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=20=E9=80=82=E9=85=8Dgitea[?= =?UTF-8?q?AC-INIT]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/pr-check.yaml | 23 +++++++++++++++-------- scripts/check-openapi-diff.sh | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/pr-check.yaml b/.gitea/workflows/pr-check.yaml index f5fe2b3..0e66a5c 100644 --- a/.gitea/workflows/pr-check.yaml +++ b/.gitea/workflows/pr-check.yaml @@ -21,19 +21,26 @@ jobs: SERVER_URL="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}" REPO_NAME="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}" COMMIT_SHA="${GITHUB_SHA:-${GITEA_SHA:-}}" - - # Try to get URL from git remote if env vars are missing - if [ -z "$SERVER_URL" ] || [ -z "$REPO_NAME" ]; then - echo "Warning: Env vars missing, attempting to detect from remote..." - fi + + : "${SERVER_URL:?Could not determine SERVER_URL}" + : "${REPO_NAME:?Could not determine REPO_NAME}" + : "${COMMIT_SHA:?Could not determine COMMIT_SHA}" echo "Using SERVER_URL=$SERVER_URL" echo "Using REPO_NAME=$REPO_NAME" echo "Using COMMIT_SHA=$COMMIT_SHA" - git clone "$SERVER_URL/$REPO_NAME.git" . - git fetch origin main:main - git checkout "$COMMIT_SHA" + if [ -d ".git" ]; then + echo "Repo already initialized in workspace; using fetch" + git remote set-url origin "$SERVER_URL/$REPO_NAME.git" + else + git clone "$SERVER_URL/$REPO_NAME.git" . + fi + + # 关键:不要把 main fetch 到本地分支 main(会冲突) + git fetch origin main:refs/remotes/origin/main + git fetch --depth=1 origin "$COMMIT_SHA" + git checkout -f "$COMMIT_SHA" - name: 1. OpenAPI Contract Level Check env: diff --git a/scripts/check-openapi-diff.sh b/scripts/check-openapi-diff.sh index 3a01c99..b8de879 100644 --- a/scripts/check-openapi-diff.sh +++ b/scripts/check-openapi-diff.sh @@ -7,7 +7,7 @@ set -eu # 3. Fail if breaking changes are found without explicit developer acknowledgement. # Base branch to compare against -BASE_BRANCH="main" +BASE_BRANCH="refs/remotes/origin/main" die() { echo "ERROR: $*" >&2 From 23e98cf50b3158ccfcf0c5311af5fc030a0e5bf8 Mon Sep 17 00:00:00 2001 From: MerCry Date: Tue, 24 Feb 2026 11:10:08 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=20=E9=80=82=E9=85=8Dgitea[?= =?UTF-8?q?AC-INIT]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/pr-check.yaml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/pr-check.yaml b/.gitea/workflows/pr-check.yaml index 0e66a5c..53262aa 100644 --- a/.gitea/workflows/pr-check.yaml +++ b/.gitea/workflows/pr-check.yaml @@ -42,7 +42,21 @@ jobs: git fetch --depth=1 origin "$COMMIT_SHA" git checkout -f "$COMMIT_SHA" - - name: 1. OpenAPI Contract Level Check + - name: 1. Commit Message Check + shell: sh + run: | + echo "Checking commit messages for [AC-...] or [TASK-...] (range: refs/remotes/origin/main..HEAD)" + # refs/remotes/origin/main is fetched in the checkout step + git log --no-merges --format=%B refs/remotes/origin/main..HEAD | cat + + if git log --no-merges --format=%B refs/remotes/origin/main..HEAD | grep -Eq '\[(AC|TASK)-'; then + echo "OK: Found [AC-...] or [TASK-...] in PR commits" + else + echo "ERROR: At least one commit message in the PR must contain [AC-...] or [TASK-...]" + exit 1 + fi + + - name: 2. OpenAPI Contract Level Check env: REQUIRE_PROVIDER_L2: "1" shell: sh @@ -50,15 +64,15 @@ jobs: chmod +x scripts/*.sh ./scripts/check-openapi-level.sh - - name: 2. AC Traceability Check + - name: 3. AC Traceability Check shell: sh run: ./scripts/check-traceability.sh - - name: 3. OpenAPI Breaking Change Check + - name: 4. OpenAPI Breaking Change Check shell: sh run: ./scripts/check-openapi-diff.sh - - name: 4. Minimum Self-Test (mvn test) + - name: 5. Minimum Self-Test (mvn test) shell: sh run: | # 针对 Java Spring 项目运行最小单测 (方案 B: 不存在则提示跳过)