From 2eb644173e50069259966140c257e69e9fc31c03 Mon Sep 17 00:00:00 2001 From: MerCry Date: Mon, 23 Feb 2026 22:08:31 +0800 Subject: [PATCH 1/2] 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 2/2] 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."