diff --git a/.gitea/workflows/class-txt-check.yml b/.gitea/workflows/class-txt-check.yml new file mode 100644 index 000000000..eda9f6e45 --- /dev/null +++ b/.gitea/workflows/class-txt-check.yml @@ -0,0 +1,81 @@ +# .gitea/workflows/class-txt-check.yml +name: CLASS.TXT.json Check + +on: + pull_request: + types: [opened, reopened, synchronize, edited] + +permissions: + contents: read + pull-requests: write + +jobs: + class-txt-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Get changed CLASS.TXT.json files + id: changed-files + run: | + BASE_SHA="${{ gitea.event.pull_request.base.sha }}" + changed=$(git diff --name-only ${BASE_SHA}...HEAD | grep -E 'CLASS\.TXT\.json$' | tr '\n' ' ' || true) + echo "files=$changed" >> $GITHUB_OUTPUT + echo "CHANGED_FILES=$changed" >> $GITHUB_ENV + echo "Changed CLASS.TXT.json files: $changed" + + - name: Run duplicate title check + id: class-check + run: | + python3 .gitea/workflows/helpers/class-txt-check.py + + - name: Comment on PR with violations + if: failure() && steps.class-check.outcome == 'failure' + env: + GITEA_SERVER_URL: ${{ gitea.server_url }} + REPO: ${{ gitea.repository }} + PR_NUMBER: ${{ gitea.event.pull_request.number }} + TOKEN: ${{ gitea.token }} + run: | + set -euo pipefail + + # Ensure URL starts with http + if [[ ! "${GITEA_SERVER_URL}" =~ ^https?:// ]]; then + GITEA_SERVER_URL="http://${GITEA_SERVER_URL}" + echo "Added http:// prefix to URL" + fi + + # Generate comment message + MSG=$(python3 .gitea/workflows/helpers/class-comment.py) + echo "$MSG" + + # Extract body from JSON + BODY=$(echo "$MSG" | python3 -c "import sys, json; print(json.load(sys.stdin)['body'])") + + # Build the full URL + FULL_URL="${GITEA_SERVER_URL}/api/v1/repos/${REPO}/issues/${PR_NUMBER}/comments" + echo "Posting comment to: ${FULL_URL}" + + # Comment on PR + curl -sS -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${FULL_URL}" \ + -d "$(echo "$BODY" | python3 -c "import sys, json; print(json.dumps({'body': sys.stdin.read()}))")" + + - name: Final status + if: always() + run: | + if [ -f violations.json ]; then + echo "::error::CLASS.TXT.json check failed. See previous step for details." + exit 1 + fi diff --git a/.gitea/workflows/docs-precheck.yml b/.gitea/workflows/docs-precheck.yml index e031ae391..16a001241 100644 --- a/.gitea/workflows/docs-precheck.yml +++ b/.gitea/workflows/docs-precheck.yml @@ -39,7 +39,7 @@ jobs: - name: Run underscore check id: underscore-check run: | - python3 .gitea/workflows/underscore-check.py + python3 .gitea/workflows/helpers/underscore-check.py - name: Comment on PR with violations if: failure() && steps.underscore-check.outcome == 'failure' @@ -58,7 +58,7 @@ jobs: fi # Generate comment message - MSG=$(python3 .gitea/workflows/generate-comment.py) + MSG=$(python3 .gitea/workflows/helpers/underscore-comment.py) echo "$MSG" # Extract body from JSON diff --git a/.gitea/workflows/helpers/class-comment.py b/.gitea/workflows/helpers/class-comment.py new file mode 100755 index 000000000..a4bfbb4de --- /dev/null +++ b/.gitea/workflows/helpers/class-comment.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Generate PR comment from CLASS.TXT.json violations.""" + +import json +import sys + + +def main(): + try: + with open("violations.json", "r") as f: + violations = json.load(f) + except Exception: + violations = [] + + if not violations: + print(json.dumps({"body": "No violations to report"})) + sys.exit(0) + + # Group violations by file + by_file = {} + for v in violations: + key = v["file"] + if key not in by_file: + by_file[key] = [] + by_file[key].append(v) + + # Build message + lines = [ + "❌ **Duplicate title detected in CLASS.TXT.json**", + "", + "Found child documents with duplicate titles under the same parent:", + "", + ] + + for filepath, file_violations in by_file.items(): + lines.append(f"**{filepath}:**") + for v in file_violations: + parent_code = v["parent_code"] + parent_title = v["parent_title"] + duplicate_title = v["duplicate_title"] + codes = v["codes"] + + lines.append(f" - Parent: `{parent_title}` (code: `{parent_code}`)") + lines.append(f" Duplicate title: `{duplicate_title}`") + for code in codes: + lines.append(f" - Document code: `{code}`") + lines.append("") + + lines.append( + "**Please ensure all child documents under the same parent have unique titles.**" + ) + + message = "\n".join(lines) + print(json.dumps({"body": message})) + + +if __name__ == "__main__": + main() diff --git a/.gitea/workflows/helpers/class-txt-check.py b/.gitea/workflows/helpers/class-txt-check.py new file mode 100755 index 000000000..592c0432b --- /dev/null +++ b/.gitea/workflows/helpers/class-txt-check.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Check CLASS.TXT.json files for duplicate titles under the same parent.""" + +import sys +import os +import json + + +def check_duplicate_titles(json_path): + """ + Check for duplicate titles under the same parent. + + Returns list of violations: + [ + { + 'file': 'path/to/CLASS.TXT.json', + 'parent_code': '3', + 'parent_title': 'Virtual Private Cloud', + 'duplicate_title': 'Creating a VPC', + 'codes': ['4', '6'] + } + ] + """ + violations = [] + + # Load the JSON file + try: + with open(json_path, "r", encoding="utf-8") as f: + data = json.load(f) + except json.JSONDecodeError as e: + print(f"Error parsing JSON in {json_path}: {e}") + return [] + except Exception as e: + print(f"Error reading {json_path}: {e}") + return [] + + # Build a code-to-title map for looking up parent titles + code_to_title = {} + for entry in data: + code = entry.get("code", "") + title = entry.get("title", "") + if code and title: + code_to_title[code] = title + + # Group documents by p_code (parent code) + parent_groups = {} + for entry in data: + p_code = entry.get("p_code", "") + code = entry.get("code", "") + title = entry.get("title", "") + + # Skip if missing required fields + if not code or not title: + continue + + # Group by parent code + if p_code not in parent_groups: + parent_groups[p_code] = [] + + parent_groups[p_code].append({"code": code, "title": title}) + + # Check each parent group for duplicate titles + for p_code, children in parent_groups.items(): + # Group children by title (case-insensitive) + title_groups = {} + for child in children: + title_lower = child["title"].lower().strip() + if title_lower not in title_groups: + title_groups[title_lower] = [] + title_groups[title_lower].append(child) + + # Find duplicates (titles that appear more than once) + for title_lower, docs in title_groups.items(): + if len(docs) > 1: + # Get the original title (from first occurrence) + original_title = docs[0]["title"] + + # Get parent title + parent_title = code_to_title.get( + p_code, "(root)" if p_code == "" else f"(unknown parent: {p_code})" + ) + + # Collect document codes + codes = [doc["code"] for doc in docs] + + violations.append( + { + "file": json_path, + "parent_code": p_code if p_code else "(root)", + "parent_title": parent_title, + "duplicate_title": original_title, + "codes": codes, + } + ) + + return violations + + +def main(): + # Get changed files from environment + changed_files_str = os.environ.get("CHANGED_FILES", "") + changed_files = [f.strip() for f in changed_files_str.split() if f.strip()] + + if not changed_files: + print("No CLASS.TXT.json files changed in this PR") + sys.exit(0) + + print(f"Checking {len(changed_files)} CLASS.TXT.json file(s): {changed_files}") + + all_violations = [] + + for filepath in changed_files: + if not os.path.exists(filepath): + print(f"Warning: File not found: {filepath}") + continue + + violations = check_duplicate_titles(filepath) + all_violations.extend(violations) + + # Output results + if all_violations: + # Write violations to JSON file for later use + with open("violations.json", "w") as f: + json.dump(all_violations, f, indent=2) + + print(f"\nFound {len(all_violations)} violation(s):") + for v in all_violations: + print(f" {v['file']}") + print(f" Parent: {v['parent_title']} (code: {v['parent_code']})") + print(f' Duplicate title: "{v["duplicate_title"]}"') + print(f" Document codes: {', '.join(v['codes'])}") + + sys.exit(1) + else: + print("\nNo violations found") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.gitea/workflows/underscore-check.py b/.gitea/workflows/helpers/underscore-check.py similarity index 100% rename from .gitea/workflows/underscore-check.py rename to .gitea/workflows/helpers/underscore-check.py diff --git a/.gitea/workflows/generate-comment.py b/.gitea/workflows/helpers/underscore-comment.py similarity index 100% rename from .gitea/workflows/generate-comment.py rename to .gitea/workflows/helpers/underscore-comment.py diff --git a/docs/apig/umn/CLASS.TXT.json b/docs/apig/umn/CLASS.TXT.json index 0fcfe8334..d566ff3cf 100644 --- a/docs/apig/umn/CLASS.TXT.json +++ b/docs/apig/umn/CLASS.TXT.json @@ -515,7 +515,7 @@ { "desc":"Meaning: Request throttling policy.Scope of effect: Operation Object (2.0)/Operation Object (3.0)Example:", "product_code":"apig", - "title":"x-apigateway-ratelimit", + "title":"x-apigateway-ratelimits", "uri":"apig_03_0098.html", "doc_type":"usermanual", "p_code":"43",