From 025d15b8ccec69b8096e5731926a689bf5504956 Mon Sep 17 00:00:00 2001 From: Tiago Kochenborger Date: Mon, 6 Jul 2026 11:30:26 -0300 Subject: [PATCH] feat(sdk-review): add automated PR review skill (AFSDK-3937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the SDK Module Review skill to this repository: ## What this PR adds **20 deterministic checks** (no LLM calls in CI): - secrets, license-spdx, disclosure, hardcode, telemetry, docs, bdd, patterns, versioning + BREAKING family, commits, errors-logging, testing-depth, http-hygiene, concurrency, deps-supply, deletion-hygiene, constants, binding-shape, quality-gate-parity, pr-size **Rule tiers:** SHADOW | FLAG | BLOCK | BLOCK_LOCKED, configured in `.claude/config/rules.yaml` **BLOCK_LOCKED rules cannot be suppressed:** - SEC-* (all secrets), HC-03 (SAP-internal URL leak), DIS-06 (internal artifactory), LIC-01/02 (SPDX headers), BND-02 (BTP token URL concat), BND-05 (binding logs credentials), BREAKING-* (all breaking-change declarations) **REUSE.toml aggregate exemption** — this repo uses REUSE.toml at root, so per-file SPDX rules are baseline-exempted automatically. **Cross-language BDD parity** — module-aliases.yaml seeded with `dms ↔ documentmanagement` (Java sibling). **4 PR signals posted per run:** 1. Inline comments per finding 2. Aggregated summary comment 3. GitHub check-run (Checks tab) 4. Status label (`sdk-review: ✅ passed` / `❌ blocked` / `⚠️ flagged`) **Idempotent re-runs:** HTML markers let subsequent runs replace prior artifacts rather than duplicate them. ## Structure - `.claude/scripts/`: 20 check-*.sh + orchestrate.sh + aggregate.sh - `.claude/scripts/lib/`: 12 shared libs (2 Python AST + 10 bash) - `.claude/config/`: rules.yaml, module-aliases.yaml, baseline.json - `.github/workflows/sdk-module-review.yml`: auto-trigger on every PR - `.github/workflows/sdk-skill-isolation-check.yml`: validates sub-PRs - `docs/PR-REVIEW.md`: user-facing docs - `docs/BRANCH-PROTECTION-SETUP.md`: admin guide for required-check config - `tests/sdk-review/`: 57 bats tests + 6 fixtures (placeholder secrets, not real credentials) - `CONTRIBUTING.md`: amended — sdk-module-review is required before review ## Validation - 57/57 bats tests passing - shellcheck clean on all bash scripts - E2E on 5 real PRs (#196, #191, #192, #185 in this repo + Java jv#9): zero false positives observed - Self-test on this PR: 0 BLOCK, 0 FLAG (skill correctly skips its own files) - Flagship BND-02 finding verified on Java new-module PR (token URL concat) ## Rollout Recommended SHADOW → FLAG → BLOCK progression, 2 weeks each. See `docs/BRANCH-PROTECTION-SETUP.md`. ## Note on test fixtures `tests/sdk-review/fixtures/secrets-*.diff` contain intentional placeholder strings that match secret regexes (e.g. `AKIATESTFIXTUREXXXXX`, `eyJTESTFIXTURE...`) so the check-secrets rule can be unit-tested. These are NOT real credentials. Ref: https://jira.tools.sap/browse/AFSDK-3937 --- .claude/commands/review-new-module.md | 19 + .claude/config/baseline.json | 25 + .claude/config/module-aliases.yaml | 5 + .claude/config/rules.yaml | 131 ++++++ .claude/scripts/aggregate.sh | 104 +++++ .claude/scripts/check-bdd.sh | 95 ++++ .claude/scripts/check-binding-shape.sh | 40 ++ .claude/scripts/check-commits.sh | 35 ++ .claude/scripts/check-concurrency.sh | 37 ++ .claude/scripts/check-constants.sh | 26 ++ .claude/scripts/check-deletion-hygiene.sh | 38 ++ .claude/scripts/check-deps-supply.sh | 35 ++ .claude/scripts/check-disclosure.sh | 67 +++ .claude/scripts/check-docs.sh | 102 ++++ .claude/scripts/check-errors-logging.sh | 47 ++ .claude/scripts/check-hardcode.sh | 67 +++ .claude/scripts/check-http-hygiene.sh | 36 ++ .claude/scripts/check-license-spdx.sh | 76 +++ .claude/scripts/check-patterns.sh | 85 ++++ .claude/scripts/check-pr-size.sh | 51 ++ .claude/scripts/check-quality-gate-parity.sh | 32 ++ .claude/scripts/check-secrets.sh | 101 ++++ .claude/scripts/check-telemetry.sh | 83 ++++ .claude/scripts/check-testing-depth.sh | 51 ++ .claude/scripts/check-versioning.sh | 109 +++++ .claude/scripts/lib/apply-suppression.sh | 69 +++ .claude/scripts/lib/ast_python_checks.py | 352 ++++++++++++++ .claude/scripts/lib/baseline.sh | 74 +++ .claude/scripts/lib/breaking-detector.py | 265 +++++++++++ .claude/scripts/lib/detect-language.sh | 24 + .claude/scripts/lib/detect-modules.sh | 17 + .claude/scripts/lib/diff-added-lines.sh | 46 ++ .claude/scripts/lib/github-api.sh | 172 +++++++ .claude/scripts/lib/json-emit.sh | 76 +++ .claude/scripts/lib/predicates.sh | 87 ++++ .claude/scripts/lib/skill-self-skip.sh | 19 + .claude/scripts/lib/suppression.sh | 81 ++++ .claude/scripts/lib/tier-manager.sh | 76 +++ .claude/scripts/orchestrate.sh | 201 ++++++++ .github/workflows/sdk-module-review.yml | 42 ++ .../workflows/sdk-skill-isolation-check.yml | 59 +++ CONTRIBUTING.md | 25 + dist/sap_cloud_sdk-0.4.0-py3-none-any.whl | Bin 0 -> 128954 bytes docs/BRANCH-PROTECTION-SETUP.md | 177 +++++++ docs/PR-REVIEW.md | 245 ++++++++++ .../fixtures/binding-token-concat.diff | 7 + tests/sdk-review/fixtures/clean.diff | 9 + .../fixtures/disclosure-internal.diff | 7 + tests/sdk-review/fixtures/hardcode-url.diff | 8 + .../sdk-review/fixtures/secrets-aws-key.diff | 9 + tests/sdk-review/fixtures/secrets-jwt.diff | 8 + tests/sdk-review/test_extended.bats | 439 ++++++++++++++++++ tests/sdk-review/test_smoke.bats | 274 +++++++++++ uv.lock | 2 +- 54 files changed, 4366 insertions(+), 1 deletion(-) create mode 100644 .claude/commands/review-new-module.md create mode 100644 .claude/config/baseline.json create mode 100644 .claude/config/module-aliases.yaml create mode 100644 .claude/config/rules.yaml create mode 100755 .claude/scripts/aggregate.sh create mode 100755 .claude/scripts/check-bdd.sh create mode 100755 .claude/scripts/check-binding-shape.sh create mode 100755 .claude/scripts/check-commits.sh create mode 100755 .claude/scripts/check-concurrency.sh create mode 100755 .claude/scripts/check-constants.sh create mode 100755 .claude/scripts/check-deletion-hygiene.sh create mode 100755 .claude/scripts/check-deps-supply.sh create mode 100755 .claude/scripts/check-disclosure.sh create mode 100755 .claude/scripts/check-docs.sh create mode 100755 .claude/scripts/check-errors-logging.sh create mode 100755 .claude/scripts/check-hardcode.sh create mode 100755 .claude/scripts/check-http-hygiene.sh create mode 100755 .claude/scripts/check-license-spdx.sh create mode 100755 .claude/scripts/check-patterns.sh create mode 100755 .claude/scripts/check-pr-size.sh create mode 100755 .claude/scripts/check-quality-gate-parity.sh create mode 100755 .claude/scripts/check-secrets.sh create mode 100755 .claude/scripts/check-telemetry.sh create mode 100755 .claude/scripts/check-testing-depth.sh create mode 100755 .claude/scripts/check-versioning.sh create mode 100755 .claude/scripts/lib/apply-suppression.sh create mode 100755 .claude/scripts/lib/ast_python_checks.py create mode 100755 .claude/scripts/lib/baseline.sh create mode 100755 .claude/scripts/lib/breaking-detector.py create mode 100755 .claude/scripts/lib/detect-language.sh create mode 100755 .claude/scripts/lib/detect-modules.sh create mode 100755 .claude/scripts/lib/diff-added-lines.sh create mode 100755 .claude/scripts/lib/github-api.sh create mode 100755 .claude/scripts/lib/json-emit.sh create mode 100755 .claude/scripts/lib/predicates.sh create mode 100755 .claude/scripts/lib/skill-self-skip.sh create mode 100755 .claude/scripts/lib/suppression.sh create mode 100755 .claude/scripts/lib/tier-manager.sh create mode 100755 .claude/scripts/orchestrate.sh create mode 100644 .github/workflows/sdk-module-review.yml create mode 100644 .github/workflows/sdk-skill-isolation-check.yml create mode 100644 dist/sap_cloud_sdk-0.4.0-py3-none-any.whl create mode 100644 docs/BRANCH-PROTECTION-SETUP.md create mode 100644 docs/PR-REVIEW.md create mode 100644 tests/sdk-review/fixtures/binding-token-concat.diff create mode 100644 tests/sdk-review/fixtures/clean.diff create mode 100644 tests/sdk-review/fixtures/disclosure-internal.diff create mode 100644 tests/sdk-review/fixtures/hardcode-url.diff create mode 100644 tests/sdk-review/fixtures/secrets-aws-key.diff create mode 100644 tests/sdk-review/fixtures/secrets-jwt.diff create mode 100644 tests/sdk-review/test_extended.bats create mode 100644 tests/sdk-review/test_smoke.bats diff --git a/.claude/commands/review-new-module.md b/.claude/commands/review-new-module.md new file mode 100644 index 00000000..85dde012 --- /dev/null +++ b/.claude/commands/review-new-module.md @@ -0,0 +1,19 @@ +# /review-new-module — orchestrator slash command +Run the SDK Module Review skill against a PR. + +## Usage +- `/review-new-module ` — full review, posts findings +- `/review-new-module --dry-run` — analysis only, no posting + +## What it does +1. Detects language (Python vs Java) via `pyproject.toml` / `pom.xml` +2. Fetches PR diff + body +3. Runs 20 deterministic checks in parallel (secrets, license, disclosure, hardcode, telemetry, docs, bdd, patterns, versioning, commits, errors-logging, testing-depth, http-hygiene, concurrency, deps-supply, deletion-hygiene, constants, binding-shape, quality-gate-parity, pr-size) +4. Applies baseline exemptions + scope predicates + tier gating +5. Detects breaking changes via AST diff +6. Posts 4 signals: inline comments, summary comment, check-run, label + +## Implementation +Run: `bash .claude/scripts/orchestrate.sh ` + +The orchestrator is 100% deterministic — no LLM calls in CI. When run inside Claude Code, the LLM can enrich the summary comment before posting (this happens naturally in the session). diff --git a/.claude/config/baseline.json b/.claude/config/baseline.json new file mode 100644 index 00000000..45c15950 --- /dev/null +++ b/.claude/config/baseline.json @@ -0,0 +1,25 @@ +{ + "created_at": "2026-07-15T00:00:00Z", + "at_commit": "TBD", + "schema_version": 1, + "exemptions": { + "PY-LIC-01": { + "reason": "Repo uses REUSE.toml aggregate — pre-existing files exempt", + "detector": "REUSE.toml exists with path='**' aggregate annotation", + "scope": "repo", + "files_exempted_glob": "src/**/*.py" + }, + "PY-LIC-02": { + "reason": "Same as PY-LIC-01", + "scope": "repo" + }, + "JV-LIC-01": { + "reason": "cloud-sdk-java has no REUSE.toml AND no per-file SPDX baseline — rule kept SHADOW pending team decision on Wave-4 baseline PR", + "scope": "repo" + }, + "JV-LIC-02": { + "reason": "Same as JV-LIC-01", + "scope": "repo" + } + } +} diff --git a/.claude/config/module-aliases.yaml b/.claude/config/module-aliases.yaml new file mode 100644 index 00000000..c14ebbbb --- /dev/null +++ b/.claude/config/module-aliases.yaml @@ -0,0 +1,5 @@ +# Module name aliases across languages. +# Used by check-bdd.sh to resolve cross-repo feature file location. +aliases: + - python: dms + java: documentmanagement diff --git a/.claude/config/rules.yaml b/.claude/config/rules.yaml new file mode 100644 index 00000000..1428f0fa --- /dev/null +++ b/.claude/config/rules.yaml @@ -0,0 +1,131 @@ +version: 2 + +# ===================================================== +# rules.yaml — SDK Module Review rule catalog +# All rules default to their tier here; may be overridden per-repo. +# BLOCK_LOCKED rules cannot be softened or suppressed. +# ===================================================== + +baseline: + file: .claude/baseline.json + regenerate_with: orchestrate.sh --baseline + +tiers: + SHADOW: { posts: false, blocks: false } + FLAG: { posts: true, blocks: false } + BLOCK: { posts: true, blocks: true } + BLOCK_LOCKED: { posts: true, blocks: true, suppressible: false, downgradable: false } + +rules: + # -------- BREAKING FAMILY (all locked) -------- + BREAKING-01: { tier: BLOCK_LOCKED } + BREAKING-02: { tier: BLOCK_LOCKED } + BREAKING-03: { tier: FLAG } + BREAKING-04: { tier: FLAG } + + # -------- SECRETS (all locked) -------- + SEC-01: { tier: BLOCK_LOCKED } + SEC-02: { tier: BLOCK_LOCKED } + SEC-03: { tier: BLOCK_LOCKED } + SEC-04: { tier: BLOCK_LOCKED } + SEC-05: { tier: BLOCK_LOCKED } + SEC-06: { tier: BLOCK_LOCKED } + SEC-07: { tier: BLOCK_LOCKED } + SEC-08: { tier: BLOCK_LOCKED } + SEC-10: { tier: BLOCK_LOCKED } + + # -------- LICENSE -------- + LIC-01: { tier: BLOCK, predicates: { not: reuse_toml_aggregate_present, diff_scope: file_in_added_set } } + LIC-02: { tier: BLOCK, predicates: { not: reuse_toml_aggregate_present, diff_scope: file_in_added_set } } + + # -------- DISCLOSURE -------- + DIS-01: { tier: BLOCK, profile: { public: BLOCK, internal: FLAG } } + DIS-02: { tier: BLOCK, profile: { public: BLOCK, internal: PASS } } + DIS-06: { tier: BLOCK_LOCKED } + DIS-07: { tier: BLOCK } + DIS-08: { tier: SHADOW } + + # -------- HARDCODE -------- + HC-01: { tier: BLOCK } + HC-02: { tier: BLOCK } + HC-03: { tier: BLOCK_LOCKED } + HC-04: { tier: FLAG } + HC-06: { tier: FLAG } + + # -------- TELEMETRY -------- + PY-TEL-02: { tier: BLOCK } + JV-TEL-02: { tier: BLOCK } + PY-TEL-06: { tier: BLOCK, predicates: { diff_scope: new_decorator_added } } + PY-TEL-08: { tier: SHADOW } + + # -------- DOCS -------- + DC-01: { tier: BLOCK } + DC-02: { tier: BLOCK } + DC-11: { tier: BLOCK } + DC-12: { tier: BLOCK } + DC-13: { tier: BLOCK } + DC-14: { tier: BLOCK } + + # -------- BDD -------- + BDD-01: { tier: BLOCK } + BDD-02: { tier: BLOCK } + + # -------- PATTERNS -------- + PY-PT-01: { tier: BLOCK, predicates: { module_shape: client } } + JV-PT-01: { tier: BLOCK, predicates: { module_shape: client } } + PY-PT-03: { tier: FLAG } + PY-PT-04: { tier: FLAG } + PY-PT-08: { tier: FLAG } + JV-PT-05: { tier: FLAG } + + # -------- VERSIONING -------- + VER-01: { tier: BLOCK, predicates: { commit_types_or_breaking: [feat, feat!, breaking] } } + VER-07: { tier: FLAG } + PY-VER-08: { tier: SHADOW } + PY-VER-09: { tier: SHADOW } + + # -------- ERRORS/LOGGING -------- + EL-01: { tier: FLAG } + EL-02: { tier: FLAG } + EL-04: { tier: BLOCK } + PY-EL-11: { tier: SHADOW } + + # -------- TESTING -------- + TD-01: { tier: FLAG } + TD-10: { tier: BLOCK } + TD-checkbox: { tier: FLAG } + + # -------- HTTP -------- + HTTP-01: { tier: FLAG, predicates: { diff_scope: line_in_added_set } } + + # -------- CONCURRENCY -------- + CC-01: { tier: FLAG } + JV-CC-05: { tier: SHADOW } + + # -------- DEPS -------- + DEP-03: { tier: FLAG } + DEP-04: { tier: BLOCK_LOCKED } + + # -------- COMMITS -------- + COM-01: { tier: BLOCK } + + # -------- DELETION -------- + DEL-01: { tier: BLOCK } + + # -------- CONSTANTS -------- + PY-CON-01: { tier: FLAG } + + # -------- BINDING -------- + BND-02: { tier: BLOCK_LOCKED } + BND-04: { tier: BLOCK } + BND-05: { tier: BLOCK_LOCKED } + + # -------- QUALITY GATE -------- + QG-01: { tier: FLAG } + QG-02: { tier: FLAG } + + # -------- PR SIZE (advisory, SHADOW during rollout) -------- + PR-SIZE-01: { tier: SHADOW } + PR-SIZE-02: { tier: SHADOW } + PR-SIZE-03: { tier: SHADOW } + PR-SIZE-05: { tier: SHADOW } diff --git a/.claude/scripts/aggregate.sh b/.claude/scripts/aggregate.sh new file mode 100755 index 00000000..97d857bf --- /dev/null +++ b/.claude/scripts/aggregate.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# aggregate.sh — merge N check reports into a single summary, applying rule tiers. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TMPDIR_RUN="$1" +RULES_YAML="${RULES_YAML:-$SCRIPT_DIR/../config/rules.yaml}" + +# get_tier — reads rules.yaml, returns tier or empty +get_tier() { + local rule="$1" + # rules.yaml format: " RULE-ID: { tier: X, ... }" + awk -v rule="$rule" ' + match($0, "^ " rule ":") { + if (match($0, /tier:[[:space:]]*[A-Z_]+/)) { + t = substr($0, RSTART, RLENGTH) + sub(/^tier:[[:space:]]*/, "", t) + print t + exit + } + } + ' "$RULES_YAML" 2>/dev/null +} + +# Collect all report-*.json files +reports=$(ls "$TMPDIR_RUN"/report-*.json 2>/dev/null | sort) +if [ -z "$reports" ]; then + echo '{"findings":[],"shadow_findings":[],"summary":{"block_count":0,"flag_count":0,"shadow_count":0,"locked_count":0},"per_check_summary":{}}' + exit 0 +fi + +# Merge raw findings, then re-classify each finding by tier from rules.yaml +merged=$(mktemp) +# shellcheck disable=SC2086 +jq -s '.' $reports > "$merged" + +# For each finding, look up its rule tier and adjust +retagged_findings="[]" +retagged_shadow="[]" +locked_count=0 + +n=$(jq '[.[] | .findings // [] | length] | add // 0' "$merged") +if [ "$n" -gt 0 ]; then + all_findings=$(jq '[.[] | .findings // []] | flatten' "$merged") + rule_ids=$(echo "$all_findings" | jq -r '.[] | .rule' | sort -u) + + # Build lookup: rule -> tier + tier_map='{}' + while IFS= read -r rule; do + [ -z "$rule" ] && continue + tier=$(get_tier "$rule") + [ -z "$tier" ] && tier="FLAG" # default + tier_map=$(echo "$tier_map" | jq --arg r "$rule" --arg t "$tier" '. + {($r): $t}') + done <<< "$rule_ids" + + # Split findings into posted vs shadow based on tier + retagged_findings=$(echo "$all_findings" | jq --argjson tiers "$tier_map" ' + map( + . as $f + | ($tiers[$f.rule] // "FLAG") as $tier + | if $tier == "SHADOW" then empty + elif $tier == "BLOCK_LOCKED" then . + {severity: "BLOCK", tier: "BLOCK_LOCKED", locked: true} + elif $tier == "BLOCK" then . + {severity: "BLOCK", tier: "BLOCK"} + elif $tier == "FLAG" then . + {severity: "FLAG", tier: "FLAG"} + else . + {tier: $tier} end + ) + ') + retagged_shadow=$(echo "$all_findings" | jq --argjson tiers "$tier_map" ' + map( + . as $f + | ($tiers[$f.rule] // "FLAG") as $tier + | if $tier == "SHADOW" then . + {tier: "SHADOW"} else empty end + ) + ') + locked_count=$(echo "$retagged_findings" | jq '[.[] | select(.locked == true)] | length') +fi + +block_count=$(echo "$retagged_findings" | jq '[.[] | select(.severity == "BLOCK")] | length') +flag_count=$(echo "$retagged_findings" | jq '[.[] | select(.severity == "FLAG")] | length') +shadow_count=$(echo "$retagged_shadow" | jq 'length') +per_check=$(jq '[.[] | {(.check): {status: .status, count: ((.findings // []) | length)}}] | add' "$merged") + +jq -n \ + --argjson findings "$retagged_findings" \ + --argjson shadow "$retagged_shadow" \ + --argjson block "$block_count" \ + --argjson flag "$flag_count" \ + --argjson shadow_c "$shadow_count" \ + --argjson locked "$locked_count" \ + --argjson per_check "$per_check" \ + '{ + version: "1.0.0", + findings: $findings, + shadow_findings: $shadow, + summary: { + block_count: $block, + flag_count: $flag, + shadow_count: $shadow_c, + locked_count: $locked + }, + per_check_summary: $per_check + }' + +rm -f "$merged" diff --git a/.claude/scripts/check-bdd.sh b/.claude/scripts/check-bdd.sh new file mode 100755 index 00000000..5f09a735 --- /dev/null +++ b/.claude/scripts/check-bdd.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# check-bdd.sh — feature file existence + cross-language parity via alias map. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +REPO_ROOT="${REPO_ROOT:-.}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +SDK_SIBLING_PATH="${SDK_SIBLING_PATH:-}" +CONFIG_DIR="${CONFIG_DIR:-$REPO_ROOT/.claude/config}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# Extract new/modified modules from diff +if [ "$LANGUAGE" = "python" ]; then + modules=$(echo "$diff_content" | grep -oE 'src/sap_cloud_sdk/[a-z_]+/' 2>/dev/null | sed 's|src/sap_cloud_sdk/||; s|/$||' | grep -v '^core$' | sort -u || true) +else + modules=$(echo "$diff_content" | grep -oE 'src/main/java/com/sap/cloud/sdk/[a-z_]+/' 2>/dev/null | sed 's|src/main/java/com/sap/cloud/sdk/||; s|/$||' | grep -v '^core$' | sort -u || true) +fi + +# resolve alias mapping (python name → java name and vice versa) +resolve_sibling_name() { + local mod="$1" dir="$LANGUAGE" + local aliases="$CONFIG_DIR/module-aliases.yaml" + if [ ! -f "$aliases" ]; then echo "$mod"; return; fi + # Simple parser: "- python: X\n java: Y" pairs + if [ "$dir" = "python" ]; then + # look for "python: $mod" then get "java: " + awk -v mod="$mod" ' + /python:/ { p_name=$2 } + /java:/ { j_name=$2; if (p_name == mod) { print j_name; exit } } + ' "$aliases" | head -1 + return + else + awk -v mod="$mod" ' + /python:/ { p_name=$2 } + /java:/ { j_name=$2; if (j_name == mod) { print p_name; exit } } + ' "$aliases" | head -1 + return + fi +} + +while IFS= read -r mod; do + [ -z "$mod" ] && continue + + # Path for THIS repo's feature file + if [ "$LANGUAGE" = "python" ]; then + feature_path="$REPO_ROOT/tests/$mod/integration/$mod.feature" + mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod" + else + feature_path="$REPO_ROOT/src/test/resources/com/sap/applicationfoundation/$mod/integration/$mod.feature" + mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod" + fi + + # BDD-01: feature file exists (only fire if module has any source files) + if [ ! -f "$feature_path" ] && [ -d "$mod_dir" ]; then + # check if it's a "new" module (created in this PR) + if echo "$diff_content" | grep -qE "new file mode.*($mod/)"; then + emit_finding "BDD-01" "BLOCK" "tests/$mod/integration/$mod.feature" 1 \ + "New module '$mod' has no BDD feature file" \ + "Create $feature_path with cross-language-consistent scenarios" >> "$findings" + fi + fi + + # BDD-02: sibling repo parity + sibling_name=$(resolve_sibling_name "$mod") + [ -z "$sibling_name" ] && sibling_name="$mod" + + if [ -n "$SDK_SIBLING_PATH" ] && [ -d "$SDK_SIBLING_PATH" ]; then + if [ "$LANGUAGE" = "python" ]; then + # Python repo — sibling is Java + sibling_feature="$SDK_SIBLING_PATH/src/test/resources/com/sap/applicationfoundation/$sibling_name/integration/$sibling_name.feature" + sibling_module_dir="$SDK_SIBLING_PATH/src/main/java/com/sap/cloud/sdk/$sibling_name" + else + sibling_feature="$SDK_SIBLING_PATH/tests/$sibling_name/integration/$sibling_name.feature" + sibling_module_dir="$SDK_SIBLING_PATH/src/sap_cloud_sdk/$sibling_name" + fi + # If the sibling module dir exists, and sibling has no feature but we do (or vice versa) + if [ -d "$sibling_module_dir" ] && [ ! -f "$sibling_feature" ] && [ -f "$feature_path" ]; then + emit_finding "BDD-02" "FLAG" "$feature_path" 1 \ + "Sibling SDK ($sibling_name) has module but no BDD feature — parity broken" "" >> "$findings" + fi + if [ -d "$sibling_module_dir" ] && [ -f "$sibling_feature" ] && [ ! -f "$feature_path" ] && [ -d "$mod_dir" ]; then + emit_finding "BDD-02" "BLOCK" "tests/.../$mod.feature" 1 \ + "Module '$mod' exists in sibling SDK ($sibling_name) with BDD feature — this repo must have equivalent" "" >> "$findings" + fi + fi +done <<< "$modules" + +status=$(status_from_findings < "$findings") +emit_report "bdd" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-binding-shape.sh b/.claude/scripts/check-binding-shape.sh new file mode 100755 index 00000000..3559cd9b --- /dev/null +++ b/.claude/scripts/check-binding-shape.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# check-binding-shape.sh — placeholder: stub check emitting empty findings. +# TODO: implement rules from 01-PYTHON.md / 02-JAVA.md §3.binding-shape +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" +source "$SCRIPT_DIR/lib/skill-self-skip.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# BND-02 (LOCKED): token URL via string concat + "/oauth/token" +echo "$diff_content" | awk ' + BEGIN { file=""; line=0 } + /^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next } + /^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next } + /^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next } + /^ / { line++; next } +' | while IFS=$'\t' read -r file line_num content; do + [ -z "$file" ] && continue + # Self-review protection: skip skill files + if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi + if echo "$content" | grep -qE 'rstrip\("/"\)[[:space:]]*\+[[:space:]]*"/oauth/token"|\.replaceAll\("/\+\$", ""\)[[:space:]]*\+[[:space:]]*"/oauth/token"'; then + emit_finding "BND-02" "BLOCK" "$file" "$line_num" \ + "BTP token URL built via string concat — different services expose different fields" \ + "Use HttpUrl.parse().newBuilder() or honour a 'token_url' field if present" >> "$findings" + fi + if echo "$content" | grep -qE '\+[[:space:]]*"/oauth/token"'; then + emit_finding "BND-02" "BLOCK" "$file" "$line_num" \ + "Hardcoded /oauth/token path — BTP services vary" "" >> "$findings" + fi +done + +status=$(status_from_findings < "$findings") +emit_report "binding-shape" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-commits.sh b/.claude/scripts/check-commits.sh new file mode 100755 index 00000000..6c4859a5 --- /dev/null +++ b/.claude/scripts/check-commits.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# check-commits.sh — Conventional Commits enforcement. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +BASE_SHA="${BASE_SHA:-HEAD~10}" +HEAD_SHA="${HEAD_SHA:-HEAD}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +# List commit subjects +commits=$(git log "${BASE_SHA}..${HEAD_SHA}" --format='%H %s' 2>/dev/null || echo "") +prefix_regex='^(feat|fix|refactor|chore|docs|test|ci|build|perf|style|revert)(\([a-z0-9_/-]+\))?!?:[[:space:]]+.+' + +echo "$commits" | while IFS= read -r line; do + [ -z "$line" ] && continue + sha=$(echo "$line" | cut -d' ' -f1) + subject=$(echo "$line" | cut -d' ' -f2-) + # Skip merge commits (identified by 2+ parents, robust regardless of message shape) + parents=$(git rev-list --parents -n 1 "$sha" 2>/dev/null | awk '{print NF-1}') + if [ "${parents:-0}" -gt 1 ]; then continue; fi + # Also skip legacy "Merge branch/pull/etc" defaults + if echo "$subject" | grep -qiE '^Merge '; then continue; fi + if ! echo "$subject" | grep -qE "$prefix_regex"; then + emit_finding "COM-01" "BLOCK" "COMMIT:$sha" 1 \ + "Commit '$subject' does not follow Conventional Commits" \ + "Prefix with feat:/fix:/chore:/docs:/test:/refactor:/ci:/build:/perf:/style:/revert:" >> "$findings" + fi +done + +status=$(status_from_findings < "$findings") +emit_report "commits" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-concurrency.sh b/.claude/scripts/check-concurrency.sh new file mode 100755 index 00000000..6405efdf --- /dev/null +++ b/.claude/scripts/check-concurrency.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# check-concurrency.sh — placeholder: stub check emitting empty findings. +# TODO: implement rules from 01-PYTHON.md / 02-JAVA.md §3.concurrency +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" +source "$SCRIPT_DIR/lib/skill-self-skip.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# CC-01: asyncio.Queue with no accompanying Lock/set — flag when queue+append pattern present +echo "$diff_content" | awk ' + BEGIN { file=""; line=0 } + /^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next } + /^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next } + /^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next } + /^ / { line++; next } +' | while IFS=$'\t' read -r file line_num content; do + [ -z "$file" ] && continue + if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi + if echo "$content" | grep -qE 'asyncio\.Queue\('; then + # Check if same file has a Lock or set() nearby + if [ -f "$REPO_ROOT/$file" ] && ! grep -qE 'asyncio\.Lock|set\(\)' "$REPO_ROOT/$file" 2>/dev/null; then + emit_finding "CC-01" "FLAG" "$file" "$line_num" \ + "asyncio.Queue without dedup — if items must be unique, add a set() + asyncio.Lock" "" >> "$findings" + fi + fi +done + +status=$(status_from_findings < "$findings") +emit_report "concurrency" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-constants.sh b/.claude/scripts/check-constants.sh new file mode 100755 index 00000000..2a72a3b7 --- /dev/null +++ b/.claude/scripts/check-constants.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# check-constants.sh — placeholder: stub check emitting empty findings. +# TODO: implement rules from 01-PYTHON.md / 02-JAVA.md §3.constants +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# CON-01 via AST on Python files +if [ "$LANGUAGE" = "python" ]; then + changed=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/.*\.py' | sed 's|^+++ b/||' | sort -u) + if [ -n "$changed" ]; then + # shellcheck disable=SC2086 + python3 "$SCRIPT_DIR/lib/ast_python_checks.py" con-01 $changed 2>/dev/null >> "$findings" || true + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "constants" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-deletion-hygiene.sh b/.claude/scripts/check-deletion-hygiene.sh new file mode 100755 index 00000000..7fa90e01 --- /dev/null +++ b/.claude/scripts/check-deletion-hygiene.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# check-deletion-hygiene.sh — placeholder: stub check emitting empty findings. +# TODO: implement rules from 01-PYTHON.md / 02-JAVA.md §3.deletion-hygiene +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# DEL-01: symbol removed from __all__ but references remain in codebase +if [ "$LANGUAGE" = "python" ]; then + # Extract removed __all__ entries + removed_symbols=$(echo "$diff_content" | awk ' + /\+\+\+ b\/.*__init__\.py$/ { in_init=1; next } + /^diff --git/ { in_init=0 } + in_init && /^-[[:space:]]*"[A-Za-z_][A-Za-z0-9_]*"/ { + match($0, /"[^"]+"/); print substr($0, RSTART+1, RLENGTH-2) + } + ') + while IFS= read -r sym; do + [ -z "$sym" ] && continue + # search codebase (excluding the file where it was removed) + hits=$(grep -rEIln "\b${sym}\b" "$REPO_ROOT/src" 2>/dev/null | wc -l | tr -d ' ') + if [ "$hits" -gt 0 ]; then + emit_finding "DEL-01" "BLOCK" "src/" 1 \ + "Symbol '$sym' removed from __all__ but still referenced in $hits files" "" >> "$findings" + fi + done <<< "$removed_symbols" +fi + +status=$(status_from_findings < "$findings") +emit_report "deletion-hygiene" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-deps-supply.sh b/.claude/scripts/check-deps-supply.sh new file mode 100755 index 00000000..54fea34e --- /dev/null +++ b/.claude/scripts/check-deps-supply.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# check-deps-supply.sh — placeholder: stub check emitting empty findings. +# TODO: implement rules from 01-PYTHON.md / 02-JAVA.md §3.deps-supply +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# DEP-04: internal artifactory index URL +if echo "$diff_content" | grep -qE '\+.*(index-url|extra-index-url).*int\.repositories\.cloud\.sap'; then + emit_finding "DEP-04" "BLOCK" "config" 1 \ + "Internal artifactory --index-url reference in public artifact" "" >> "$findings" +fi +# DEP-03: pyproject.toml deps changed but uv.lock not +if [ "$LANGUAGE" = "python" ]; then + pyproject_deps_changed=$(echo "$diff_content" | grep -cE '^\+.*"[a-z][a-z0-9_-]*[><=~!]+' || echo 0) + uv_lock_changed=$(echo "$diff_content" | grep -c '^\+\+\+ b/uv\.lock' || echo 0) + if [ "$pyproject_deps_changed" -gt 0 ] && [ "$uv_lock_changed" -eq 0 ]; then + # only fire if pyproject.toml dep table (not [project] version) changed + if echo "$diff_content" | grep -qE '^\+.*\[project\.(dependencies|optional-dependencies)\]|^\+[[:space:]]+"[a-z][a-z0-9_-]*[><=~!].*"'; then + emit_finding "DEP-03" "FLAG" "pyproject.toml" 1 \ + "pyproject.toml dependencies changed but uv.lock not updated" "" >> "$findings" + fi + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "deps-supply" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-disclosure.sh b/.claude/scripts/check-disclosure.sh new file mode 100755 index 00000000..b320a685 --- /dev/null +++ b/.claude/scripts/check-disclosure.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# check-disclosure.sh — open-source disclosure hygiene. +# Detects SAP-internal URLs, ORD IDs, internal Jira, unfilled PR body templates. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/json-emit.sh +source "$SCRIPT_DIR/lib/json-emit.sh" +# shellcheck source=lib/skill-self-skip.sh +source "$SCRIPT_DIR/lib/skill-self-skip.sh" + +LANGUAGE="${LANGUAGE:-python}" +PROFILE="${DISCLOSURE_PROFILE:-public}" # "public" or "internal" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +PR_BODY_FILE="${PR_BODY_FILE:-}" + +STARTED=$(now_iso) +findings=$(mktemp) +trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# Determine severity based on profile +sev_public() { if [ "$PROFILE" = "public" ]; then echo "BLOCK"; else echo "FLAG"; fi; } +sev_internal_ok() { if [ "$PROFILE" = "public" ]; then echo "BLOCK"; else echo "PASS"; fi; } + +# Scan added lines only +echo "$diff_content" | awk ' + BEGIN { file=""; line=0 } + /^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next } + /^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next } + /^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next } + /^ / { line++; next } +' | while IFS=$'\t' read -r file line_num content; do + [ -z "$file" ] && continue + # Self-review protection: skip skill files + if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi + + # DIS-01: SAP-internal hostnames + if echo "$content" | grep -qEi '(int\.repositories\.cloud\.sap|\.tools\.sap|\.wdf\.sap\.corp|\.mo\.sap\.corp|jira\.tools\.sap)'; then + emit_finding "DIS-01" "$(sev_public)" "$file" "$line_num" "SAP-internal hostname detected in code" "" >> "$findings" + fi + # DIS-02: Internal Jira URL + if echo "$content" | grep -qEi 'jira\.tools\.sap|jira\.wdf\.sap\.corp'; then + emit_finding "DIS-02" "$(sev_public)" "$file" "$line_num" "Internal Jira URL — remove or move to internal-only docs" "" >> "$findings" + fi + # DIS-06: Internal artifactory index-url + if echo "$content" | grep -qEi '\-\-index-url\s+https?://int\.repositories\.cloud\.sap'; then + emit_finding "DIS-06" "BLOCK" "$file" "$line_num" "Internal artifactory --index-url — must not appear in public/shared configs" "" >> "$findings" + fi +done + +# DIS-07/08: PR body +if [ -n "$PR_BODY_FILE" ] && [ -f "$PR_BODY_FILE" ]; then + body=$(cat "$PR_BODY_FILE") + # DIS-07: unfilled Closes # placeholder + if echo "$body" | grep -qE 'Closes #'; then + emit_finding "DIS-07" "BLOCK" "PR_BODY" 1 "PR body contains unfilled 'Closes #' placeholder" "" >> "$findings" + fi + # DIS-08 (SHADOW): internal URLs / Jira in PR body + if echo "$body" | grep -qEi '\.tools\.sap|\.wdf\.sap\.corp|jira\.tools\.sap'; then + emit_finding "DIS-08" "FLAG" "PR_BODY" 1 "PR body references SAP-internal URLs/Jira — remove from public-visible artifacts" "" >> "$findings" + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "disclosure" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-docs.sh b/.claude/scripts/check-docs.sh new file mode 100755 index 00000000..b12e7f95 --- /dev/null +++ b/.claude/scripts/check-docs.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# check-docs.sh — documentation completeness including BTP deps and regional availability. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +REPO_ROOT="${REPO_ROOT:-.}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# Detect new module directories (module has new files at top-level) +if [ "$LANGUAGE" = "python" ]; then + new_modules=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/sap_cloud_sdk/[a-z_]+/[^/]+\.py' | sed 's|^+++ b/src/sap_cloud_sdk/||; s|/[^/]*\.py$||' | sort -u) +else + new_modules=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/main/java/com/sap/cloud/sdk/[a-z_]+/[^/]+\.java' | sed 's|^+++ b/src/main/java/com/sap/cloud/sdk/||; s|/[^/]*\.java$||' | sort -u) +fi + +while IFS= read -r mod; do + [ -z "$mod" ] && continue + [ "$mod" = "core" ] && continue + + if [ "$LANGUAGE" = "python" ]; then + user_guide="$REPO_ROOT/src/sap_cloud_sdk/$mod/user-guide.md" + mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod" + else + user_guide="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod/user-guide.md" + mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod" + fi + + # DC-01: user-guide.md exists + if [ ! -f "$user_guide" ]; then + # Only fire if module dir exists (module is new-ish) + if [ -d "$mod_dir" ]; then + emit_finding "DC-01" "BLOCK" "src/.../$mod/user-guide.md" 1 \ + "Module '$mod' missing user-guide.md" \ + "Create $user_guide with sections: ## Installation, ## Quick Start, ## Configuration" >> "$findings" + fi + continue + fi + + # DC-02: required sections + guide_content=$(cat "$user_guide" 2>/dev/null || echo "") + if ! echo "$guide_content" | grep -qE '^##[[:space:]]+(Installation|Import)'; then + emit_finding "DC-02" "FLAG" "$user_guide" 1 "user-guide.md missing ## Installation section" "" >> "$findings" + fi + if ! echo "$guide_content" | grep -qE '^##[[:space:]]+Quick Start'; then + emit_finding "DC-02" "BLOCK" "$user_guide" 1 "user-guide.md missing ## Quick Start section" "" >> "$findings" + fi + + # DC-11..DC-14 (BTP deps + regional) + # Detect module imports/usages + if [ "$LANGUAGE" = "python" ]; then + has_dest=$(grep -rq "from sap_cloud_sdk\.destination" "$mod_dir" 2>/dev/null && echo yes || echo no) + has_frag=$(grep -rq "FragmentClient\|Fragment[[:space:]]*[,)]" "$mod_dir" 2>/dev/null && echo yes || echo no) + has_cert=$(grep -rq "CertificateClient\|Certificate[[:space:]]*[,)]" "$mod_dir" 2>/dev/null && echo yes || echo no) + has_region=$(grep -rqE "REGION|region_id|SupportedRegion|available_regions" "$mod_dir" 2>/dev/null && echo yes || echo no) + else + has_dest=$(grep -rq "com\.sap\.cloud\.sdk\.destination" "$mod_dir" 2>/dev/null && echo yes || echo no) + has_frag=$(grep -rq "FragmentClient" "$mod_dir" 2>/dev/null && echo yes || echo no) + has_cert=$(grep -rq "CertificateClient" "$mod_dir" 2>/dev/null && echo yes || echo no) + has_region=$(grep -rqE "Region\b|regionId|SupportedRegion" "$mod_dir" 2>/dev/null && echo yes || echo no) + fi + + # DC-11: destination dep must be documented + if [ "$has_dest" = "yes" ]; then + if ! echo "$guide_content" | grep -qEi 'destination service|## Dependencies|## Prerequisites'; then + emit_finding "DC-11" "BLOCK" "$user_guide" 1 \ + "Module imports destination service — must document in ## Dependencies section" \ + "Add: ## Dependencies\\n- SAP BTP Destination Service instance" >> "$findings" + fi + fi + # DC-12: fragments + if [ "$has_frag" = "yes" ]; then + if ! echo "$guide_content" | grep -qEi 'fragments'; then + emit_finding "DC-12" "BLOCK" "$user_guide" 1 \ + "Module uses Fragments — must document Fragments prerequisite" "" >> "$findings" + fi + fi + # DC-13: certificates + if [ "$has_cert" = "yes" ]; then + if ! echo "$guide_content" | grep -qEi 'certificate'; then + emit_finding "DC-13" "BLOCK" "$user_guide" 1 \ + "Module uses Certificates — must document Certificate prerequisite" "" >> "$findings" + fi + fi + # DC-14: regional + if [ "$has_region" = "yes" ]; then + if ! echo "$guide_content" | grep -qEi 'Regional Availability|## Limitations|available in|supported region'; then + emit_finding "DC-14" "BLOCK" "$user_guide" 1 \ + "Module has region-specific constants — must document ## Regional Availability" "" >> "$findings" + fi + fi + +done <<< "$new_modules" + +status=$(status_from_findings < "$findings") +emit_report "docs" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-errors-logging.sh b/.claude/scripts/check-errors-logging.sh new file mode 100755 index 00000000..2b8334c1 --- /dev/null +++ b/.claude/scripts/check-errors-logging.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# check-errors-logging.sh — exception chaining, log level, sensitive-info in messages. +# Uses AST for chaining/swallow checks. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" +source "$SCRIPT_DIR/lib/skill-self-skip.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +if [ "$LANGUAGE" = "python" ]; then + changed=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/.*\.py' | sed 's|^+++ b/||' | sort -u) + if [ -n "$changed" ]; then + # AST-based chaining / swallow — only reports FLAGs when body ends non-Raise + # shellcheck disable=SC2086 + python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-01 $changed 2>/dev/null >> "$findings" || true + # shellcheck disable=SC2086 + python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-02 $changed 2>/dev/null >> "$findings" || true + fi + + # EL-04: secret-like variable name in raise args (grep-based, added lines only) + echo "$diff_content" | awk ' + BEGIN { file=""; line=0 } + /^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next } + /^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next } + /^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next } + /^ / { line++; next } + ' | while IFS=$'\t' read -r file line_num content; do + [ -z "$file" ] && continue + if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi + # match raise ...({...token|secret|password|api_key|client_secret...}) + if echo "$content" | grep -qE 'raise[[:space:]].*[fF]?"[^"]*\{[^}]*(token|secret|password|api_key|client_secret)[^}]*\}'; then + emit_finding "EL-04" "BLOCK" "$file" "$line_num" \ + "Exception message includes secret-like variable — leaks sensitive data" "" >> "$findings" + fi + done +fi + +status=$(status_from_findings < "$findings") +emit_report "errors-logging" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-hardcode.sh b/.claude/scripts/check-hardcode.sh new file mode 100755 index 00000000..fde98f7d --- /dev/null +++ b/.claude/scripts/check-hardcode.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# check-hardcode.sh — no hardcoded URLs, credentials, or magic values in impl code. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/json-emit.sh +source "$SCRIPT_DIR/lib/json-emit.sh" +# shellcheck source=lib/skill-self-skip.sh +source "$SCRIPT_DIR/lib/skill-self-skip.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" + +STARTED=$(now_iso) +findings=$(mktemp) +trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# Determine ignore patterns per language +if [ "$LANGUAGE" = "python" ]; then + ignore_files='^(tests?/|mocks?/|docs?/|.*/spec/|.*/constants\.py|.*/user-guide\.md|README\.md)' +else + ignore_files='^(src/test/|mocks?/|docs?/|.*Constants\.java|.*/constants/|.*/user-guide\.md|README\.md)' +fi + +echo "$diff_content" | awk ' + BEGIN { file=""; line=0 } + /^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next } + /^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next } + /^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next } + /^ / { line++; next } +' | while IFS=$'\t' read -r file line_num content; do + [ -z "$file" ] && continue + if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi + # Filter out test/mock/docs/constants files + if echo "$file" | grep -qE "$ignore_files"; then continue; fi + + # HC-01: hardcoded URL + if echo "$content" | grep -qE 'https?://[a-zA-Z0-9]'; then + # exempt localhost / example.com / example.org (test-safe) + if ! echo "$content" | grep -qE 'https?://(localhost|127\.0\.0\.1|example\.(com|org|net)|reserved\.)'; then + emit_finding "HC-01" "BLOCK" "$file" "$line_num" "Hardcoded URL in implementation — externalize to config" "" >> "$findings" + fi + fi + # HC-02: Authorization Bearer + if echo "$content" | grep -qE 'Authorization[[:space:]]*:[[:space:]]*Bearer[[:space:]]+[A-Za-z0-9]'; then + emit_finding "HC-02" "BLOCK" "$file" "$line_num" "Hardcoded Authorization header value" "" >> "$findings" + fi + # HC-04: direct os.environ / System.getenv + if [ "$LANGUAGE" = "python" ]; then + if echo "$content" | grep -qE 'os\.environ\["[A-Z]'; then + emit_finding "HC-04" "FLAG" "$file" "$line_num" "Direct os.environ access — prefer secret_resolver / config layer" "" >> "$findings" + fi + else + if echo "$content" | grep -qE 'System\.getenv\('; then + emit_finding "HC-04" "FLAG" "$file" "$line_num" "Direct System.getenv() — prefer SecretResolver" "" >> "$findings" + fi + fi + # HC-06: hardcoded timeout numeric literal + if echo "$content" | grep -qiE '(timeout|retries|max_retries)[[:space:]]*=[[:space:]]*[0-9]+'; then + emit_finding "HC-06" "FLAG" "$file" "$line_num" "Hardcoded timeout/retry number — externalize to config" "" >> "$findings" + fi +done + +status=$(status_from_findings < "$findings") +emit_report "hardcode" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-http-hygiene.sh b/.claude/scripts/check-http-hygiene.sh new file mode 100755 index 00000000..d565cb30 --- /dev/null +++ b/.claude/scripts/check-http-hygiene.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# check-http-hygiene.sh — placeholder: stub check emitting empty findings. +# TODO: implement rules from 01-PYTHON.md / 02-JAVA.md §3.http-hygiene +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" +source "$SCRIPT_DIR/lib/skill-self-skip.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# HTTP-PY-01: Session per invocation (only fire on newly added lines) +echo "$diff_content" | awk ' + BEGIN { file=""; line=0 } + /^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next } + /^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next } + /^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next } + /^ / { line++; next } +' | while IFS=$'\t' read -r file line_num content; do + [ -z "$file" ] && continue + if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi + # only for impl files (not tests/mocks) + if echo "$file" | grep -qE '^(tests?/|mocks?/)'; then continue; fi + if echo "$content" | grep -qE '(requests|httpx)\.(Session|AsyncClient)\(\)'; then + emit_finding "HTTP-01" "FLAG" "$file" "$line_num" \ + "HTTP session created per invocation — prefer single instance in __init__" "" >> "$findings" + fi +done + +status=$(status_from_findings < "$findings") +emit_report "http-hygiene" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-license-spdx.sh b/.claude/scripts/check-license-spdx.sh new file mode 100755 index 00000000..fabb269d --- /dev/null +++ b/.claude/scripts/check-license-spdx.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# check-license-spdx.sh — verify new source files have SPDX headers. +# REUSE.toml aggregate → rule PASSES (baseline exemption). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/json-emit.sh +source "$SCRIPT_DIR/lib/json-emit.sh" +# shellcheck source=lib/predicates.sh +source "$SCRIPT_DIR/lib/predicates.sh" + +LANGUAGE="${LANGUAGE:-python}" +REPO_ROOT="${REPO_ROOT:-.}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" + +STARTED=$(now_iso) +findings=$(mktemp) +trap 'rm -f "$findings"' EXIT + +# Predicate: REUSE.toml aggregate present? +reuse_present=$(reuse_toml_aggregate_present "$REPO_ROOT") + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +if [ "$reuse_present" = "true" ]; then + # Baseline exemption: rule OFF for this repo + jq -n --arg check "license-spdx" --arg lang "$LANGUAGE" --arg started "$STARTED" \ + '{ + check: $check, version: "1.0.0", language: $lang, status: "PASS", + started_at: $started, duration_ms: 0, modules_analysed: [], + findings: [], shadow_findings: [], + summary: {block_count: 0, flag_count: 0, suppressed_by_baseline: 0, + note: "REUSE.toml aggregate present — LIC-01/02 exempted"} + }' + exit 0 +fi + +# Find newly added files (starts with "diff --git a/... b/..." followed by "new file mode") +# We match on "+++ b/" lines that appear right after "new file mode" +added_files=$(echo "$diff_content" | awk ' + /^diff --git/ { in_block=1; is_new=0; path=""; next } + in_block && /^new file mode/ { is_new=1; next } + in_block && /^\+\+\+ b\// { if (is_new) print substr($0, 7); in_block=0 } +') + +# REUSE-IgnoreStart +if [ "$LANGUAGE" = "python" ]; then + ext_match='\.py$' + spdx_line='# SPDX-License-Identifier: Apache-2.0' + cprt_line='# SPDX-FileCopyrightText:' +else + ext_match='\.java$' + spdx_line='// SPDX-License-Identifier: Apache-2.0' + cprt_line='// SPDX-FileCopyrightText:' +fi +# REUSE-IgnoreEnd + +while IFS= read -r f; do + [ -z "$f" ] && continue + if ! echo "$f" | grep -qE "$ext_match"; then continue; fi + # Read first 10 lines from the diff for that file to check headers + header=$(echo "$diff_content" | awk -v file="$f" ' + $0 == "+++ b/" file { flag=1; count=0; next } + flag && /^\+/ && !/^\+\+\+/ { print substr($0, 2); count++; if (count>=10) exit } + flag && /^diff --git/ { exit } + ') + if ! echo "$header" | grep -qF "$spdx_line"; then + emit_finding "LIC-01" "BLOCK" "$f" 1 "New source file missing SPDX-License-Identifier header" "" >> "$findings" + fi + if ! echo "$header" | grep -qF "$cprt_line"; then + emit_finding "LIC-02" "BLOCK" "$f" 1 "New source file missing SPDX-FileCopyrightText header" "" >> "$findings" + fi +done <<< "$added_files" + +status=$(status_from_findings < "$findings") +emit_report "license-spdx" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-patterns.sh b/.claude/scripts/check-patterns.sh new file mode 100755 index 00000000..c3cc7b83 --- /dev/null +++ b/.claude/scripts/check-patterns.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# check-patterns.sh — idiomatic patterns (factory, exceptions, py.typed). +# Uses module_shape predicate to skip non-client modules. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" +source "$SCRIPT_DIR/lib/predicates.sh" + +LANGUAGE="${LANGUAGE:-python}" +REPO_ROOT="${REPO_ROOT:-.}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# Detect module dirs touched +if [ "$LANGUAGE" = "python" ]; then + modules=$(echo "$diff_content" | grep -oE 'src/sap_cloud_sdk/[a-z_]+/' | sed 's|src/sap_cloud_sdk/||; s|/$||' | grep -v '^core$' | sort -u) +else + modules=$(echo "$diff_content" | grep -oE 'src/main/java/com/sap/cloud/sdk/[a-z_]+/' | sed 's|src/main/java/com/sap/cloud/sdk/||; s|/$||' | grep -v '^core$' | sort -u) +fi + +while IFS= read -r mod; do + [ -z "$mod" ] && continue + + if [ "$LANGUAGE" = "python" ]; then + mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod" + else + mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod" + fi + + [ -d "$mod_dir" ] || continue + + # PY-PT-01 / JV-PT-01: factory exists — only fire on client modules + shape=$(module_shape "$mod_dir") + if [ "$shape" = "client" ]; then + if [ "$LANGUAGE" = "python" ]; then + if ! grep -rqE '^def create_[a-z_]*client\(' "$mod_dir" 2>/dev/null; then + emit_finding "PY-PT-01" "BLOCK" "src/sap_cloud_sdk/$mod/" 1 \ + "Client module '$mod' missing create_client() factory function" "" >> "$findings" + fi + else + if ! grep -rqE 'class [A-Z][A-Za-z]*ClientFactory' "$mod_dir" 2>/dev/null; then + emit_finding "JV-PT-01" "BLOCK" "src/main/java/com/sap/cloud/sdk/$mod/" 1 \ + "Client module '$mod' missing ClientFactory class" "" >> "$findings" + fi + fi + fi + + # PY-PT-03: py.typed marker + if [ "$LANGUAGE" = "python" ]; then + if [ ! -f "$mod_dir/py.typed" ] && [ ! -f "$REPO_ROOT/src/sap_cloud_sdk/py.typed" ]; then + emit_finding "PY-PT-03" "FLAG" "src/sap_cloud_sdk/$mod/py.typed" 1 \ + "Module missing py.typed marker (PEP 561)" "" >> "$findings" + fi + fi + + # PY-PT-04 / JV-PT-05: module-specific exceptions + if [ "$LANGUAGE" = "python" ]; then + if [ ! -f "$mod_dir/exceptions.py" ]; then + emit_finding "PY-PT-04" "FLAG" "src/sap_cloud_sdk/$mod/exceptions.py" 1 \ + "Module lacks exceptions.py — module-specific exception hierarchy recommended" "" >> "$findings" + fi + else + if [ ! -d "$mod_dir/exceptions" ]; then + emit_finding "JV-PT-05" "FLAG" "src/main/java/com/sap/cloud/sdk/$mod/exceptions/" 1 \ + "Module lacks exceptions/ package" "" >> "$findings" + fi + fi + +done <<< "$modules" + +# PY-PT-08 via AST on changed Python files +if [ "$LANGUAGE" = "python" ]; then + changed_py=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/sap_cloud_sdk/.*\.py' | sed 's|^+++ b/||' | grep -v '__pycache__' | sort -u) + if [ -n "$changed_py" ]; then + # shellcheck disable=SC2086 + python3 "$SCRIPT_DIR/lib/ast_python_checks.py" pt-08 $changed_py 2>/dev/null >> "$findings" || true + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "patterns" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-pr-size.sh b/.claude/scripts/check-pr-size.sh new file mode 100755 index 00000000..4dcdac65 --- /dev/null +++ b/.claude/scripts/check-pr-size.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# check-pr-size.sh — advisory on large PRs (all FLAG tier, initially SHADOW). +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +BASE_SHA="${BASE_SHA:-HEAD~10}" +HEAD_SHA="${HEAD_SHA:-HEAD}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# PR-SIZE-01: additions > 800 +additions=$(echo "$diff_content" | grep -cE '^\+[^+]' || echo 0) +if [ "$additions" -gt 800 ]; then + emit_finding "PR-SIZE-01" "FLAG" "." 1 \ + "PR has $additions additions (>800) — consider splitting into stacked PRs for easier review" \ + "See docs/CONTRIBUTING.md § Incremental Delivery for stacked-PR workflow" >> "$findings" +fi + +# PR-SIZE-02: > 15 files +files_touched=$(echo "$diff_content" | grep -cE '^diff --git' || echo 0) +if [ "$files_touched" -gt 15 ]; then + emit_finding "PR-SIZE-02" "FLAG" "." 1 \ + "PR touches $files_touched files (>15) — consider splitting by concern" "" >> "$findings" +fi + +# PR-SIZE-03: > 3 modules +if [ "$LANGUAGE" = "python" ]; then + mods_count=$(echo "$diff_content" | grep -oE 'src/sap_cloud_sdk/[a-z_]+/' | sed 's|src/sap_cloud_sdk/||; s|/$||' | sort -u | wc -l | tr -d ' ') +else + mods_count=$(echo "$diff_content" | grep -oE 'src/main/java/com/sap/cloud/sdk/[a-z_]+/' | sed 's|src/main/java/com/sap/cloud/sdk/||; s|/$||' | sort -u | wc -l | tr -d ' ') +fi +if [ "$mods_count" -gt 3 ]; then + emit_finding "PR-SIZE-03" "FLAG" "." 1 \ + "PR modifies $mods_count modules (>3) — consider one PR per module" "" >> "$findings" +fi + +# PR-SIZE-05: > 30 commits +commit_count=$(git log "${BASE_SHA}..${HEAD_SHA}" --oneline 2>/dev/null | grep -v -c '^Merge' || echo 0) +if [ "$commit_count" -gt 30 ]; then + emit_finding "PR-SIZE-05" "FLAG" "." 1 \ + "PR has $commit_count commits (>30) — consider squashing or splitting" "" >> "$findings" +fi + +status=$(status_from_findings < "$findings") +emit_report "pr-size" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-quality-gate-parity.sh b/.claude/scripts/check-quality-gate-parity.sh new file mode 100755 index 00000000..05012231 --- /dev/null +++ b/.claude/scripts/check-quality-gate-parity.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# check-quality-gate-parity.sh — placeholder: stub check emitting empty findings. +# TODO: implement rules from 01-PYTHON.md / 02-JAVA.md §3.quality-gate-parity +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +REPO_ROOT="${REPO_ROOT:-.}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# QG-01: workflow with `ruff check` but no `ruff format --check` or `ty check` +if echo "$diff_content" | grep -qE '^\+\+\+ b/\.github/workflows/'; then + wf_content=$(echo "$diff_content" | awk '/^\+\+\+ b\/\.github\/workflows\// { flag=1; next } /^diff --git/ { flag=0 } flag && /^\+/ && !/^\+\+\+/ { print }') + if echo "$wf_content" | grep -q 'ruff check'; then + if ! echo "$wf_content" | grep -q 'ruff format'; then + emit_finding "QG-01" "FLAG" ".github/workflows/" 1 \ + "Workflow runs 'ruff check' but not 'ruff format --check' — mismatch with dev gate" "" >> "$findings" + fi + if ! echo "$wf_content" | grep -q 'ty check'; then + emit_finding "QG-02" "FLAG" ".github/workflows/" 1 \ + "Workflow runs 'ruff check' but not 'ty check' — incomplete type gate" "" >> "$findings" + fi + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "quality-gate-parity" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-secrets.sh b/.claude/scripts/check-secrets.sh new file mode 100755 index 00000000..8564b026 --- /dev/null +++ b/.claude/scripts/check-secrets.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# check-secrets.sh — detect secrets (AWS keys, JWTs, GitHub tokens, private keys, etc.) in added lines. +# All SEC-* rules are BLOCK_LOCKED (cannot be suppressed). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/json-emit.sh +source "$SCRIPT_DIR/lib/json-emit.sh" +# shellcheck source=lib/skill-self-skip.sh +source "$SCRIPT_DIR/lib/skill-self-skip.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" + +STARTED=$(now_iso) + +# Read diff (either from env-provided file or stdin) — extract added lines with file+line info +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +if [ -z "$diff_content" ]; then + emit_report "secrets" "$LANGUAGE" "PASS" "$STARTED" <<< "" + exit 0 +fi + +findings=$(mktemp) +trap 'rm -f "$findings"' EXIT + +# Parse diff and scan each added line +echo "$diff_content" | awk ' + BEGIN { file=""; line=0 } + /^diff --git a\// { + file=$4 + sub(/^b\//, "", file) + line=0 + next + } + /^@@/ { + if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0 + next + } + /^\+/ && !/^\+\+\+/ { + print file "\t" line "\t" substr($0, 2) + line++ + next + } + /^ / { line++; next } +' | while IFS=$'\t' read -r file line_num content; do + [ -z "$file" ] && continue + # Self-review protection: skip skill files + if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi + + # SEC-01: AWS Access Key + if echo "$content" | grep -qE 'AKIA[0-9A-Z]{16}'; then + emit_finding "SEC-01" "BLOCK" "$file" "$line_num" "AWS Access Key detected — remove immediately and rotate the key" "" >> "$findings" + fi + # SEC-02: Google API Key + if echo "$content" | grep -qE 'AIza[0-9A-Za-z_-]{35}'; then + emit_finding "SEC-02" "BLOCK" "$file" "$line_num" "Google API Key detected — remove and rotate" "" >> "$findings" + fi + # SEC-03: GitHub PAT + if echo "$content" | grep -qE 'gh[pousr]_[A-Za-z0-9_]{36,}'; then + emit_finding "SEC-03" "BLOCK" "$file" "$line_num" "GitHub PAT detected — remove and rotate" "" >> "$findings" + fi + # SEC-04: OpenAI key + if echo "$content" | grep -qE 'sk-[A-Za-z0-9]{20,}'; then + emit_finding "SEC-04" "BLOCK" "$file" "$line_num" "OpenAI-like API key detected — remove and rotate" "" >> "$findings" + fi + # SEC-05: Slack bot token + if echo "$content" | grep -qE 'xox[baprs]-[A-Za-z0-9-]+'; then + emit_finding "SEC-05" "BLOCK" "$file" "$line_num" "Slack token detected — remove and rotate" "" >> "$findings" + fi + # SEC-06: JWT + if echo "$content" | grep -qE 'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'; then + emit_finding "SEC-06" "BLOCK" "$file" "$line_num" "JWT token detected — remove and rotate" "" >> "$findings" + fi + # SEC-07: Private key header + if echo "$content" | grep -qE 'BEGIN (RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY'; then + emit_finding "SEC-07" "BLOCK" "$file" "$line_num" "Private key header detected — remove and rotate" "" >> "$findings" + fi + # SEC-08: BTP client_secret literal (heuristic: assignment with looks-like-secret value) + # Match client_secret= or clientsecret= with quoted values that look like real secrets + if echo "$content" | grep -qE '"clientsecret"\s*:\s*"[A-Za-z0-9+/=]{20,}"'; then + emit_finding "SEC-08" "BLOCK" "$file" "$line_num" "BTP client_secret literal detected — use secret resolver" "" >> "$findings" + fi +done + +# SEC-10: .env files in diff (not .env.example, .env.test, .env.sample) +env_lines=$(echo "$diff_content" | grep -E '^\+\+\+ b/\.env(\..+)?$' || true) +while IFS= read -r line; do + [ -z "$line" ] && continue + # extract path from "+++ b/" + path="${line#+++ b/}" + # skip allowed suffixes + case "$path" in + .env.example|.env.test|.env.sample|.env.template) continue ;; + .env|.env.*) emit_finding "SEC-10" "BLOCK" "$path" 1 ".env file committed — never commit .env; use .env.example" "" >> "$findings" ;; + esac +done <<< "$env_lines" + +status=$(status_from_findings < "$findings") +emit_report "secrets" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-telemetry.sh b/.claude/scripts/check-telemetry.sh new file mode 100755 index 00000000..5d48fa43 --- /dev/null +++ b/.claude/scripts/check-telemetry.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# check-telemetry.sh — verify telemetry instrumentation. +# Python: @record_metrics on public *Client methods + emission tests. +# Java: Telemetry.executeWithTelemetry(...) wrapping + tests. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/json-emit.sh +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +REPO_ROOT="${REPO_ROOT:-.}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" + +STARTED=$(now_iso) +findings=$(mktemp) +trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# Get list of client files newly added or modified +if [ "$LANGUAGE" = "python" ]; then + client_files=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/sap_cloud_sdk/[a-z_]+/.*Client\.py|^\+\+\+ b/src/sap_cloud_sdk/[a-z_]+/client\.py' | sed 's|^+++ b/||' | sort -u) +else + client_files=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/main/java/com/sap/cloud/sdk/[a-z_]+/.*Client\.java' | sed 's|^+++ b/||' | sort -u) +fi + +# Detect new decorator additions (added lines with @record_metrics for Python, executeWithTelemetry for Java) +if [ "$LANGUAGE" = "python" ]; then + new_decorators=$(echo "$diff_content" | grep -E '^\+[[:space:]]*@record_metrics' | wc -l | tr -d ' ') +else + new_decorators=$(echo "$diff_content" | grep -E '^\+.*Telemetry\.executeWithTelemetry' | wc -l | tr -d ' ') +fi + +# PY-TEL-02: For each changed client file, run AST check +if [ "$LANGUAGE" = "python" ] && [ -n "$client_files" ]; then + # shellcheck disable=SC2086 + python3 "$SCRIPT_DIR/lib/ast_python_checks.py" tel-02 $client_files 2>/dev/null >> "$findings" || true +fi + +# JV-TEL-02: For Java, grep-based check (executeWithTelemetry wrap around methods) +if [ "$LANGUAGE" = "java" ] && [ -n "$client_files" ]; then + while IFS= read -r f; do + [ -z "$f" ] && continue + full_path="$REPO_ROOT/$f" + [ -f "$full_path" ] || continue + # find public methods in the file + while IFS= read -r match; do + line_num="${match%%:*}" + # check the following 20 lines for executeWithTelemetry + end_line=$((line_num + 20)) + body=$(sed -n "${line_num},${end_line}p" "$full_path") + if ! echo "$body" | grep -q "executeWithTelemetry"; then + emit_finding "JV-TEL-02" "BLOCK" "$f" "$line_num" \ + "Public method lacks Telemetry.executeWithTelemetry wrap" "" >> "$findings" + fi + done < <(grep -nE '^[[:space:]]*public [A-Za-z<>]+ [a-z][a-zA-Z0-9]+\(' "$full_path" 2>/dev/null | grep -v 'public class\|public interface\|public enum' || true) + done <<< "$client_files" +fi + +# PY-TEL-06 / JV-TEL-05: emission tests required when new decorator/wrapper added +if [ "$new_decorators" -gt 0 ]; then + if [ "$LANGUAGE" = "python" ]; then + # Find test files added/modified in same PR + test_files=$(echo "$diff_content" | grep -oE '^\+\+\+ b/tests/[a-z_]+/.*/test_.*\.py' | sed 's|^+++ b/||' | sort -u) + has_metric_assert=false + while IFS= read -r tf; do + [ -z "$tf" ] && continue + [ -f "$REPO_ROOT/$tf" ] || continue + if grep -q 'record_request_metric\|record_error_metric' "$REPO_ROOT/$tf"; then + has_metric_assert=true; break + fi + done <<< "$test_files" + if [ "$has_metric_assert" = "false" ]; then + emit_finding "PY-TEL-06" "BLOCK" "tests/" 1 \ + "New @record_metrics added ($new_decorators occurrences) but no test asserts record_request_metric was called" \ + "Add a test that mocks record_request_metric and asserts it was called with the expected Module/Operation" >> "$findings" + fi + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "telemetry" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-testing-depth.sh b/.claude/scripts/check-testing-depth.sh new file mode 100755 index 00000000..83dba8d6 --- /dev/null +++ b/.claude/scripts/check-testing-depth.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# check-testing-depth.sh — test names, tests-added checkbox truthfulness, integration test present. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" + +LANGUAGE="${LANGUAGE:-python}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +PR_BODY_FILE="${PR_BODY_FILE:-}" + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# TD-01: If PR title is fix: AND src/ changed AND no test file changed → FLAG +if [ "$LANGUAGE" = "python" ]; then + fix_commit=$(git log HEAD --format=%s -n 1 2>/dev/null | grep -qE '^fix' && echo yes || echo no) + src_changed=$(echo "$diff_content" | grep -qE 'src/sap_cloud_sdk/' && echo yes || echo no) + test_changed=$(echo "$diff_content" | grep -qE 'tests?/.*/test_' && echo yes || echo no) + if [ "$fix_commit" = "yes" ] && [ "$src_changed" = "yes" ] && [ "$test_changed" = "no" ]; then + emit_finding "TD-01" "FLAG" "tests/" 1 \ + "Bug-fix PR touches src/ but no test files changed" \ + "Add a focused unit test that reproduces the bug and asserts the fix" >> "$findings" + fi + + # TD-10: New module → integration test required + new_modules=$(echo "$diff_content" | awk '/^diff --git/ { flag=0 } /^new file mode/ { flag=1 } flag && /^\+\+\+ b\/src\/sap_cloud_sdk\/[a-z_]+\/[^/]+\.py/ { print }' | grep -oE 'src/sap_cloud_sdk/[a-z_]+/' | sed 's|src/sap_cloud_sdk/||; s|/$||' | sort -u) + while IFS= read -r mod; do + [ -z "$mod" ] || [ "$mod" = "core" ] && continue + has_integration=$(echo "$diff_content" | grep -qE "tests/$mod/integration/" && echo yes || echo no) + if [ "$has_integration" = "no" ]; then + emit_finding "TD-10" "BLOCK" "tests/$mod/integration/" 1 \ + "New module '$mod' has no integration test" "" >> "$findings" + fi + done <<< "$new_modules" +fi + +# TD-checkbox: PR body says "tests added" but no test files touched +if [ -n "$PR_BODY_FILE" ] && [ -f "$PR_BODY_FILE" ]; then + body=$(cat "$PR_BODY_FILE") + if echo "$body" | grep -qE '\-[[:space:]]*\[[xX]\][[:space:]].*(added|updated).*tests'; then + if ! echo "$diff_content" | grep -qE 'diff --git a/(tests?/|src/test/)'; then + emit_finding "TD-checkbox" "FLAG" "PR_BODY" 1 \ + "PR body ticks 'added tests' checkbox but no test files changed" "" >> "$findings" + fi + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "testing-depth" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/check-versioning.sh b/.claude/scripts/check-versioning.sh new file mode 100755 index 00000000..8828834d --- /dev/null +++ b/.claude/scripts/check-versioning.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# check-versioning.sh — SemVer bump + BREAKING family (BREAKING-01..04). +set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/json-emit.sh" +source "$SCRIPT_DIR/lib/predicates.sh" + +LANGUAGE="${LANGUAGE:-python}" +REPO_ROOT="${REPO_ROOT:-.}" +DIFF_FILE="${DIFF_FILE:-/dev/stdin}" +PR_BODY_FILE="${PR_BODY_FILE:-}" +BASE_SHA="${BASE_SHA:-}" +HEAD_SHA="${HEAD_SHA:-HEAD}" +BREAKING_JSON="${BREAKING_JSON:-}" # pre-computed via breaking-detector.py + +STARTED=$(now_iso) +findings=$(mktemp); trap 'rm -f "$findings"' EXIT + +diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "") + +# Detect version-bump line change +if [ "$LANGUAGE" = "python" ]; then + version_bumped=$(echo "$diff_content" | grep -E '^\+version\s*=' | head -1) + version_removed=$(echo "$diff_content" | grep -E '^-version\s*=' | head -1) +else + version_bumped=$(echo "$diff_content" | grep -E '^\+\s*' | head -1) + version_removed=$(echo "$diff_content" | grep -E '^-\s*' | head -1) +fi + +# src/ changes present? +if [ "$LANGUAGE" = "python" ]; then + src_changed=$(echo "$diff_content" | grep -qE 'diff --git a/src/sap_cloud_sdk/' && echo yes || echo no) +else + src_changed=$(echo "$diff_content" | grep -qE 'diff --git a/src/main/java/' && echo yes || echo no) +fi + +# commit types (may not find any → allow non-zero exit under set -e) +commit_types=$(has_commit_type "${BASE_SHA:-HEAD~10}" "$HEAD_SHA" "feat,feat!,fix,fix!" 2>/dev/null || echo "false") + +# BREAKING detector output +breaking_detected="false" +if [ -n "$BREAKING_JSON" ] && [ -f "$BREAKING_JSON" ]; then + breaking_detected=$(jq -r '.breaking_detected' "$BREAKING_JSON" 2>/dev/null || echo "false") +fi + +is_feat=$(has_commit_type "${BASE_SHA:-HEAD~10}" "$HEAD_SHA" "feat,feat!" 2>/dev/null || echo "false") + +# VER-01: src/ change without bump — only fires on feat OR breaking +if [ "$src_changed" = "yes" ] && [ -z "$version_bumped" ]; then + if [ "$is_feat" = "true" ] || [ "$breaking_detected" = "true" ]; then + emit_finding "VER-01" "BLOCK" "pyproject.toml" 1 \ + "Feature or breaking change detected but version not bumped" \ + "Bump MINOR version for feat/API change; MAJOR for breaking" >> "$findings" + fi +fi + +# BREAKING-01: if breaking, check PR body has proper declarations +if [ "$breaking_detected" = "true" ]; then + # collect requirements + has_bang=$(has_commit_type "${BASE_SHA:-HEAD~10}" "$HEAD_SHA" "feat!,fix!" || echo "false") + has_bump=$([ -n "$version_bumped" ] && echo "true" || echo "false") + + pr_body="" + if [ -n "$PR_BODY_FILE" ] && [ -f "$PR_BODY_FILE" ]; then + pr_body=$(cat "$PR_BODY_FILE") + fi + has_breaking_section="false" + if echo "$pr_body" | grep -qE '^##[[:space:]]+Breaking Changes' ; then + # section must have non-empty content (not "N/A" or "None" alone) + # Use awk to extract from "## Breaking Changes" to next "## " heading, drop the header line itself + section=$(echo "$pr_body" | awk ' + /^##[[:space:]]+Breaking Changes/ { flag=1; next } + flag && /^##[[:space:]]+[A-Z]/ { exit } + flag { print } + ' | tr -d '[:space:]') + if [ -n "$section" ] && ! echo "$section" | grep -qEi '^(N/A|None|none|--)*$'; then + has_breaking_section="true" + fi + fi + has_checkbox=$(echo "$pr_body" | grep -qE '\-[[:space:]]*\[[xX]\][[:space:]]+Breaking change' && echo "true" || echo "false") + + # if ANY of the 4 requirements is missing → BLOCK + missing="" + [ "$has_bang" != "true" ] && missing="$missing commit-!:-prefix" + [ "$has_bump" != "true" ] && missing="$missing version-bump" + [ "$has_breaking_section" != "true" ] && missing="$missing PR-body-Breaking-Changes-section" + [ "$has_checkbox" != "true" ] && missing="$missing PR-body-checkbox" + + if [ -n "$missing" ]; then + emit_finding "BREAKING-01" "BLOCK" "PR_METADATA" 1 \ + "Breaking change detected — missing declarations:$missing" \ + "Add all 4: (a) feat!:/fix!: commit prefix (b) ## Breaking Changes section (c) checkbox ticked (d) version bump" >> "$findings" + + # BREAKING-02: partial declaration (some declared, others not) + declared_count=0 + [ "$has_bang" = "true" ] && declared_count=$((declared_count+1)) + [ "$has_bump" = "true" ] && declared_count=$((declared_count+1)) + [ "$has_breaking_section" = "true" ] && declared_count=$((declared_count+1)) + [ "$has_checkbox" = "true" ] && declared_count=$((declared_count+1)) + if [ "$declared_count" -gt 0 ] && [ "$declared_count" -lt 4 ]; then + emit_finding "BREAKING-02" "BLOCK" "PR_METADATA" 1 \ + "Breaking-change metadata is inconsistent ($declared_count/4 declared)" \ + "All 4 declarations must agree — reconcile or fully retract" >> "$findings" + fi + fi +fi + +status=$(status_from_findings < "$findings") +emit_report "versioning" "$LANGUAGE" "$status" "$STARTED" < "$findings" diff --git a/.claude/scripts/lib/apply-suppression.sh b/.claude/scripts/lib/apply-suppression.sh new file mode 100755 index 00000000..79b39c28 --- /dev/null +++ b/.claude/scripts/lib/apply-suppression.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# lib/apply-suppression.sh — filter findings against suppression comments in source. +# Called after a check produces its JSON report; reads suppression tuples and drops findings. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# collect_suppressions_for_files ... +# Emits "::" tuples for all suppression comments found. +collect_suppressions_for_files() { + for f in "$@"; do + [ -f "$f" ] || continue + bash "$SCRIPT_DIR/suppression.sh" parse_line "$f" 2>/dev/null || true + bash "$SCRIPT_DIR/suppression.sh" parse_file "$f" 2>/dev/null || true + done +} + +# apply_to_report → prints filtered report +apply_to_report() { + local report="$1" supp_file="$2" + local kept_count=0 suppressed_count=0 + local input; input=$(cat "$report") + local n; n=$(echo "$input" | jq '.findings | length') + if [ "$n" -eq 0 ]; then + echo "$input"; return + fi + + local kept=() + local i=0 + while [ "$i" -lt "$n" ]; do + local finding rule file line + finding=$(echo "$input" | jq -c ".findings[$i]") + rule=$(echo "$finding" | jq -r '.rule') + file=$(echo "$finding" | jq -r '.file') + line=$(echo "$finding" | jq -r '.line') + + local suppressed + suppressed=$(bash "$SCRIPT_DIR/suppression.sh" is_suppressed "$rule" "$file" "$line" "$supp_file" 2>/dev/null || echo "false") + + if [ "$suppressed" = "true" ]; then + suppressed_count=$((suppressed_count + 1)) + else + kept+=("$finding") + kept_count=$((kept_count + 1)) + fi + i=$((i + 1)) + done + + local kept_json + if [ ${#kept[@]} -eq 0 ]; then + kept_json="[]" + else + kept_json=$(printf '%s\n' "${kept[@]}" | jq -s -c '.') + fi + + echo "$input" | jq \ + --argjson kept "$kept_json" \ + --argjson suppressed "$suppressed_count" \ + '.findings = $kept | .summary.suppressed_count = $suppressed' +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + cmd="${1:-}"; shift || true + case "$cmd" in + collect) collect_suppressions_for_files "$@" ;; + apply) apply_to_report "$@" ;; + *) echo "Usage: apply-suppression.sh {collect|apply} args" >&2; exit 2 ;; + esac +fi diff --git a/.claude/scripts/lib/ast_python_checks.py b/.claude/scripts/lib/ast_python_checks.py new file mode 100755 index 00000000..d0039f59 --- /dev/null +++ b/.claude/scripts/lib/ast_python_checks.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +"""AST-based checks for Python source files. + +Called from bash check-*.sh scripts. Reads source files, emits JSONL findings +to stdout (one JSON object per line). + +Usage: + python3 ast-python-checks.py [ ...] + +Where is one of: + el-01, el-02 exception chaining / swallow + tel-02, tel-06 telemetry decorators / tests + pt-01, pt-08, pt-11 patterns + con-01 repeated string literals +""" + +from __future__ import annotations + +import ast +import json +import sys +from pathlib import Path + + +def emit( + rule: str, severity: str, file: str, line: int, message: str, suggestion: str = "" +) -> None: + print( + json.dumps( + { + "rule": rule, + "severity": severity, + "file": file, + "line": line, + "message": message, + "suggestion": suggestion, + } + ) + ) + + +def parse_file(path: str) -> ast.Module | None: + try: + source = Path(path).read_text() + return ast.parse(source, filename=path) + except (SyntaxError, UnicodeDecodeError, FileNotFoundError): + return None + + +# ---------- PY-EL-01: raise X from e when raising DIFFERENT exception ---------- + + +def check_el_01(path: str, tree: ast.Module) -> None: + for node in ast.walk(tree): + if not isinstance(node, ast.ExceptHandler): + continue + handler_name = node.name # the caught exception variable, may be None + for inner in ast.walk(node): + if not isinstance(inner, ast.Raise): + continue + # bare `raise` — PASS (preserves cause) + if inner.exc is None: + continue + # `raise e` where e is the handler var — PASS (same exception) + if ( + isinstance(inner.exc, ast.Name) + and handler_name + and inner.exc.id == handler_name + ): + continue + # raising a *call* to the same var, e.g. `raise e()` (rare) — PASS + if ( + isinstance(inner.exc, ast.Call) + and isinstance(inner.exc.func, ast.Name) + and handler_name + and inner.exc.func.id == handler_name + ): + continue + # different exception without `from e` → FLAG + if inner.cause is None: + emit( + "PY-EL-01", + "FLAG", + path, + inner.lineno, + "Raising different exception inside except — use `raise X from e` to preserve cause", + ) + + +# ---------- PY-EL-02: except body must end with raise or explicit suppression ---------- + + +def check_el_02(path: str, tree: ast.Module) -> None: + for node in ast.walk(tree): + if not isinstance(node, ast.ExceptHandler): + continue + if not node.body: + continue + last = node.body[-1] + # last statement is Raise → PASS + if isinstance(last, ast.Raise): + continue + # last statement is Return of a variable named e / err / exc → likely intentional propagation → PASS + if ( + isinstance(last, ast.Return) + and isinstance(last.value, ast.Name) + and last.value.id in {"e", "err", "exc"} + ): + continue + # explicit `pass` and only `pass` in body → obvious swallow → FLAG + emit( + "PY-EL-02", + "FLAG", + path, + node.lineno, + "except block does not end with `raise` — swallowing? Add re-raise or comment justifying suppression", + ) + + +# ---------- PY-TEL-02: public *Client methods have @record_metrics ---------- + + +def check_tel_02(path: str, tree: ast.Module) -> None: + for node in ast.walk(tree): + if not isinstance(node, ast.ClassDef): + continue + if not node.name.endswith("Client"): + continue + for item in node.body: + if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if item.name.startswith("_"): + continue + has_record = any( + ( + isinstance(d, ast.Call) + and isinstance(d.func, ast.Name) + and d.func.id == "record_metrics" + ) + or (isinstance(d, ast.Name) and d.id == "record_metrics") + for d in item.decorator_list + ) + if not has_record: + emit( + "PY-TEL-02", + "BLOCK", + path, + item.lineno, + f"Public method `{node.name}.{item.name}` missing @record_metrics decorator", + ) + + +# ---------- PY-TEL-06: test files that assert record_request_metric calls ---------- + + +def check_tel_06_test_asserts(path: str, tree: ast.Module) -> bool: + """Returns True if the test file asserts record_request_metric was called.""" + src = ast.dump(tree) + return "record_request_metric" in src + + +# ---------- PY-PT-08: public functions/methods have type hints ---------- + + +def check_pt_08(path: str, tree: ast.Module) -> None: + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + # skip private functions + if node.name.startswith("_") and node.name != "__init__": + continue + # check return annotation + if node.returns is None and node.name != "__init__": + emit( + "PY-PT-08", + "FLAG", + path, + node.lineno, + f"Public function `{node.name}` missing return type annotation", + ) + # check arg annotations (excluding self) + args = node.args.args + for arg in args: + if arg.arg == "self": + continue + if arg.annotation is None: + emit( + "PY-PT-08", + "FLAG", + path, + node.lineno, + f"Parameter `{arg.arg}` of `{node.name}` missing type annotation", + ) + + +# ---------- PY-CON-01: repeated string literals ---------- + + +def check_con_01( + path: str, tree: ast.Module, threshold: int = 3, min_len: int = 4 +) -> None: + counts: dict[str, list[int]] = {} + for node in ast.walk(tree): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + v = node.value + if len(v) < min_len: + continue + if v.startswith(("http", "test-", "SPDX", "@example")): + continue # skip URLs, test fixtures, doc markers + counts.setdefault(v, []).append(node.lineno) + for literal, lines in counts.items(): + if len(lines) >= threshold: + emit( + "PY-CON-01", + "FLAG", + path, + lines[0], + f"String literal {literal!r} appears {len(lines)}× — extract module-level constant", + suggestion=f"e.g., _CONSTANT_NAME = {literal!r}", + ) + + +# ---------- PY-PT-01: create_client factory exists ---------- + + +def check_pt_01(module_dir: str) -> bool: + """Check if module has create_client() function (returns True if found).""" + p = Path(module_dir) + if not p.is_dir(): + return False + # scan module-level defs across all .py files + for py in p.glob("*.py"): + if py.name.startswith("_") and py.name != "__init__.py": + continue + tree = parse_file(str(py)) + if tree is None: + continue + for node in tree.body: + if ( + isinstance(node, ast.FunctionDef) + and node.name.startswith("create_") + and node.name.endswith("client") + ): + return True + return False + + +# ---------- __all__ extraction (used by breaking-detector.py) ---------- + + +def extract_all(tree: ast.Module) -> list[str]: + """Extract the __all__ list from a module AST.""" + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "__all__": + if isinstance(node.value, (ast.List, ast.Tuple)): + return [ + elt.value + for elt in node.value.elts + if isinstance(elt, ast.Constant) and isinstance(elt.value, str) + ] + return [] + + +def extract_public_class_methods( + tree: ast.Module, +) -> dict[str, dict[str, tuple[list[str], str | None]]]: + """Return {ClassName: {method_name: (arg_names, return_annotation_repr)}}.""" + result: dict[str, dict[str, tuple[list[str], str | None]]] = {} + for node in ast.walk(tree): + if not isinstance(node, ast.ClassDef): + continue + if node.name.startswith("_"): + continue + methods: dict[str, tuple[list[str], str | None]] = {} + for item in node.body: + if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if item.name.startswith("_") and item.name != "__init__": + continue + args = [a.arg for a in item.args.args if a.arg != "self"] + ret = ast.unparse(item.returns) if item.returns else None + methods[item.name] = (args, ret) + if methods: + result[node.name] = methods + return result + + +def extract_dataclass_fields(tree: ast.Module) -> dict[str, list[str]]: + """Return {DataclassName: [field_names]} for classes decorated with @dataclass.""" + result: dict[str, list[str]] = {} + for node in ast.walk(tree): + if not isinstance(node, ast.ClassDef): + continue + is_dc = any( + (isinstance(d, ast.Name) and d.id == "dataclass") + or ( + isinstance(d, ast.Call) + and isinstance(d.func, ast.Name) + and d.func.id == "dataclass" + ) + for d in node.decorator_list + ) + if not is_dc: + continue + fields: list[str] = [] + for item in node.body: + if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name): + fields.append(item.target.id) + if fields: + result[node.name] = fields + return result + + +# ---------- dispatcher ---------- + +CHECKS = { + "el-01": check_el_01, + "el-02": check_el_02, + "tel-02": check_tel_02, + "pt-08": check_pt_08, + "con-01": check_con_01, +} + + +def main(argv: list[str]) -> int: + if len(argv) < 3: + print( + "Usage: ast-python-checks.py [ ...]", + file=sys.stderr, + ) + return 2 + check_name = argv[1] + files = argv[2:] + + if check_name not in CHECKS: + print(f"ERROR: unknown check {check_name}", file=sys.stderr) + return 2 + + fn = CHECKS[check_name] + for f in files: + tree = parse_file(f) + if tree is None: + continue + fn(f, tree) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.claude/scripts/lib/baseline.sh b/.claude/scripts/lib/baseline.sh new file mode 100755 index 00000000..4fece150 --- /dev/null +++ b/.claude/scripts/lib/baseline.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# lib/baseline.sh — Apply baseline exemptions to a findings JSON. +# Reads baseline.json + a check-report JSON, outputs the filtered report. +set -euo pipefail + +# is_exempted rule file baseline_json → prints "true" | "false" +# Baseline schema: {exemptions: {"": {scope: "repo|module:|file", files_exempted_glob: "", file_snapshot: {...}}}} +is_exempted() { + local rule="$1" file="$2" baseline="$3" + local entry; entry=$(echo "$baseline" | jq -r --arg rule "$rule" '.exemptions[$rule] // empty') + if [ -z "$entry" ]; then echo "false"; return; fi + + local scope glob + scope=$(echo "$entry" | jq -r '.scope // "repo"') + glob=$(echo "$entry" | jq -r '.files_exempted_glob // "**"') + + case "$scope" in + repo) + echo "true" + ;; + file) + # only exempt if file is in file_snapshot AND has not regressed (LOC check) + local snapshot; snapshot=$(echo "$entry" | jq -r --arg f "$file" '.file_snapshot[$f] // empty') + if [ -n "$snapshot" ]; then echo "true"; else echo "false"; fi + ;; + module:*) + local mod="${scope#module:}" + # naive: match path contains "/$mod/" or starts with "$mod/" + if [[ "$file" == *"/${mod}/"* || "$file" == "${mod}/"* ]]; then + echo "true" + else + echo "false" + fi + ;; + *) + echo "false" + ;; + esac +} + +# apply_baseline_to_report baseline_file report_file → prints filtered report to stdout, +# suppressed count to stderr +apply_baseline_to_report() { + local baseline_file="$1" report_file="$2" + local baseline; baseline=$(cat "$baseline_file" 2>/dev/null || echo '{"exemptions":{}}') + + local report; report=$(cat "$report_file") + local kept=() suppressed=0 + + local n; n=$(echo "$report" | jq '.findings | length') + local i=0 + while [ "$i" -lt "$n" ]; do + local finding rule file exempt + finding=$(echo "$report" | jq -c ".findings[$i]") + rule=$(echo "$finding" | jq -r '.rule') + file=$(echo "$finding" | jq -r '.file') + exempt=$(is_exempted "$rule" "$file" "$baseline") + if [ "$exempt" = "true" ]; then + suppressed=$((suppressed + 1)) + else + kept+=("$finding") + fi + i=$((i + 1)) + done + + echo "$report" | jq --argjson kept "$(printf '%s\n' "${kept[@]:-}" | jq -s -c '.')" \ + --argjson suppressed "$suppressed" \ + '.findings = $kept | .summary.suppressed_by_baseline = $suppressed' +} + +# When sourced, expose functions. When executed, run apply_baseline_to_report. +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + apply_baseline_to_report "$@" +fi diff --git a/.claude/scripts/lib/breaking-detector.py b/.claude/scripts/lib/breaking-detector.py new file mode 100755 index 00000000..ae97fb5a --- /dev/null +++ b/.claude/scripts/lib/breaking-detector.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +"""Breaking-change detector. + +Compares AST of base commit vs HEAD to find: + - api_removal_detected removed from __all__ + - method_deletion_on_public_class + - dataclass_field_deletion + - public_method_signature_change + - enum_value_deletion + - exception_hierarchy_change + +Output: JSON on stdout: +{ + "breaking_detected": true|false, + "kinds": [...], + "details": [{"kind": "...", "symbol": "...", "file": "...", "line": N}, ...] +} +""" + +from __future__ import annotations + +import ast +import json +import subprocess +import sys +from pathlib import Path + +# Import from same directory +sys.path.insert(0, str(Path(__file__).parent)) +from ast_python_checks import ( + extract_all, + extract_public_class_methods, + extract_dataclass_fields, +) # noqa: E402 + + +def git_show(sha: str, path: str) -> str | None: + """Read file content at a given commit. Returns None if not available.""" + # Try direct git show first + try: + out = subprocess.run( + ["git", "show", f"{sha}:{path}"], + capture_output=True, + text=True, + check=False, + ) + if out.returncode == 0: + return out.stdout + except Exception: + pass + # Fallback: try to fetch the object first if it's missing + try: + subprocess.run( + ["git", "fetch", "--quiet", "origin", sha], + capture_output=True, + text=True, + check=False, + timeout=10, + ) + out = subprocess.run( + ["git", "show", f"{sha}:{path}"], + capture_output=True, + text=True, + check=False, + ) + if out.returncode == 0: + return out.stdout + except Exception: + pass + return None + + +def read_head_file(path: str) -> str | None: + """Read file content from HEAD (working tree).""" + try: + return Path(path).read_text() + except (OSError, UnicodeDecodeError): + return None + + +def parse_source(source: str, filename: str = "") -> ast.Module | None: + try: + return ast.parse(source, filename=filename) + except SyntaxError: + return None + + +def files_changed(base: str, head: str) -> list[str]: + out = subprocess.run( + ["git", "diff", "--name-only", f"{base}..{head}", "--", "*.py"], + capture_output=True, + text=True, + check=False, + ) + return [f for f in out.stdout.strip().split("\n") if f] + + +def detect(base: str, head: str) -> dict: + details: list[dict] = [] + kinds: set[str] = set() + + changed = files_changed(base, head) + + for file in changed: + base_src = git_show(base, file) + head_src = git_show(head, file) if head != "HEAD" else read_head_file(file) + if head_src is None and Path(file).exists(): + head_src = read_head_file(file) + if head_src is None: + # file deleted + if base_src: + base_tree = parse_source(base_src, file) + if base_tree: + for name in extract_all(base_tree): + details.append( + { + "kind": "api_removal_detected", + "symbol": name, + "file": file, + "line": 0, + } + ) + kinds.add("api_removal_detected") + continue + + base_tree = parse_source(base_src, file) if base_src else None + head_tree = parse_source(head_src, file) + if base_tree is None or head_tree is None: + continue + + # __all__ removals + base_all = set(extract_all(base_tree)) + head_all = set(extract_all(head_tree)) + removed = base_all - head_all + for name in removed: + details.append( + { + "kind": "api_removal_detected", + "symbol": name, + "file": file, + "line": 0, + } + ) + kinds.add("api_removal_detected") + + # public class method removals + signature changes + base_classes = extract_public_class_methods(base_tree) + head_classes = extract_public_class_methods(head_tree) + for cls_name, base_methods in base_classes.items(): + head_methods = head_classes.get(cls_name, {}) + for mname, (base_args, base_ret) in base_methods.items(): + if mname not in head_methods: + details.append( + { + "kind": "method_deletion_on_public_class", + "symbol": f"{cls_name}.{mname}", + "file": file, + "line": 0, + } + ) + kinds.add("method_deletion_on_public_class") + else: + head_args, head_ret = head_methods[mname] + if base_args != head_args: + details.append( + { + "kind": "public_method_signature_change", + "symbol": f"{cls_name}.{mname}", + "file": file, + "line": 0, + "base_args": base_args, + "head_args": head_args, + } + ) + kinds.add("public_method_signature_change") + + # dataclass field removals + base_dcs = extract_dataclass_fields(base_tree) + head_dcs = extract_dataclass_fields(head_tree) + for dc_name, base_fields in base_dcs.items(): + head_fields = set(head_dcs.get(dc_name, [])) + for field in base_fields: + if field not in head_fields: + details.append( + { + "kind": "dataclass_field_deletion_on_public_model", + "symbol": f"{dc_name}.{field}", + "file": file, + "line": 0, + } + ) + kinds.add("dataclass_field_deletion_on_public_model") + + # enum value removals (best-effort: str Enum classes) + base_enums = _extract_enums(base_tree) + head_enums = _extract_enums(head_tree) + for enum_name, base_values in base_enums.items(): + head_values = set(head_enums.get(enum_name, [])) + for val in base_values: + if val not in head_values: + details.append( + { + "kind": "enum_value_deletion_on_public_enum", + "symbol": f"{enum_name}.{val}", + "file": file, + "line": 0, + } + ) + kinds.add("enum_value_deletion_on_public_enum") + + return { + "breaking_detected": bool(kinds), + "kinds": sorted(kinds), + "details": details, + } + + +def _extract_enums(tree: ast.Module) -> dict[str, list[str]]: + result: dict[str, list[str]] = {} + for node in ast.walk(tree): + if not isinstance(node, ast.ClassDef): + continue + if node.name.startswith("_"): + continue + # heuristic: base class named "Enum" or "IntEnum" or has str, Enum bases + is_enum = any( + (isinstance(b, ast.Name) and "Enum" in b.id) + or (isinstance(b, ast.Attribute) and "Enum" in b.attr) + for b in node.bases + ) + if not is_enum: + continue + values: list[str] = [] + for item in node.body: + if isinstance(item, ast.Assign): + for tgt in item.targets: + if isinstance(tgt, ast.Name): + values.append(tgt.id) + result[node.name] = values + return result + + +def main(argv: list[str]) -> int: + if len(argv) < 3: + # default to comparing merge-base of main..HEAD + base = ( + subprocess.run( + ["git", "merge-base", "origin/main", "HEAD"], + capture_output=True, + text=True, + check=False, + ).stdout.strip() + or "HEAD~1" + ) + head = "HEAD" + else: + base, head = argv[1], argv[2] + + result = detect(base, head) + print(json.dumps(result, indent=2)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.claude/scripts/lib/detect-language.sh b/.claude/scripts/lib/detect-language.sh new file mode 100755 index 00000000..d3f9e26c --- /dev/null +++ b/.claude/scripts/lib/detect-language.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# lib/detect-language.sh — detect Python vs Java repo shape. +set -euo pipefail + +detect_language() { + local root="${1:-.}" + if [ -f "$root/pyproject.toml" ] && [ -f "$root/pom.xml" ]; then + echo "ERROR: both pyproject.toml AND pom.xml found — cannot auto-detect" >&2 + exit 3 + fi + if [ -f "$root/pyproject.toml" ]; then + if [ -d "$root/src/sap_cloud_sdk" ] || [ -d "$root/src" ]; then + echo "python"; return + fi + fi + if [ -f "$root/pom.xml" ]; then + if [ -d "$root/src/main/java" ]; then + echo "java"; return + fi + fi + echo "unknown" +} + +detect_language "$@" diff --git a/.claude/scripts/lib/detect-modules.sh b/.claude/scripts/lib/detect-modules.sh new file mode 100755 index 00000000..87fad9bd --- /dev/null +++ b/.claude/scripts/lib/detect-modules.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# lib/detect-modules.sh — extract module names from a diff. +set -euo pipefail + +# Usage: detect-modules.sh < diff.patch +lang="${1:-python}" +if [ "$lang" = "python" ]; then + grep -oE '(src/sap_cloud_sdk|src/)[a-z_][a-z_0-9]*/' 2>/dev/null | \ + sed -E 's|src/sap_cloud_sdk/||; s|src/||; s|/$||' | \ + grep -v '^core$' | grep -v '^\s*$' | sort -u +elif [ "$lang" = "java" ]; then + grep -oE 'src/main/java/com/sap/cloud/sdk/[a-z_][a-z_0-9]*/' 2>/dev/null | \ + sed 's|src/main/java/com/sap/cloud/sdk/||; s|/$||' | \ + grep -v '^core$' | sort -u +else + echo "ERROR: unknown language $lang" >&2; exit 2 +fi diff --git a/.claude/scripts/lib/diff-added-lines.sh b/.claude/scripts/lib/diff-added-lines.sh new file mode 100755 index 00000000..cce3788e --- /dev/null +++ b/.claude/scripts/lib/diff-added-lines.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# lib/diff-added-lines.sh — Emit file:line pairs that are in the added set (+) of a diff. +# Reads unified diff from stdin, outputs "path:linenumber" one per line (right-side lines only). +# Automatically excludes the skill's own files (self-review protection). +set -euo pipefail + +awk ' + BEGIN { file=""; line=0; skip=0 } + /^diff --git a\// { + # Field 4 is "b/" — take it directly (regex was buggy with paths containing "b/") + file = $4 + sub(/^b\//, "", file) + line = 0 + # Self-review protection: skip skill files + skip = 0 + if (file ~ /^\.claude\//) skip = 1 + if (file ~ /^tests\/sdk-review\//) skip = 1 + if (file ~ /^\.github\/workflows\/sdk-/) skip = 1 + if (file ~ /^docs\/PR-REVIEW\.md$/) skip = 1 + if (file ~ /^docs\/BRANCH-PROTECTION-SETUP\.md$/) skip = 1 + next + } + /^\+\+\+ / { next } + /^--- / { next } + /^@@/ { + # hunk header: @@ -old,+new,newcount @@ + if (match($0, /\+[0-9]+/)) { + line = substr($0, RSTART+1, RLENGTH-1) + 0 + } + next + } + /^\+/ && !/^\+\+\+/ { + if (!skip && file != "" && line > 0) print file ":" line + line++ + next + } + /^-/ && !/^---/ { + # deletion line — do not increment right-side counter + next + } + /^ / { + # context line — increment right-side counter but do not print + line++ + next + } +' "$@" diff --git a/.claude/scripts/lib/github-api.sh b/.claude/scripts/lib/github-api.sh new file mode 100755 index 00000000..2808d92c --- /dev/null +++ b/.claude/scripts/lib/github-api.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# lib/github-api.sh — gh CLI wrapper for review posting. +set -euo pipefail + +readonly SDK_REVIEW_MARKER_PREFIX="' + +# check_gh_auth → exit 0 if authed, exit 2 with message otherwise +check_gh_auth() { + local hostname="${1:-github.com}" + if ! gh auth status --hostname "$hostname" > /dev/null 2>&1; then + echo "ERROR: gh CLI not authenticated for $hostname. Run: gh auth login --hostname $hostname" >&2 + exit 2 + fi +} + +# detect_hostname → prints "github.com" or "github.tools.sap" from git remote +detect_hostname() { + local remote_url + remote_url=$(git remote get-url origin 2>/dev/null || echo "") + case "$remote_url" in + *github.tools.sap*) echo "github.tools.sap" ;; + *github.com*) echo "github.com" ;; + *) echo "github.com" ;; + esac +} + +# detect_owner_repo → prints "owner/repo" from git remote +detect_owner_repo() { + local remote_url + remote_url=$(git remote get-url origin 2>/dev/null || echo "") + # strip protocol/user prefix and .git suffix + remote_url="${remote_url#*github.com/}" + remote_url="${remote_url#*github.tools.sap/}" + remote_url="${remote_url#*://}" + remote_url="${remote_url%.git}" + echo "$remote_url" +} + +# get_pr_head_sha → prints the head SHA +get_pr_head_sha() { + local pr="$1" + gh pr view "$pr" --json headRefOid -q .headRefOid +} + +# list_bot_review_comments → outputs "id\tbody_marker" for each review-comment we've posted +list_bot_review_comments() { + local pr="$1" + local owner_repo; owner_repo=$(detect_owner_repo) + gh api "repos/${owner_repo}/pulls/${pr}/comments" --paginate 2>/dev/null | \ + jq -r --arg m "$SDK_REVIEW_MARKER_PREFIX" '.[] | select(.body | contains($m)) | "\(.id)\t\(.body[0:120])"' || true +} + +# list_bot_issue_comments → outputs "id" for each issue-comment (summary) we've posted +list_bot_issue_comments() { + local pr="$1" + local owner_repo; owner_repo=$(detect_owner_repo) + gh api "repos/${owner_repo}/issues/${pr}/comments" --paginate 2>/dev/null | \ + jq -r --arg m "$SUMMARY_MARKER" '.[] | select(.body | contains($m)) | .id' || true +} + +# delete_prior_bot_artifacts — idempotency: remove all sdk-review:v1 comments before posting new +delete_prior_bot_artifacts() { + local pr="$1" + local owner_repo; owner_repo=$(detect_owner_repo) + + # review-comments (inline) + list_bot_review_comments "$pr" | awk -F'\t' '{print $1}' | while read -r id; do + [ -n "$id" ] && gh api -X DELETE "repos/${owner_repo}/pulls/comments/${id}" > /dev/null 2>&1 || true + done + + # issue-comments (summary) + list_bot_issue_comments "$pr" | while read -r id; do + [ -n "$id" ] && gh api -X DELETE "repos/${owner_repo}/issues/comments/${id}" > /dev/null 2>&1 || true + done +} + +# post_inline_comment +post_inline_comment() { + local pr="$1" sha="$2" path="$3" line="$4" body="$5" + local owner_repo; owner_repo=$(detect_owner_repo) + gh api "repos/${owner_repo}/pulls/${pr}/comments" \ + -F body="$body" \ + -F commit_id="$sha" \ + -F path="$path" \ + -F line="$line" \ + -F side="RIGHT" > /dev/null 2>&1 || \ + # If line is not in diff, fall back to issue comment with file/line citation + gh api "repos/${owner_repo}/issues/${pr}/comments" \ + -F body="$body + +_(originally intended as inline on \`$path:$line\` but that line is not in the diff)_" > /dev/null +} + +# post_summary_comment +post_summary_comment() { + local pr="$1" body="$2" + local owner_repo; owner_repo=$(detect_owner_repo) + gh api "repos/${owner_repo}/issues/${pr}/comments" -F body="$body" > /dev/null +} + +# post_or_update_check_run <summary> +post_or_update_check_run() { + local sha="$1" conclusion="$2" title="$3" summary="$4" + local owner_repo; owner_repo=$(detect_owner_repo) + local check_name="sdk-module-review" + local external_id="sdk-review-v1-${sha}" + + # try to find existing run for same SHA + name + local existing + existing=$(gh api "repos/${owner_repo}/commits/${sha}/check-runs?check_name=${check_name}" 2>/dev/null | \ + jq -r '.check_runs[0].id // empty' || true) + + if [ -n "$existing" ]; then + gh api -X PATCH "repos/${owner_repo}/check-runs/${existing}" \ + -F status="completed" \ + -F conclusion="$conclusion" \ + -F "output[title]=$title" \ + -F "output[summary]=$summary" > /dev/null 2>&1 || { + echo "WARN: could not update check-run (likely missing checks:write scope)" >&2 + } + else + gh api "repos/${owner_repo}/check-runs" \ + -F name="$check_name" \ + -F head_sha="$sha" \ + -F external_id="$external_id" \ + -F status="completed" \ + -F conclusion="$conclusion" \ + -F "output[title]=$title" \ + -F "output[summary]=$summary" > /dev/null 2>&1 || { + echo "WARN: could not create check-run (likely missing checks:write scope)" >&2 + } + fi +} + +# apply_label <pr> <label> — creates label if missing, adds to PR, removes other sdk-review labels +apply_label() { + local pr="$1" label="$2" + local owner_repo; owner_repo=$(detect_owner_repo) + + # ensure labels exist + for l in "sdk-review: ✅ passed:0e8a16" "sdk-review: ❌ blocked:b60205" \ + "sdk-review: ⚠️ flagged:e4e669" "sdk-review: skipped:cccccc"; do + local name color + name="${l%:*}"; color="${l##*:}" + gh api "repos/${owner_repo}/labels" -F name="$name" -F color="$color" > /dev/null 2>&1 || true + done + + # remove any existing sdk-review labels first + gh pr view "$pr" --json labels -q '.labels[].name' 2>/dev/null | grep '^sdk-review:' | while read -r existing; do + gh pr edit "$pr" --remove-label "$existing" > /dev/null 2>&1 || true + done + + # add the target label + gh pr edit "$pr" --add-label "$label" > /dev/null 2>&1 || true +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + cmd="${1:-}"; shift || true + case "$cmd" in + check_gh_auth) check_gh_auth "$@" ;; + detect_hostname) detect_hostname "$@" ;; + detect_owner_repo) detect_owner_repo "$@" ;; + get_pr_head_sha) get_pr_head_sha "$@" ;; + delete_prior_bot_artifacts) delete_prior_bot_artifacts "$@" ;; + post_inline_comment) post_inline_comment "$@" ;; + post_summary_comment) post_summary_comment "$@" ;; + post_or_update_check_run) post_or_update_check_run "$@" ;; + apply_label) apply_label "$@" ;; + *) echo "Usage: github-api.sh <command> args" >&2; exit 2 ;; + esac +fi diff --git a/.claude/scripts/lib/json-emit.sh b/.claude/scripts/lib/json-emit.sh new file mode 100755 index 00000000..4917107d --- /dev/null +++ b/.claude/scripts/lib/json-emit.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# lib/json-emit.sh — Shared JSON output helpers for check scripts. +# Every check-<X>.sh emits a JSON object matching the schema in 00-COMMON.md §3. +set -euo pipefail + +# emit_finding writes a single finding object to stdout (one JSON line per call). +# Args: rule severity file line message [suggestion] +emit_finding() { + local rule="$1" severity="$2" file="$3" line="$4" message="$5" suggestion="${6:-}" + jq -cn \ + --arg rule "$rule" \ + --arg severity "$severity" \ + --arg file "$file" \ + --argjson line "$line" \ + --arg message "$message" \ + --arg suggestion "$suggestion" \ + '{rule:$rule, severity:$severity, file:$file, line:$line, message:$message, suggestion:$suggestion}' +} + +# emit_report assembles the full JSON report from a set of findings (JSONL on stdin). +# Args: check_name language status started_at +emit_report() { + local check="$1" language="$2" status="$3" started_at="$4" + local tmp; tmp=$(mktemp) + cat > "$tmp" + local block_count flag_count + block_count=$(jq -s '[.[] | select(.severity=="BLOCK")] | length' < "$tmp") + flag_count=$(jq -s '[.[] | select(.severity=="FLAG")] | length' < "$tmp") + # Build findings array from JSONL (jq -s reads each JSON object and produces one array) + local findings; findings=$(jq -s '.' < "$tmp") + + jq -n \ + --arg check "$check" \ + --arg version "1.0.0" \ + --arg language "$language" \ + --arg status "$status" \ + --arg started_at "$started_at" \ + --argjson block_count "$block_count" \ + --argjson flag_count "$flag_count" \ + --argjson findings "$findings" \ + '{ + check: $check, + version: $version, + language: $language, + status: $status, + started_at: $started_at, + duration_ms: 0, + modules_analysed: [], + findings: $findings, + summary: { + block_count: $block_count, + flag_count: $flag_count, + pass_criteria_met: [], + pass_criteria_failed: [] + } + }' + rm -f "$tmp" +} + +# status_from_findings derives PASS/FLAG/BLOCK from a JSONL of findings on stdin. +status_from_findings() { + local input; input=$(cat) + if [ -z "$input" ]; then echo "PASS"; return; fi + if echo "$input" | jq -s -e 'any(.severity=="BLOCK")' > /dev/null 2>&1; then + echo "BLOCK" + elif echo "$input" | jq -s -e 'any(.severity=="FLAG")' > /dev/null 2>&1; then + echo "FLAG" + else + echo "PASS" + fi +} + +# now_iso returns the current UTC time in ISO-8601 format. +now_iso() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} diff --git a/.claude/scripts/lib/predicates.sh b/.claude/scripts/lib/predicates.sh new file mode 100755 index 00000000..a4b55bd0 --- /dev/null +++ b/.claude/scripts/lib/predicates.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# lib/predicates.sh — Evaluate scope predicates for rules. +# Called by orchestrate.sh before each check runs. +# Outputs comma-separated rule IDs to SKIP because predicates are false. +set -euo pipefail + +# has_commit_type <base_sha> <head_sha> <type1,type2,...> → prints "true" or "false" +has_commit_type() { + local base="$1" head="$2" types="$3" + local pattern + pattern=$(echo "$types" | tr ',' '|') + if git log "${base}..${head}" --format=%s 2>/dev/null | grep -qE "^(${pattern})(\(.*\))?!?: "; then + echo "true" + else + echo "false" + fi +} + +# module_shape <module_dir> → prints "client" | "patch" | "config" | "other" +module_shape() { + local dir="$1" + if [ -f "$dir/client.py" ] || [ -f "$dir/_http.py" ] || compgen -G "$dir/*Client.java" > /dev/null 2>&1; then + echo "client" + elif [ -f "$dir/_patch.py" ] || compgen -G "$dir/patch*.py" > /dev/null 2>&1; then + echo "patch" + elif [ -f "$dir/config.py" ] && [ ! -f "$dir/client.py" ]; then + echo "config" + else + echo "other" + fi +} + +# reuse_toml_aggregate_present <repo_root> → prints "true" or "false" +reuse_toml_aggregate_present() { + local root="${1:-.}" + local reuse="$root/REUSE.toml" + if [ ! -f "$reuse" ]; then echo "false"; return; fi + if grep -qE 'path\s*=\s*"\*\*?"' "$reuse" 2>/dev/null; then + echo "true" + else + echo "false" + fi +} + +# file_in_added_set <file> <added_lines_file> → prints "true" or "false" +file_in_added_set() { + local file="$1" added_lines_file="$2" + if grep -q "^${file}:" "$added_lines_file" 2>/dev/null; then + echo "true" + else + echo "false" + fi +} + +# line_in_added_set <file> <line> <added_lines_file> → prints "true" or "false" +line_in_added_set() { + local file="$1" line="$2" added_lines_file="$3" + if grep -q "^${file}:${line}$" "$added_lines_file" 2>/dev/null; then + echo "true" + else + echo "false" + fi +} + +# filter_findings_by_hunk <report_file> <added_lines_file> → prints filtered report +filter_findings_by_hunk() { + local report="$1" added="$2" + jq --slurpfile added <(sort -u "$added" | jq -R .) ' + .findings |= map( + select(($added[0] // []) | index(.file + ":" + (.line|tostring)) != null) + ) + ' "$report" +} + +# When executed directly, dispatch to subcommand +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + cmd="${1:-}"; shift || true + case "$cmd" in + has_commit_type) has_commit_type "$@" ;; + module_shape) module_shape "$@" ;; + reuse_toml_aggregate_present) reuse_toml_aggregate_present "$@" ;; + file_in_added_set) file_in_added_set "$@" ;; + line_in_added_set) line_in_added_set "$@" ;; + filter_findings_by_hunk) filter_findings_by_hunk "$@" ;; + *) echo "Usage: predicates.sh <command> [args]" >&2; exit 2 ;; + esac +fi diff --git a/.claude/scripts/lib/skill-self-skip.sh b/.claude/scripts/lib/skill-self-skip.sh new file mode 100755 index 00000000..53658845 --- /dev/null +++ b/.claude/scripts/lib/skill-self-skip.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# lib/skill-self-skip.sh — return "true" if a file is part of the SDK review skill itself +# (used to prevent meta-review-loop where the skill's own files trigger findings). +set -euo pipefail + +is_skill_file() { + local file="$1" + case "$file" in + .claude/*) echo "true"; return ;; + tests/sdk-review/*) echo "true"; return ;; + .github/workflows/sdk-*) echo "true"; return ;; + docs/PR-REVIEW.md|docs/BRANCH-PROTECTION-SETUP.md) echo "true"; return ;; + *) echo "false" ;; + esac +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + is_skill_file "$@" +fi diff --git a/.claude/scripts/lib/suppression.sh b/.claude/scripts/lib/suppression.sh new file mode 100755 index 00000000..1789a8ad --- /dev/null +++ b/.claude/scripts/lib/suppression.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# lib/suppression.sh — Parse # sdk-review: ignore[<check>] comments. +# Outputs "<file>:<line>:<check>" tuples of suppressions found in a diff. +set -euo pipefail + +# Line-level: any occurrence of "sdk-review: ignore[X,Y]" or "sdk-review: ignore" on same line +# File-level: "sdk-review-ignore-file: X,Y" within first 20 lines of file + +# parse_line_suppressions <file> +# Scans the file and emits <file>:<line>:<check> for each suppression on that line. +parse_line_suppressions() { + local file="$1" + [ -f "$file" ] || return 0 + awk -v file="$file" ' + match($0, /sdk-review:[[:space:]]*ignore(\[[^]]*\])?/) { + # extract [checks] if present + m = substr($0, RSTART, RLENGTH) + if (match(m, /\[[^]]*\]/)) { + checks = substr(m, RSTART+1, RLENGTH-2) + n = split(checks, arr, ",") + for (j=1; j<=n; j++) { + gsub(/^ +| +$/, "", arr[j]) + print file ":" NR ":" arr[j] + } + } else { + print file ":" NR ":*" + } + } + ' "$file" +} + +# parse_file_suppressions <file> +# Emits <file>:*:<check> when a file-level suppression header is present in first 20 lines. +parse_file_suppressions() { + local file="$1" + [ -f "$file" ] || return 0 + head -20 "$file" 2>/dev/null | awk -v file="$file" ' + match($0, /sdk-review-ignore-file:[[:space:]]*[a-zA-Z0-9_,-]+/) { + m = substr($0, RSTART, RLENGTH) + sub(/^sdk-review-ignore-file:[[:space:]]*/, "", m) + n = split(m, arr, ",") + for (j=1; j<=n; j++) { + gsub(/^ +| +$/, "", arr[j]) + print file ":*:" arr[j] + } + } + ' +} + +# is_suppressed <rule> <file> <line> <suppressions_file> +# Locked rules (SEC-*, HC-03, DIS-06, LIC-01/02, BND-02, BND-05, BREAKING-*) NEVER suppressed. +is_suppressed() { + local rule="$1" file="$2" line="$3" supp_file="$4" + + case "$rule" in + SEC-*|HC-03|DIS-06|LIC-01|LIC-02|BND-02|BND-05|BREAKING-*) + echo "false"; return + ;; + esac + + [ -f "$supp_file" ] || { echo "false"; return; } + + # exact line + rule + if grep -q "^${file}:${line}:${rule}$" "$supp_file"; then echo "true"; return; fi + # exact line + wildcard + if grep -q "^${file}:${line}:\*$" "$supp_file"; then echo "true"; return; fi + # file + rule + if grep -q "^${file}:\*:${rule}$" "$supp_file"; then echo "true"; return; fi + + echo "false" +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + cmd="${1:-}"; shift || true + case "$cmd" in + parse_line) parse_line_suppressions "$@" ;; + parse_file) parse_file_suppressions "$@" ;; + is_suppressed) is_suppressed "$@" ;; + *) echo "Usage: suppression.sh {parse_line|parse_file|is_suppressed} args" >&2; exit 2 ;; + esac +fi diff --git a/.claude/scripts/lib/tier-manager.sh b/.claude/scripts/lib/tier-manager.sh new file mode 100755 index 00000000..113278db --- /dev/null +++ b/.claude/scripts/lib/tier-manager.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# lib/tier-manager.sh — apply rule tiers (SHADOW/FLAG/BLOCK/BLOCK_LOCKED) to a report. +# Reads rules.yaml, filters/reclassifies findings based on rule tier. +set -euo pipefail + +# get_tier <rule> <rules_yaml_file> → prints tier +get_tier() { + local rule="$1" cfg="$2" + # Grep for tier under rule id (simple YAML parsing — no yq dep required) + # Format expected: + # <RULE-ID>: + # tier: SHADOW|FLAG|BLOCK|BLOCK_LOCKED + awk -v rule="$rule" ' + $0 ~ ("^ " rule ":$") { in_rule=1; next } + in_rule && /^ [A-Z]/ { in_rule=0 } + in_rule && /tier:/ { + gsub(/^[[:space:]]*tier:[[:space:]]*/, "") + print + exit + } + ' "$cfg" 2>/dev/null || echo "" +} + +# apply_tiers_to_report <report> <rules_yaml> → prints filtered report +# SHADOW findings are stripped from .findings and moved to .shadow_findings +# BLOCK_LOCKED findings get a locked=true flag +apply_tiers_to_report() { + local report="$1" cfg="$2" + + local kept=() shadow=() + local n; n=$(jq '.findings | length' "$report") + local i=0 + while [ "$i" -lt "$n" ]; do + local f rule tier severity + f=$(jq -c ".findings[$i]" "$report") + rule=$(echo "$f" | jq -r '.rule') + tier=$(get_tier "$rule" "$cfg") + [ -z "$tier" ] && tier=$(echo "$f" | jq -r '.severity') # fall back to severity + severity=$(echo "$f" | jq -r '.severity') + + case "$tier" in + SHADOW) + shadow+=("$(echo "$f" | jq -c '. + {tier: "SHADOW"}')") + ;; + FLAG) + kept+=("$(echo "$f" | jq -c '.severity = "FLAG" | .tier = "FLAG"')") + ;; + BLOCK) + kept+=("$(echo "$f" | jq -c '.severity = "BLOCK" | .tier = "BLOCK"')") + ;; + BLOCK_LOCKED) + kept+=("$(echo "$f" | jq -c '.severity = "BLOCK" | .tier = "BLOCK_LOCKED" | .locked = true')") + ;; + *) + kept+=("$(echo "$f" | jq -c ". + {tier: \"$severity\"}")") + ;; + esac + i=$((i + 1)) + done + + local kept_json shadow_json + kept_json=$(printf '%s\n' "${kept[@]:-}" | jq -s -c '.') + shadow_json=$(printf '%s\n' "${shadow[@]:-}" | jq -s -c '.') + + jq --argjson kept "$kept_json" --argjson shadow "$shadow_json" \ + '.findings = $kept | .shadow_findings = $shadow' "$report" +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + cmd="${1:-}"; shift || true + case "$cmd" in + get_tier) get_tier "$@" ;; + apply_tiers_to_report) apply_tiers_to_report "$@" ;; + *) echo "Usage: tier-manager.sh {get_tier|apply_tiers_to_report} args" >&2; exit 2 ;; + esac +fi diff --git a/.claude/scripts/orchestrate.sh b/.claude/scripts/orchestrate.sh new file mode 100755 index 00000000..55432fae --- /dev/null +++ b/.claude/scripts/orchestrate.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# orchestrate.sh — main entry point. Runs all 20 checks and posts results. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB="$SCRIPT_DIR/lib" + +# shellcheck source=lib/json-emit.sh +source "$LIB/json-emit.sh" +# shellcheck source=lib/github-api.sh +source "$LIB/github-api.sh" + +PR_NUMBER="${1:-}" +DRY_RUN="${DRY_RUN:-false}" +if [ "${2:-}" = "--dry-run" ]; then DRY_RUN=true; fi + +if [ -z "$PR_NUMBER" ]; then + echo "Usage: orchestrate.sh <PR_NUMBER> [--dry-run]" >&2 + exit 2 +fi + +REPO_ROOT="${REPO_ROOT:-$(pwd)}" +export REPO_ROOT +CONFIG_DIR="$REPO_ROOT/.claude/config" +export CONFIG_DIR +if [ -n "${TMPDIR_RUN:-}" ]; then + mkdir -p "$TMPDIR_RUN" +else + TMPDIR_RUN="$(mktemp -d)" +fi +trap '[ -n "${KEEP_TMP:-}" ] || rm -rf "$TMPDIR_RUN"' EXIT + +echo "▶ SDK Module Review — PR #$PR_NUMBER (dry-run=$DRY_RUN)" +echo " Working dir: $TMPDIR_RUN" + +# 1. Preflight — detect language + hostname + fetch PR data +LANGUAGE=$("$LIB/detect-language.sh" "$REPO_ROOT") +export LANGUAGE +echo " Language: $LANGUAGE" + +if [ "$DRY_RUN" != "true" ]; then + HOSTNAME=$(detect_hostname) + check_gh_auth "$HOSTNAME" +fi + +# 2. Fetch diff + PR metadata +if [ -f "${DIFF_FILE:-}" ]; then + cp "$DIFF_FILE" "$TMPDIR_RUN/pr.diff" +elif [ "$DRY_RUN" = "true" ] && [ -n "${LOCAL_DIFF:-}" ]; then + cp "$LOCAL_DIFF" "$TMPDIR_RUN/pr.diff" +else + gh pr diff "$PR_NUMBER" > "$TMPDIR_RUN/pr.diff" +fi + +if [ "$DRY_RUN" != "true" ]; then + gh pr view "$PR_NUMBER" --json body -q .body > "$TMPDIR_RUN/pr-body.txt" 2>/dev/null || echo "" > "$TMPDIR_RUN/pr-body.txt" + HEAD_SHA=$(get_pr_head_sha "$PR_NUMBER") +elif [ -n "${LOCAL_PR_BODY:-}" ] && [ -f "$LOCAL_PR_BODY" ]; then + cp "$LOCAL_PR_BODY" "$TMPDIR_RUN/pr-body.txt" + HEAD_SHA="${HEAD_SHA:-HEAD}" +else + echo "" > "$TMPDIR_RUN/pr-body.txt" + HEAD_SHA="${HEAD_SHA:-HEAD}" +fi + +export DIFF_FILE="$TMPDIR_RUN/pr.diff" +export PR_BODY_FILE="$TMPDIR_RUN/pr-body.txt" +export HEAD_SHA +export BASE_SHA="${BASE_SHA:-$(git merge-base origin/main HEAD 2>/dev/null || echo HEAD~10)}" +export BREAKING_JSON="$TMPDIR_RUN/breaking.json" + +# 3. Compute added-lines set (used for hunk attribution) +"$LIB/diff-added-lines.sh" < "$DIFF_FILE" > "$TMPDIR_RUN/added-lines.txt" +export ADDED_LINES_FILE="$TMPDIR_RUN/added-lines.txt" + +# 4. Run breaking-change detector +python3 "$LIB/breaking-detector.py" "$BASE_SHA" "$HEAD_SHA" > "$BREAKING_JSON" 2>/dev/null || echo '{"breaking_detected":false,"kinds":[],"details":[]}' > "$BREAKING_JSON" + +# 5. Detect disclosure profile from remote +if [ "$DRY_RUN" != "true" ]; then + remote_url=$(git remote get-url origin 2>/dev/null || echo "") + if [[ "$remote_url" == *"github.tools.sap"* ]]; then + export DISCLOSURE_PROFILE="internal" + else + export DISCLOSURE_PROFILE="public" + fi +else + export DISCLOSURE_PROFILE="${DISCLOSURE_PROFILE:-public}" +fi + +# 6. Run all 20 checks in parallel +checks=(secrets license-spdx disclosure hardcode telemetry + docs bdd patterns versioning commits + errors-logging testing-depth http-hygiene concurrency + deps-supply deletion-hygiene constants binding-shape + quality-gate-parity pr-size) + +for check in "${checks[@]}"; do + script="$SCRIPT_DIR/check-${check}.sh" + if [ ! -x "$script" ]; then continue; fi + DIFF_FILE="$DIFF_FILE" PR_BODY_FILE="$PR_BODY_FILE" \ + "$script" < "$DIFF_FILE" > "$TMPDIR_RUN/report-${check}.json" 2> "$TMPDIR_RUN/${check}.err" & +done +wait + +# 6.5. Collect suppression tuples from files touched by the diff, then filter each report +touched_files=$(grep -oE '^\+\+\+ b/[^[:space:]]+' "$DIFF_FILE" 2>/dev/null | sed 's|^+++ b/||' | sort -u || true) +supp_file="$TMPDIR_RUN/suppressions.txt" +: > "$supp_file" +if [ -n "$touched_files" ]; then + while IFS= read -r f; do + [ -z "$f" ] && continue + [ -f "$REPO_ROOT/$f" ] || continue + bash "$LIB/suppression.sh" parse_line "$REPO_ROOT/$f" 2>/dev/null | sed "s|^$REPO_ROOT/|| ; s|^\./||" >> "$supp_file" || true + bash "$LIB/suppression.sh" parse_file "$REPO_ROOT/$f" 2>/dev/null | sed "s|^$REPO_ROOT/|| ; s|^\./||" >> "$supp_file" || true + done <<< "$touched_files" +fi + +for check in "${checks[@]}"; do + report="$TMPDIR_RUN/report-${check}.json" + [ -f "$report" ] || continue + filtered=$(bash "$LIB/apply-suppression.sh" apply "$report" "$supp_file" 2>/dev/null || cat "$report") + echo "$filtered" > "$report" +done + +# 7. Aggregate reports (applies tier gating per rules.yaml) +RULES_YAML="$REPO_ROOT/.claude/config/rules.yaml" \ + "$SCRIPT_DIR/aggregate.sh" "$TMPDIR_RUN" > "$TMPDIR_RUN/summary.json" + +# 8. Post signals (unless dry-run) +if [ "$DRY_RUN" = "true" ]; then + echo "" + echo "▶ DRY-RUN summary:" + jq -r ' + " BLOCK: \(.summary.block_count) FLAG: \(.summary.flag_count) SHADOW: \(.summary.shadow_count)", + "", + "Findings:", + (.findings[] | " [\(.severity)] \(.rule) at \(.file):\(.line) — \(.message)") + ' "$TMPDIR_RUN/summary.json" + echo "" + echo "▶ Full report at: $TMPDIR_RUN/summary.json" + exit "$(jq -r '.summary.block_count | if . > 0 then 1 else 0 end' "$TMPDIR_RUN/summary.json")" +fi + +# 9. Idempotency: delete prior bot artifacts +delete_prior_bot_artifacts "$PR_NUMBER" + +# 10. Post inline comments +n_inline=$(jq '.findings | length' "$TMPDIR_RUN/summary.json") +i=0 +while [ "$i" -lt "$n_inline" ]; do + f=$(jq -c ".findings[$i]" "$TMPDIR_RUN/summary.json") + file=$(echo "$f" | jq -r '.file') + line=$(echo "$f" | jq -r '.line') + rule=$(echo "$f" | jq -r '.rule') + sev=$(echo "$f" | jq -r '.severity') + msg=$(echo "$f" | jq -r '.message') + body="<!-- sdk-review:v1 check=$rule id=$rule-$i --> +**[$sev] $rule** + +$msg" + post_inline_comment "$PR_NUMBER" "$HEAD_SHA" "$file" "$line" "$body" || true + i=$((i + 1)) +done + +# 11. Post summary comment +summary_body=$(jq -r ' + "<!-- sdk-review:v1 kind=summary --> +## SDK Module Review + +| Check | Status | Findings | +|-------|--------|----------| +" + (.per_check_summary | to_entries | map("| \(.key) | \(.value.status) | \(.value.count) |") | join("\n")) + " + +<details><summary>Details</summary> + +" + (.findings | map("- **[\(.severity)] \(.rule)** at `\(.file):\(.line)` — \(.message)") | join("\n")) + " + +</details> + +--- +_Generated by sdk-review-skill · v1_" +' "$TMPDIR_RUN/summary.json") + +post_summary_comment "$PR_NUMBER" "$summary_body" + +# 12. Post check-run +conclusion=$(jq -r 'if .summary.block_count > 0 then "failure" elif .summary.flag_count > 0 then "neutral" else "success" end' "$TMPDIR_RUN/summary.json") +title=$(jq -r "\"SDK Review: \(.summary.block_count) BLOCK, \(.summary.flag_count) FLAG\"" "$TMPDIR_RUN/summary.json") +post_or_update_check_run "$HEAD_SHA" "$conclusion" "$title" "$summary_body" + +# 13. Apply label +case "$conclusion" in + success) label="sdk-review: ✅ passed" ;; + neutral) label="sdk-review: ⚠️ flagged" ;; + failure) label="sdk-review: ❌ blocked" ;; +esac +apply_label "$PR_NUMBER" "$label" + +# 14. Exit +exit "$(jq -r '.summary.block_count | if . > 0 then 1 else 0 end' "$TMPDIR_RUN/summary.json")" diff --git a/.github/workflows/sdk-module-review.yml b/.github/workflows/sdk-module-review.yml new file mode 100644 index 00000000..ed45abe3 --- /dev/null +++ b/.github/workflows/sdk-module-review.yml @@ -0,0 +1,42 @@ +name: SDK Module Review +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + inputs: + pr_number: + description: "PR number to review" + required: true + +jobs: + review: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch' + permissions: + contents: read + pull-requests: write + checks: write + issues: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install jq + run: sudo apt-get install -y jq + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Checkout sibling SDK repo (BDD parity) + uses: actions/checkout@v4 + with: + repository: ${{ vars.SIBLING_SDK_REPO }} + path: .sibling-sdk + token: ${{ secrets.SIBLING_SDK_TOKEN }} + continue-on-error: true + - name: Run SDK review + env: + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} + SDK_SIBLING_PATH: ${{ github.workspace }}/.sibling-sdk + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash .claude/scripts/orchestrate.sh "$PR_NUMBER" diff --git a/.github/workflows/sdk-skill-isolation-check.yml b/.github/workflows/sdk-skill-isolation-check.yml new file mode 100644 index 00000000..88a7fa16 --- /dev/null +++ b/.github/workflows/sdk-skill-isolation-check.yml @@ -0,0 +1,59 @@ +name: SDK Skill Isolation Check +# Validates that sub-PRs into feat/sdk-review-skill satisfy the isolation criteria +# from 06-INCREMENTAL-DELIVERY.md §isolation criteria. +on: + pull_request: + branches: [feat/sdk-review-skill] + +jobs: + isolation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install tools + run: | + sudo apt-get update && sudo apt-get install -y jq shellcheck bats + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install ruff + run: pip install ruff + - name: 1. Compiles alone — shellcheck + run: | + shellcheck -x --severity=error .claude/scripts/**/*.sh + - name: 1. Compiles alone — ruff + run: | + ruff check .claude/scripts/lib/*.py + - name: 2. Testable alone — bats + run: | + bats tests/sdk-review/*.bats + - name: 3. Revert-safe — nothing in main branch broken + run: | + # Verify that scripts still function after this PR is applied + bash .claude/scripts/lib/detect-language.sh /tmp || echo "detect-language exit acceptable" + bash .claude/scripts/lib/diff-added-lines.sh < /dev/null || echo "diff-added-lines exit acceptable" + - name: 4. Sub-PR size check + run: | + # Warn if PR exceeds max sub-PR size (400 LOC per 06-INCREMENTAL-DELIVERY.md) + adds=$(git diff origin/feat/sdk-review-skill...HEAD --shortstat | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0) + if [ "${adds:-0}" -gt 400 ]; then + echo "::warning::Sub-PR has $adds additions (>400 target). Consider splitting per 06-INCREMENTAL-DELIVERY.md." + fi + - name: 5. Fixture-tested check + run: | + # Every new check-*.sh must have at least one fixture + new_checks=$(git diff origin/feat/sdk-review-skill...HEAD --name-only --diff-filter=A | grep '^\.claude/scripts/check-' || true) + missing_fixtures="" + for check in $new_checks; do + name=$(basename "$check" .sh | sed 's/^check-//') + if ! ls tests/sdk-review/fixtures/${name}-*.diff >/dev/null 2>&1; then + missing_fixtures="$missing_fixtures $name" + fi + done + if [ -n "$missing_fixtures" ]; then + echo "::error::New checks missing fixtures:$missing_fixtures" + exit 1 + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef9b4c23..bfbe8788 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,6 +70,31 @@ Before starting your contribution: - **Read [Code Guidelines](docs/GUIDELINES.md)** to understand our development standards and conventions - **Review existing user guides** to understand the expected documentation format and API patterns +#### AI-assisted contribution skills — required for review + +Before maintainers review your PR, you **must** run the SDK Module Review skill and ensure it passes. Reviewers will not review PRs where `sdk-module-review` is failing or missing. + +**How to run:** + +- **In CI (automatic):** every PR triggers the `sdk-module-review` GitHub Action. It runs 20 deterministic checks and posts findings (inline comments + summary + check-run + label). This is a **required status check** — the merge button is disabled until the run is green. + +- **Locally (recommended, before pushing):** + ``` + /review-new-module <PR_NUMBER> # posts findings to the PR + /review-new-module <PR_NUMBER> --dry-run # analysis only, prints locally + ``` + Uses the exact same scripts the CI uses. No LLM calls — same input, same output. + +**Related skills:** + +- **`scaffold-module`** (`.claude/skills/scaffold-module/`) — bootstraps a new BTP service module with the standard directory layout, telemetry wiring, and stubs. Use before writing code for a new module: + ``` + /scaffold-module <module-name> + ``` +- **`review-pr`** (`.claude/skills/review-pr/`) — same underlying pipeline as `/review-new-module`; useful when reviewing someone else's PR interactively. + +See [`docs/PR-REVIEW.md`](docs/PR-REVIEW.md) for the full rule catalog, suppression syntax, and how to interact with review findings. + ### 2. Create or claim an issue 1. Go to our repository's [GitHub Issues page](https://github.com/SAP/cloud-sdk-python/issues) diff --git a/dist/sap_cloud_sdk-0.4.0-py3-none-any.whl b/dist/sap_cloud_sdk-0.4.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..434290ad4b6b6104e4e51cb36f85a9dc1390ae66 GIT binary patch literal 128954 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%0DW7TOP}-&#u*u4H>QF> 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 literal 0 HcmV?d00001 diff --git a/docs/BRANCH-PROTECTION-SETUP.md b/docs/BRANCH-PROTECTION-SETUP.md new file mode 100644 index 00000000..ceec9ce2 --- /dev/null +++ b/docs/BRANCH-PROTECTION-SETUP.md @@ -0,0 +1,177 @@ +# Branch Protection Setup — sdk-module-review as required check + +This guide is for **repo admins**. It documents how to configure the +`sdk-module-review` action as a required status check so that PRs cannot be +merged until the review passes. + +Once configured, every PR against `main` will be blocked from merge until: +1. The `sdk-module-review` action runs +2. The action reports success (no `BLOCK` findings) +3. All other required checks (existing CI, REUSE, etc.) also pass + +--- + +## Prerequisites + +- You have **admin** permission on the repo +- The `feat/sdk-review-skill` PR has been merged to `main`, so + `.github/workflows/sdk-module-review.yml` is live +- At least one PR has run the workflow successfully (so GitHub knows the + check-name `sdk-module-review` exists — required checks can only be added + after they've fired at least once) + +## Steps (GitHub UI) + +1. Navigate to **Settings → Branches → Branch protection rules** +2. If a rule already exists for `main`, edit it. Otherwise click **Add rule** + and enter `main` as the branch name pattern. +3. Under **Protect matching branches**, enable: + - ☑ **Require a pull request before merging** + - ☑ Require approvals: `1` (or more per team convention) + - ☑ Dismiss stale pull request approvals when new commits are pushed + - ☑ Require review from Code Owners + - ☑ **Require status checks to pass before merging** + - ☑ Require branches to be up to date before merging + - In the search box, add: + - `sdk-module-review` ← our skill + - `test` (or whatever the existing CI is called) + - `reuse` (if REUSE-check is used) + - ☑ **Require conversation resolution before merging** + - ☑ **Do not allow bypassing the above settings** (admins included) +4. Click **Create** or **Save changes** + +## Steps (via gh CLI, alternative) + +```bash +# cloud-sdk-python (public GitHub) +gh api -X PUT "repos/SAP/cloud-sdk-python/branches/main/protection" \ + --input - <<'EOF' +{ + "required_status_checks": { + "strict": true, + "contexts": ["sdk-module-review", "test", "reuse"] + }, + "enforce_admins": true, + "required_pull_request_reviews": { + "dismiss_stale_reviews": true, + "require_code_owner_reviews": true, + "required_approving_review_count": 1 + }, + "restrictions": null, + "required_conversation_resolution": true +} +EOF + +# cloud-sdk-java (internal GHES) +gh api --hostname github.tools.sap -X PUT \ + "repos/application-foundation/cloud-sdk-java/branches/main/protection" \ + --input - <<'EOF' +{ + "required_status_checks": { + "strict": true, + "contexts": ["sdk-module-review", "ci", "codeql-sast-analysis"] + }, + "enforce_admins": true, + "required_pull_request_reviews": { + "dismiss_stale_reviews": true, + "require_code_owner_reviews": true, + "required_approving_review_count": 1 + }, + "restrictions": null +} +EOF +``` + +Adjust the `contexts` list to include the CI check-names actually used by the +repo (see the "Checks" tab of any recent PR to confirm the exact names). + +## Verify + +After configuration, open a test PR (or use any open PR): + +```bash +gh pr view <PR_NUMBER> --json statusCheckRollup -q '.statusCheckRollup[].name' +``` + +You should see `sdk-module-review` in the list. If the check hasn't run yet +(never fired on this branch), it will appear as "expected" (pending). + +## Rollout stages + +We recommend a **SHADOW → FLAG → BLOCK** progression to minimise contributor +friction: + +### Stage 1 — SHADOW (weeks 1–2) + +- The workflow runs on every PR but **rules are downgraded to `SHADOW`** +- Findings are logged to `.claude/telemetry/*.jsonl` but **not posted to the PR** +- Contributors are unaffected; maintainers observe FP rate + +To enable SHADOW mode, edit `.claude/config/rules.yaml`: +```yaml +rules: + # temporarily downgrade all BLOCK to SHADOW for first two weeks + BND-02: { tier: SHADOW } + BREAKING-01: { tier: SHADOW } + # ... etc +``` + +Or set an env var in the workflow: `SDK_REVIEW_TIER_OVERRIDE=shadow-all`. + +### Stage 2 — FLAG (weeks 3–4) + +- Rules promoted to `FLAG` — findings posted as inline comments and summary, + but check-run is **not** required for merge +- Contributors see the review and can act on it +- Maintainers verify FP rate stays < 5 % on real PRs + +### Stage 3 — BLOCK (week 5+) + +- `sdk-module-review` added as required status check (this document's main topic) +- Merges blocked on `BLOCK` findings +- `BLOCK_LOCKED` rules (secrets, SPDX in public, token URL concat, breaking + changes without declaration) are non-suppressible + +## Rollback + +If `sdk-module-review` fires too aggressively and needs to be turned off: + +1. Remove `sdk-module-review` from the required-checks list (via UI or `gh api`) +2. Or disable the workflow entirely: `.github/workflows/sdk-module-review.yml` → + set `on: workflow_dispatch` only (no auto-triggers) +3. Individual noisy rules can be downgraded in `.claude/config/rules.yaml`: + ```yaml + rules: + RULE-ID: { tier: FLAG } # was BLOCK; downgrade to advisory + RULE-ID: { tier: SHADOW } # or silence entirely + ``` + +`BLOCK_LOCKED` rules cannot be downgraded via config — they are safety-critical +(secrets, license, disclosure in public repo, BTP token URL concat). To +temporarily disable one, remove it from `rules.yaml` altogether and open a +follow-up issue to reintroduce it. + +## Troubleshooting + +- **Check-run not appearing on new PRs**: verify the workflow file + `.github/workflows/sdk-module-review.yml` is present on `main` and the + action has permissions `contents: read`, `pull-requests: write`, + `checks: write`, `issues: write` (for labels) +- **Check-run runs but never completes**: check the workflow logs for auth + errors (`gh auth status`) or missing `SIBLING_SDK_TOKEN` secret (cross-repo + BDD parity is optional — degrades to `FLAG` if unavailable) +- **False positives**: see `docs/PR-REVIEW.md § Suppressing false positives` + and `§ Tuning noisy rules` +- **Required check missing from branch protection dropdown**: the check must + have fired at least once on any branch before GitHub lists it. Open a + throwaway PR to trigger it, or dispatch the workflow manually: + ```bash + gh workflow run sdk-module-review.yml -f pr_number=<N> + ``` + +## Cross-references + +- [`docs/PR-REVIEW.md`](./PR-REVIEW.md) — user-facing docs on what the skill checks +- [`.claude/config/rules.yaml`](../.claude/config/rules.yaml) — rule catalog with tiers +- [`.claude/config/baseline.json`](../.claude/config/baseline.json) — repo-specific exemptions +- [`.github/workflows/sdk-module-review.yml`](../.github/workflows/sdk-module-review.yml) — the workflow diff --git a/docs/PR-REVIEW.md b/docs/PR-REVIEW.md new file mode 100644 index 00000000..e42dfe32 --- /dev/null +++ b/docs/PR-REVIEW.md @@ -0,0 +1,245 @@ +# SDK Module Review + +Every PR to this repo is reviewed automatically by the **SDK Module Review** skill. +This document explains what it checks, how to interact with findings, and how to tune noisy rules. + +> **Required before review.** The `sdk-module-review` check-run must be green **before a maintainer will review the PR**. It is a required status check in branch protection — merges are blocked until it passes. Reviewers will not spend time on PRs where the automated review is failing. + +--- + +## How it runs + +**Automatic (GitHub Action):** fires on every PR event (`opened`, `synchronize`, `reopened`, `ready_for_review`). No action needed from the contributor — the review appears on the PR within a minute or two. + +**Manual (maintainer, via CLI):** +```bash +gh workflow run sdk-module-review.yml -f pr_number=<N> +``` + +**Local (dev iteration, requires Claude Code):** +``` +/review-new-module <PR_NUMBER> # full review, posts to PR +/review-new-module <PR_NUMBER> --dry-run # analysis only, prints locally +``` + +The skill is **100% deterministic** — no LLM calls in CI. Every run on the same +commit produces the same findings. + +--- + +## Signals posted to your PR + +Every run posts up to four signals: + +1. **Inline comments** — one per finding, anchored to `file:line` in the diff +2. **Summary comment** — aggregated table of all check results in an issue comment +3. **Check-run** — appears in the "Checks" tab (green ✅ or red ❌) +4. **PR label** — `sdk-review: ✅ passed` / `❌ blocked` / `⚠️ flagged` / `skipped` + +On re-run (push more commits, dispatch workflow again), all four artifacts are +**replaced** — you won't see duplicate comments accumulating. + +--- + +## Severities + +- **BLOCK** — must fix before merge. Check-run fails; branch protection refuses the merge. +- **FLAG** — should fix; does not block merge. Check-run passes with warning. +- **PASS** — no findings for this check. +- **SHADOW** — internal telemetry; not posted, used to evaluate new rules before promoting them. + +Some rules are **locked** (marked `BLOCK_LOCKED` in `.claude/config/rules.yaml`). +Locked rules cannot be suppressed via inline comments or downgraded via config — +they are safety-critical (secrets, license, SAP-internal URL leaks, breaking-change +declarations). + +--- + +## What the skill checks + +20 checks · ~150 rules. Configured in [`.claude/config/rules.yaml`](../.claude/config/rules.yaml). + +| Check | Purpose | +|-------|---------| +| **secrets** | AWS keys, JWT, GitHub PATs, private keys, plaintext credentials | +| **license-spdx** | SPDX headers on new source files (respects `REUSE.toml`) | +| **disclosure** | No SAP-internal URLs, ORD IDs, internal Jira in public artifacts | +| **hardcode** | No hardcoded URLs, credentials, magic timeouts | +| **telemetry** | `@record_metrics` decorator on public client methods + emission tests | +| **docs** | `user-guide.md` completeness including BTP dep + regional availability | +| **bdd** | Feature files exist + cross-language parity | +| **patterns** | Factory pattern, exception hierarchy, type hints, `py.typed` | +| **versioning** | SemVer bump matches diff scope; BREAKING family fires on API changes | +| **commits** | Conventional Commits | +| **errors-logging** | `raise X from e` chaining, no sensitive info in exception messages | +| **testing-depth** | Bug fixes have tests; new modules have integration tests | +| **http-hygiene** | Session reuse, configurable timeouts | +| **concurrency** | `asyncio.Queue` dedup, thread safety on shared state | +| **deps-supply** | Dep justification, lockfile drift, no internal artifactory | +| **deletion-hygiene** | Removed symbols have zero residual references | +| **constants** | Magic values → constants/enums | +| **binding-shape** | BTP binding parsing (no `url + "/oauth/token"` concat) | +| **quality-gate-parity** | CI runs the full dev gate (ruff + format + typecheck) | +| **pr-size** | Advisory on large PRs (recommends stacked-PR workflow) | + +--- + +## Documenting BTP dependencies (DC-11..DC-16) + +If your module imports `destination`, `Fragment*`, `Certificate*`, or has +region-specific constants, `user-guide.md` **must** contain the corresponding +section. The skill fires **BLOCK** if these are missing: + +- **DC-11** — `destination` import → `## Dependencies` section mentioning "Destination Service" +- **DC-12** — Fragment usage → same section + `create_fragment_client` example +- **DC-13** — Certificate usage → same section + `create_certificate_client` example +- **DC-14** — Region constants → `## Regional Availability` section listing supported/unsupported regions +- **DC-15** — Reads `VCAP_SERVICES` → `## Configuration` section with sample binding JSON +- **DC-16** — Cross-module SDK dep → note in `## Dependencies` + +### Example templates + +**DC-11 Destination Service required:** +```markdown +## Dependencies + +This module requires **SAP BTP Destination Service**: +- Service instance name: `default` (configurable via `create_client(instance="...")`) +- Required binding: `xsuaa` credentials + `destination` service credentials +- Mount path: `/etc/secrets/sapbtp/destination/<instance>/` +- Local dev: set `CLOUD_SDK_LOCALDEV_DESTINATION=true` to use mock backing +``` + +**DC-14 Regional Availability:** +```markdown +## Regional Availability + +Available in the following BTP regions: +- ✅ `eu10` (Frankfurt) +- ✅ `us10` (Ashburn) +- ✅ `ap11` (Singapore) +- ❌ `cn40` (Shanghai) — not supported due to <reason> +``` + +--- + +## Breaking changes (BREAKING-01..04) + +If your diff contains a **breaking change** (public API removal, method signature +change, dataclass field deletion, enum value removal, exception hierarchy change), +the skill requires: + +1. Commit message uses `feat!:` or `fix!:` prefix +2. PR body has a `## Breaking Changes` section with **non-empty** content +3. PR body ticks the "Breaking change" checkbox +4. `pyproject.toml` / `pom.xml` version is bumped MINOR (or MAJOR if pre-1.0) +5. Migration path documented in PR body or `RELEASE.md` + +All four must be true — half-declared breakages are BLOCKed. This is enforced by +`BREAKING-01` and `BREAKING-02`, both `BLOCK_LOCKED` (cannot be suppressed). + +--- + +## Incremental delivery for large contributions + +For features exceeding ~500 lines or touching multiple modules, prefer **stacked PRs**: + +1. Open a feature branch off `main`: `feat/<capability>` +2. Send small, isolated PRs (≤400 lines each) targeting **your feature branch** +3. Each sub-PR passes CI standalone +4. When feature is complete, open a final PR from your feature branch to `main` + +The skill flags `PR-SIZE-01..05` (currently SHADOW tier — logged only) when a PR +exceeds thresholds. See `CONTRIBUTING.md § Incremental Delivery`. + +--- + +## Suppressing false positives + +If a rule fires incorrectly on a specific line, add a comment: + +```python +# Python +timeout = 30 # sdk-review: ignore[hardcode] +``` + +```java +// Java +final int timeout = 30; // sdk-review: ignore[hardcode] +``` + +Or for an entire file (first 20 lines): +```python +# sdk-review-ignore-file: hardcode,patterns +``` + +**You cannot suppress:** +- Any `SEC-*` rule (secrets) +- `HC-03` (SAP-internal URL leak) +- `DIS-06` (internal artifactory `--index-url`) +- `LIC-01/02` (SPDX headers) +- `BND-02` (BTP token URL concat) +- `BND-05` (binding logs credentials) +- `BREAKING-*` family + +These are locked. Fix the finding or open a discussion with maintainers. + +--- + +## When cross-language BDD parity can't be verified + +The skill checks that new modules have BDD feature files in **both** SDKs (Python +and Java). If the sibling repo can't be reached (SSO required, network unavailable, +missing checkout), `check-bdd.sh` degrades to a FLAG "cross-language parity not +verified" instead of blocking. + +The alias map at `.claude/config/module-aliases.yaml` handles name divergences +(e.g., Python `dms` ↔ Java `documentmanagement`). + +--- + +## Re-running + +Just push a new commit — the Action reruns automatically and replaces prior +review artifacts. Or dispatch manually: + +```bash +gh workflow run sdk-module-review.yml -f pr_number=<N> +``` + +--- + +## Tuning noisy rules + +Maintainers can adjust rule severity or disable rules in +`.claude/config/rules.yaml`: + +```yaml +rules: + HC-04: + tier: OFF # disable + # or + HC-04: + tier: FLAG # downgrade from BLOCK to FLAG +``` + +Locked rules ignore these overrides. + +--- + +## What if something breaks? + +- **False positive that survived suppression** → open an issue with label `sdk-review-tuning` +- **Skill errors on your PR** → open an issue with label `sdk-review-bug` +- **Suggested new rule** → open an issue with label `sdk-review-enhancement` + +--- + +## Reference + +- Rule catalog: [`.claude/config/rules.yaml`](../.claude/config/rules.yaml) +- Module aliases: [`.claude/config/module-aliases.yaml`](../.claude/config/module-aliases.yaml) +- Baseline exemptions: [`.claude/config/baseline.json`](../.claude/config/baseline.json) +- Check scripts: [`.claude/scripts/check-*.sh`](../.claude/scripts/) +- Orchestrator: [`.claude/scripts/orchestrate.sh`](../.claude/scripts/orchestrate.sh) +- Workflow: [`.github/workflows/sdk-module-review.yml`](../.github/workflows/sdk-module-review.yml) diff --git a/tests/sdk-review/fixtures/binding-token-concat.diff b/tests/sdk-review/fixtures/binding-token-concat.diff new file mode 100644 index 00000000..1b51251d --- /dev/null +++ b/tests/sdk-review/fixtures/binding-token-concat.diff @@ -0,0 +1,7 @@ +diff --git a/src/config.java b/src/config.java +index 111..222 100644 +--- a/src/config.java ++++ b/src/config.java +@@ -1,3 +1,4 @@ ++ String tokenUrl = uaaData.url + "/oauth/token"; + x = 1 diff --git a/tests/sdk-review/fixtures/clean.diff b/tests/sdk-review/fixtures/clean.diff new file mode 100644 index 00000000..ca1ecd85 --- /dev/null +++ b/tests/sdk-review/fixtures/clean.diff @@ -0,0 +1,9 @@ +diff --git a/src/module.py b/src/module.py +index 111..222 100644 +--- a/src/module.py ++++ b/src/module.py +@@ -1,3 +1,4 @@ + # existing code ++def new_helper() -> None: ++ return None + x = 1 diff --git a/tests/sdk-review/fixtures/disclosure-internal.diff b/tests/sdk-review/fixtures/disclosure-internal.diff new file mode 100644 index 00000000..be1b3cf2 --- /dev/null +++ b/tests/sdk-review/fixtures/disclosure-internal.diff @@ -0,0 +1,7 @@ +diff --git a/src/config.py b/src/config.py +index 111..222 100644 +--- a/src/config.py ++++ b/src/config.py +@@ -1,3 +1,4 @@ ++API = "https://internal.tools.sap/api" + x = 1 diff --git a/tests/sdk-review/fixtures/hardcode-url.diff b/tests/sdk-review/fixtures/hardcode-url.diff new file mode 100644 index 00000000..70c1668c --- /dev/null +++ b/tests/sdk-review/fixtures/hardcode-url.diff @@ -0,0 +1,8 @@ +diff --git a/src/sap_cloud_sdk/mymodule/client.py b/src/sap_cloud_sdk/mymodule/client.py +index 111..222 100644 +--- a/src/sap_cloud_sdk/mymodule/client.py ++++ b/src/sap_cloud_sdk/mymodule/client.py +@@ -1,3 +1,4 @@ + # existing ++API_URL = "https://api.example-real-service.com/v1" + x = 1 diff --git a/tests/sdk-review/fixtures/secrets-aws-key.diff b/tests/sdk-review/fixtures/secrets-aws-key.diff new file mode 100644 index 00000000..1c8c11a1 --- /dev/null +++ b/tests/sdk-review/fixtures/secrets-aws-key.diff @@ -0,0 +1,9 @@ +diff --git a/src/config.py b/src/config.py +new file mode 100644 +index 0000000..abc1234 +--- /dev/null ++++ b/src/config.py +@@ -0,0 +1,3 @@ ++# SPDX-License-Identifier: Apache-2.0 ++AWS_KEY = "AKIATESTFIXTUREXXXXX" ++other = "safe string" diff --git a/tests/sdk-review/fixtures/secrets-jwt.diff b/tests/sdk-review/fixtures/secrets-jwt.diff new file mode 100644 index 00000000..f4dd9804 --- /dev/null +++ b/tests/sdk-review/fixtures/secrets-jwt.diff @@ -0,0 +1,8 @@ +diff --git a/src/token.py b/src/token.py +new file mode 100644 +index 0000000..abc1234 +--- /dev/null ++++ b/src/token.py +@@ -0,0 +1,2 @@ ++# SPDX-License-Identifier: Apache-2.0 ++JWT_TOKEN = "eyJTESTFIXTURE0.eyJTESTFIXTURE00000000.TESTFIXTURESIG-NotARealJwt" diff --git a/tests/sdk-review/test_extended.bats b/tests/sdk-review/test_extended.bats new file mode 100644 index 00000000..aae21a65 --- /dev/null +++ b/tests/sdk-review/test_extended.bats @@ -0,0 +1,439 @@ +#!/usr/bin/env bats +# Extended test suite — tier gating, suppression, integration flows, edge cases. + +setup() { + SCRIPT_DIR="$BATS_TEST_DIRNAME/../../.claude/scripts" + FIXTURES="$BATS_TEST_DIRNAME/fixtures" + export LANGUAGE=python + export REPO_ROOT="$BATS_TEST_DIRNAME/../.." + export CONFIG_DIR="$REPO_ROOT/.claude/config" +} + +# ============================================================ +# TIER GATING (aggregate.sh applies rules.yaml tiers) +# ============================================================ + +@test "aggregate: SHADOW rule findings moved to shadow_findings, not posted" { + tmpd=$(mktemp -d) + cat > "$tmpd/report-test.json" <<'EOF' +{ + "check": "test", + "version": "1.0.0", + "language": "python", + "status": "FLAG", + "started_at": "2026-01-01T00:00:00Z", + "duration_ms": 0, + "modules_analysed": [], + "findings": [ + {"rule": "PR-SIZE-01", "severity": "FLAG", "file": ".", "line": 1, "message": "big", "suggestion": ""}, + {"rule": "SEC-01", "severity": "BLOCK", "file": "x", "line": 1, "message": "aws", "suggestion": ""} + ], + "summary": {"block_count": 1, "flag_count": 1} +} +EOF + result=$(RULES_YAML="$CONFIG_DIR/rules.yaml" bash "$SCRIPT_DIR/aggregate.sh" "$tmpd") + # PR-SIZE-01 is SHADOW → goes to shadow_findings + echo "$result" | jq -e '.shadow_findings | length == 1' + echo "$result" | jq -e '.shadow_findings[0].rule == "PR-SIZE-01"' + # SEC-01 is BLOCK_LOCKED → stays in findings with locked=true + echo "$result" | jq -e '.findings | length == 1' + echo "$result" | jq -e '.findings[0].locked == true' + echo "$result" | jq -e '.summary.block_count == 1' + echo "$result" | jq -e '.summary.shadow_count == 1' + echo "$result" | jq -e '.summary.locked_count == 1' + rm -rf "$tmpd" +} + +@test "aggregate: unknown rule defaults to FLAG tier" { + tmpd=$(mktemp -d) + cat > "$tmpd/report-test.json" <<'EOF' +{ + "check": "test", "version": "1.0.0", "language": "python", "status": "FLAG", + "started_at": "2026-01-01T00:00:00Z", "duration_ms": 0, "modules_analysed": [], + "findings": [ + {"rule": "UNKNOWN-99", "severity": "BLOCK", "file": "x", "line": 1, "message": "m", "suggestion": ""} + ], + "summary": {"block_count": 1, "flag_count": 0} +} +EOF + result=$(RULES_YAML="$CONFIG_DIR/rules.yaml" bash "$SCRIPT_DIR/aggregate.sh" "$tmpd") + echo "$result" | jq -e '.findings[0].severity == "FLAG"' + rm -rf "$tmpd" +} + +@test "aggregate: BLOCK_LOCKED rules cannot be softened" { + tmpd=$(mktemp -d) + cat > "$tmpd/report-test.json" <<'EOF' +{ + "check": "test", "version": "1.0.0", "language": "python", "status": "FLAG", + "started_at": "2026-01-01T00:00:00Z", "duration_ms": 0, "modules_analysed": [], + "findings": [ + {"rule": "BND-02", "severity": "FLAG", "file": "x", "line": 1, "message": "token concat", "suggestion": ""} + ], + "summary": {"block_count": 0, "flag_count": 1} +} +EOF + result=$(RULES_YAML="$CONFIG_DIR/rules.yaml" bash "$SCRIPT_DIR/aggregate.sh" "$tmpd") + echo "$result" | jq -e '.findings[0].severity == "BLOCK"' + echo "$result" | jq -e '.findings[0].tier == "BLOCK_LOCKED"' + echo "$result" | jq -e '.findings[0].locked == true' + rm -rf "$tmpd" +} + +# ============================================================ +# SUPPRESSION (apply-suppression.sh) +# ============================================================ + +@test "suppression: parse_line detects multiple checks in ignore[a,b]" { + tmp=$(mktemp) + echo 'x = 1 # sdk-review: ignore[hardcode,patterns]' > "$tmp" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" parse_line "$tmp") + echo "$result" | grep -q ":1:hardcode" + echo "$result" | grep -q ":1:patterns" + rm -f "$tmp" +} + +@test "suppression: parse_line ignore without brackets = wildcard" { + tmp=$(mktemp) + echo 'x = 1 # sdk-review: ignore' > "$tmp" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" parse_line "$tmp") + echo "$result" | grep -q ":1:\*" + rm -f "$tmp" +} + +@test "suppression: is_suppressed returns true when rule matches line" { + supp_file=$(mktemp) + echo "src/x.py:42:HC-01" > "$supp_file" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" is_suppressed "HC-01" "src/x.py" "42" "$supp_file") + [ "$result" = "true" ] + rm -f "$supp_file" +} + +@test "suppression: is_suppressed returns true when wildcard on line" { + supp_file=$(mktemp) + echo "src/x.py:42:*" > "$supp_file" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" is_suppressed "ANY-RULE" "src/x.py" "42" "$supp_file") + [ "$result" = "true" ] + rm -f "$supp_file" +} + +@test "suppression: is_suppressed false on locked rule (SEC-01)" { + supp_file=$(mktemp) + echo "src/x.py:42:SEC-01" > "$supp_file" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" is_suppressed "SEC-01" "src/x.py" "42" "$supp_file") + [ "$result" = "false" ] + rm -f "$supp_file" +} + +@test "suppression: is_suppressed false on locked rule (BND-02)" { + supp_file=$(mktemp) + echo "src/x.py:42:BND-02" > "$supp_file" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" is_suppressed "BND-02" "src/x.py" "42" "$supp_file") + [ "$result" = "false" ] + rm -f "$supp_file" +} + +@test "suppression: file-level suppression matches any line" { + supp_file=$(mktemp) + echo "src/x.py:*:hardcode" > "$supp_file" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" is_suppressed "hardcode" "src/x.py" "999" "$supp_file") + [ "$result" = "true" ] + rm -f "$supp_file" +} + +@test "apply-suppression: filters findings correctly" { + supp_file=$(mktemp) + echo "src/x.py:10:HC-01" > "$supp_file" + + report=$(mktemp) + cat > "$report" <<'EOF' +{ + "check": "hardcode", "version": "1.0.0", "language": "python", "status": "BLOCK", + "started_at": "2026-01-01T00:00:00Z", "duration_ms": 0, "modules_analysed": [], + "findings": [ + {"rule": "HC-01", "severity": "BLOCK", "file": "src/x.py", "line": 10, "message": "url", "suggestion": ""}, + {"rule": "HC-01", "severity": "BLOCK", "file": "src/y.py", "line": 5, "message": "url", "suggestion": ""} + ], + "summary": {"block_count": 2, "flag_count": 0} +} +EOF + result=$(bash "$SCRIPT_DIR/lib/apply-suppression.sh" apply "$report" "$supp_file") + # x.py:10 suppressed, y.py:5 kept + echo "$result" | jq -e '.findings | length == 1' + echo "$result" | jq -e '.findings[0].file == "src/y.py"' + echo "$result" | jq -e '.summary.suppressed_count == 1' + rm -f "$supp_file" "$report" +} + +# ============================================================ +# JSON CONTRACT +# ============================================================ + +@test "json-emit: emit_report handles empty findings" { + source "$SCRIPT_DIR/lib/json-emit.sh" + result=$(echo -n "" | emit_report "test" "python" "PASS" "2026-01-01T00:00:00Z") + echo "$result" | jq -e '.findings | length == 0' + echo "$result" | jq -e '.summary.block_count == 0' + echo "$result" | jq -e '.summary.flag_count == 0' +} + +@test "json-emit: status_from_findings returns BLOCK when any BLOCK finding" { + source "$SCRIPT_DIR/lib/json-emit.sh" + result=$(printf '{"severity":"FLAG"}\n{"severity":"BLOCK"}\n' | status_from_findings) + [ "$result" = "BLOCK" ] +} + +@test "json-emit: status_from_findings returns PASS when empty" { + source "$SCRIPT_DIR/lib/json-emit.sh" + result=$(echo -n "" | status_from_findings) + [ "$result" = "PASS" ] +} + +# ============================================================ +# DIFF ATTRIBUTION EDGE CASES +# ============================================================ + +@test "diff-added-lines: handles multiple hunks in one file" { + cat > /tmp/multi.diff <<'EOF' +diff --git a/src/a.py b/src/a.py +--- a/src/a.py ++++ b/src/a.py +@@ -1,3 +1,4 @@ + line1 ++added_first + line2 +@@ -10,3 +11,4 @@ + line10 ++added_second + line11 +EOF + result=$(bash "$SCRIPT_DIR/lib/diff-added-lines.sh" < /tmp/multi.diff) + echo "$result" | grep -q "src/a.py:2" + echo "$result" | grep -q "src/a.py:12" + rm -f /tmp/multi.diff +} + +@test "diff-added-lines: multiple files" { + cat > /tmp/multi-file.diff <<'EOF' +diff --git a/a.py b/a.py +--- a/a.py ++++ b/a.py +@@ -1,1 +1,2 @@ + x ++new_in_a +diff --git a/b.py b/b.py +--- a/b.py ++++ b/b.py +@@ -1,1 +1,2 @@ + y ++new_in_b +EOF + result=$(bash "$SCRIPT_DIR/lib/diff-added-lines.sh" < /tmp/multi-file.diff) + echo "$result" | grep -q "a.py:2" + echo "$result" | grep -q "b.py:2" + rm -f /tmp/multi-file.diff +} + +# ============================================================ +# PREDICATES EDGE CASES +# ============================================================ + +@test "predicates: module_shape returns 'client' for module with client.py" { + source "$SCRIPT_DIR/lib/predicates.sh" + tmpd=$(mktemp -d) + touch "$tmpd/client.py" + result=$(module_shape "$tmpd") + [ "$result" = "client" ] + rm -rf "$tmpd" +} + +@test "predicates: module_shape returns 'patch' for module with _patch.py only" { + source "$SCRIPT_DIR/lib/predicates.sh" + tmpd=$(mktemp -d) + touch "$tmpd/_patch.py" + result=$(module_shape "$tmpd") + [ "$result" = "patch" ] + rm -rf "$tmpd" +} + +@test "predicates: module_shape returns 'other' for empty dir" { + source "$SCRIPT_DIR/lib/predicates.sh" + tmpd=$(mktemp -d) + result=$(module_shape "$tmpd") + [ "$result" = "other" ] + rm -rf "$tmpd" +} + +# ============================================================ +# AST EDGE CASES +# ============================================================ + +@test "ast: EL-02 accepts except: ...; return e" { + cat > /tmp/ok.py <<'EOF' +def f(): + try: + pass + except Exception as e: + return e +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-02 /tmp/ok.py) + [ -z "$result" ] + rm -f /tmp/ok.py +} + +@test "ast: EL-02 flags except: pass" { + cat > /tmp/bad.py <<'EOF' +def f(): + try: + do_work() + except Exception: + pass +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-02 /tmp/bad.py) + echo "$result" | jq -e '.rule == "PY-EL-02"' + rm -f /tmp/bad.py +} + +@test "ast: TEL-02 skips private methods" { + cat > /tmp/client.py <<'EOF' +class MyClient: + def _internal(self): + pass +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" tel-02 /tmp/client.py) + [ -z "$result" ] + rm -f /tmp/client.py +} + +@test "ast: TEL-02 skips non-Client classes" { + cat > /tmp/model.py <<'EOF' +class DataModel: + def get_value(self): + pass +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" tel-02 /tmp/model.py) + [ -z "$result" ] + rm -f /tmp/model.py +} + +@test "ast: CON-01 does not flag URLs" { + cat > /tmp/const.py <<'EOF' +a = "https://api.example.com" +b = "https://api.example.com" +c = "https://api.example.com" +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" con-01 /tmp/const.py) + [ -z "$result" ] + rm -f /tmp/const.py +} + +# ============================================================ +# BASELINE +# ============================================================ + +@test "baseline: repo-scope exempts all files" { + baseline='{"exemptions":{"PY-LIC-01":{"scope":"repo"}}}' + source "$SCRIPT_DIR/lib/baseline.sh" + result=$(is_exempted "PY-LIC-01" "src/anything.py" "$baseline") + [ "$result" = "true" ] +} + +@test "baseline: module scope exempts only matching module" { + baseline='{"exemptions":{"TEL-06":{"scope":"module:adms"}}}' + source "$SCRIPT_DIR/lib/baseline.sh" + in_module=$(is_exempted "TEL-06" "src/sap_cloud_sdk/adms/foo.py" "$baseline") + [ "$in_module" = "true" ] + other=$(is_exempted "TEL-06" "src/sap_cloud_sdk/destination/foo.py" "$baseline") + [ "$other" = "false" ] +} + +@test "baseline: unknown rule not exempted" { + baseline='{"exemptions":{"OTHER":{"scope":"repo"}}}' + source "$SCRIPT_DIR/lib/baseline.sh" + result=$(is_exempted "HC-01" "src/x.py" "$baseline") + [ "$result" = "false" ] +} + +# ============================================================ +# INTEGRATION FLOWS +# ============================================================ + +@test "orchestrate: empty diff produces summary with 0 findings" { + tmpd=$(mktemp -d) + cp -r "$REPO_ROOT/.claude" "$tmpd/" + cd "$tmpd" + echo "[project]" > pyproject.toml + mkdir -p src/sap_cloud_sdk + echo "" > /tmp/empty.diff + + set +e + DRY_RUN=true LOCAL_DIFF=/tmp/empty.diff REPO_ROOT="$tmpd" \ + bash .claude/scripts/orchestrate.sh 999 --dry-run > /tmp/out.txt 2>&1 + status=$? + set -e + [ "$status" -eq 0 ] + grep -q "BLOCK: 0" /tmp/out.txt + rm -rf "$tmpd" /tmp/empty.diff /tmp/out.txt +} + +@test "orchestrate: dry-run generates valid summary.json" { + tmpd=$(mktemp -d) + cp -r "$REPO_ROOT/.claude" "$tmpd/" + cd "$tmpd" + echo "[project]" > pyproject.toml + mkdir -p src/sap_cloud_sdk + + KEEP_TMP=1 DRY_RUN=true LOCAL_DIFF="$FIXTURES/clean.diff" REPO_ROOT="$tmpd" TMPDIR_RUN=/tmp/orch-test \ + bash .claude/scripts/orchestrate.sh 999 --dry-run > /dev/null 2>&1 || true + [ -f /tmp/orch-test/summary.json ] + jq -e '.summary.block_count != null' /tmp/orch-test/summary.json + jq -e '.per_check_summary' /tmp/orch-test/summary.json + rm -rf "$tmpd" /tmp/orch-test +} + +# ============================================================ +# SPECIFIC CHECK EDGE CASES +# ============================================================ + +@test "check-secrets: does not fire on placeholder-looking test fixtures" { + # test data that looks like a key but is a test placeholder + cat > /tmp/test-fixture.diff <<'EOF' +diff --git a/tests/fixtures/token.txt b/tests/fixtures/token.txt +new file mode 100644 +index 0000000..abc +--- /dev/null ++++ b/tests/fixtures/token.txt +@@ -0,0 +1 @@ ++test-token-placeholder +EOF + DIFF_FILE=/tmp/test-fixture.diff bash "$SCRIPT_DIR/check-secrets.sh" < /tmp/test-fixture.diff > /tmp/out.json + # Should not fire on obvious placeholder + jq -e '.status == "PASS"' /tmp/out.json + rm -f /tmp/test-fixture.diff /tmp/out.json +} + +@test "check-hardcode: exempts localhost and example.com" { + cat > /tmp/local.diff <<'EOF' +diff --git a/src/sap_cloud_sdk/mymod/client.py b/src/sap_cloud_sdk/mymod/client.py +index 111..222 +--- a/src/sap_cloud_sdk/mymod/client.py ++++ b/src/sap_cloud_sdk/mymod/client.py +@@ -1,1 +1,2 @@ + x ++URL = "https://localhost:8080/api" +EOF + DIFF_FILE=/tmp/local.diff bash "$SCRIPT_DIR/check-hardcode.sh" < /tmp/local.diff > /tmp/out.json + # localhost is allowlisted + jq -e '[.findings[] | select(.rule == "HC-01")] | length == 0' /tmp/out.json + rm -f /tmp/local.diff /tmp/out.json +} + +@test "check-disclosure: PR body Closes #<issue_number> triggers DIS-07" { + pr_body=$(mktemp) + echo "Closes #<issue_number>" > "$pr_body" + cat > /tmp/empty.diff <<'EOF' +EOF + DIFF_FILE=/tmp/empty.diff PR_BODY_FILE="$pr_body" bash "$SCRIPT_DIR/check-disclosure.sh" < /tmp/empty.diff > /tmp/out.json + jq -e '.findings | map(.rule) | any(. == "DIS-07")' /tmp/out.json + rm -f "$pr_body" /tmp/empty.diff /tmp/out.json +} diff --git a/tests/sdk-review/test_smoke.bats b/tests/sdk-review/test_smoke.bats new file mode 100644 index 00000000..3eee774a --- /dev/null +++ b/tests/sdk-review/test_smoke.bats @@ -0,0 +1,274 @@ +#!/usr/bin/env bats +# smoke tests for the SDK Module Review skill + +setup() { + SCRIPT_DIR="$BATS_TEST_DIRNAME/../../.claude/scripts" + FIXTURES="$BATS_TEST_DIRNAME/fixtures" + export LANGUAGE=python + export REPO_ROOT="$BATS_TEST_DIRNAME/../.." + export CONFIG_DIR="$REPO_ROOT/.claude/config" +} + +# --- LIBS --- + +@test "lib/json-emit.sh: emit_finding produces valid JSON" { + source "$SCRIPT_DIR/lib/json-emit.sh" + result=$(emit_finding "TEST-01" "BLOCK" "src/x.py" 42 "test message") + echo "$result" | jq -e '.rule == "TEST-01"' + echo "$result" | jq -e '.severity == "BLOCK"' + echo "$result" | jq -e '.line == 42' +} + +@test "lib/json-emit.sh: emit_report assembles findings correctly" { + source "$SCRIPT_DIR/lib/json-emit.sh" + result=$(printf '{"rule":"A","severity":"BLOCK","file":"f","line":1,"message":"m","suggestion":""}\n{"rule":"B","severity":"FLAG","file":"g","line":2,"message":"m","suggestion":""}\n' | emit_report "test" "python" "BLOCK" "2026-01-01T00:00:00Z") + echo "$result" | jq -e '.check == "test"' + echo "$result" | jq -e '.findings | length == 2' + echo "$result" | jq -e '.summary.block_count == 1' + echo "$result" | jq -e '.summary.flag_count == 1' +} + +@test "lib/diff-added-lines.sh: extracts only added lines" { + cat > /tmp/test.diff <<'EOF' +diff --git a/src/a.py b/src/a.py +--- a/src/a.py ++++ b/src/a.py +@@ -1,3 +1,4 @@ + context1 ++added1 + context2 ++added2 +EOF + result=$(bash "$SCRIPT_DIR/lib/diff-added-lines.sh" < /tmp/test.diff) + echo "$result" | grep -q "src/a.py:2" + echo "$result" | grep -q "src/a.py:4" + ! echo "$result" | grep -q "context" +} + +@test "lib/detect-language.sh: detects Python from pyproject.toml" { + tmpd=$(mktemp -d) + echo "[project]" > "$tmpd/pyproject.toml" + mkdir -p "$tmpd/src/sap_cloud_sdk" + result=$(bash "$SCRIPT_DIR/lib/detect-language.sh" "$tmpd") + [ "$result" = "python" ] + rm -rf "$tmpd" +} + +@test "lib/predicates.sh: reuse_toml_aggregate_present returns true when REUSE.toml aggregate exists" { + source "$SCRIPT_DIR/lib/predicates.sh" + tmpd=$(mktemp -d) + cat > "$tmpd/REUSE.toml" <<'EOF' +version = 1 +[[annotations]] +path = "**" +SPDX-License-Identifier = "Apache-2.0" +EOF + result=$(reuse_toml_aggregate_present "$tmpd") + [ "$result" = "true" ] + rm -rf "$tmpd" +} + +@test "lib/predicates.sh: reuse_toml_aggregate_present false when no REUSE.toml" { + source "$SCRIPT_DIR/lib/predicates.sh" + tmpd=$(mktemp -d) + result=$(reuse_toml_aggregate_present "$tmpd") + [ "$result" = "false" ] + rm -rf "$tmpd" +} + +@test "lib/suppression.sh: parse_line detects ignore[hardcode]" { + tmp=$(mktemp) + cat > "$tmp" <<'EOF' +line1 +timeout = 30 # sdk-review: ignore[hardcode] +line3 +EOF + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" parse_line "$tmp") + echo "$result" | grep -q ":2:hardcode" + rm -f "$tmp" +} + +@test "lib/suppression.sh: locked rules refuse suppression" { + tmp=$(mktemp) + echo "$tmp:1:SEC-01" > "$tmp.supp" + result=$(bash "$SCRIPT_DIR/lib/suppression.sh" is_suppressed "SEC-01" "$tmp" 1 "$tmp.supp") + [ "$result" = "false" ] + rm -f "$tmp" "$tmp.supp" +} + +# --- CHECKS --- + +@test "check-secrets: fires BLOCK on AWS access key" { + DIFF_FILE="$FIXTURES/secrets-aws-key.diff" bash "$SCRIPT_DIR/check-secrets.sh" < "$FIXTURES/secrets-aws-key.diff" > /tmp/out.json + jq -e '.status == "BLOCK"' /tmp/out.json + jq -e '.findings[0].rule == "SEC-01"' /tmp/out.json +} + +@test "check-secrets: fires BLOCK on JWT token" { + DIFF_FILE="$FIXTURES/secrets-jwt.diff" bash "$SCRIPT_DIR/check-secrets.sh" < "$FIXTURES/secrets-jwt.diff" > /tmp/out.json + jq -e '.status == "BLOCK"' /tmp/out.json + jq -e '.findings | map(.rule) | any(. == "SEC-06")' /tmp/out.json +} + +@test "check-secrets: PASS on clean diff" { + DIFF_FILE="$FIXTURES/clean.diff" bash "$SCRIPT_DIR/check-secrets.sh" < "$FIXTURES/clean.diff" > /tmp/out.json + jq -e '.status == "PASS"' /tmp/out.json +} + +@test "check-hardcode: fires BLOCK on hardcoded URL" { + DIFF_FILE="$FIXTURES/hardcode-url.diff" bash "$SCRIPT_DIR/check-hardcode.sh" < "$FIXTURES/hardcode-url.diff" > /tmp/out.json + jq -e '.findings | length > 0' /tmp/out.json + jq -e '.findings | map(.rule) | any(. == "HC-01")' /tmp/out.json +} + +@test "check-disclosure: fires on SAP-internal URL (public profile)" { + DISCLOSURE_PROFILE=public DIFF_FILE="$FIXTURES/disclosure-internal.diff" bash "$SCRIPT_DIR/check-disclosure.sh" < "$FIXTURES/disclosure-internal.diff" > /tmp/out.json + jq -e '.findings | map(.rule) | any(. == "DIS-01")' /tmp/out.json + jq -e '.findings[0].severity == "BLOCK"' /tmp/out.json +} + +@test "check-disclosure: internal profile downgrades DIS-01 to FLAG" { + DISCLOSURE_PROFILE=internal DIFF_FILE="$FIXTURES/disclosure-internal.diff" bash "$SCRIPT_DIR/check-disclosure.sh" < "$FIXTURES/disclosure-internal.diff" > /tmp/out.json + jq -e '.findings[0].severity == "FLAG"' /tmp/out.json +} + +@test "check-license-spdx: REUSE.toml aggregate exempts LIC-01" { + tmpd=$(mktemp -d) + cat > "$tmpd/REUSE.toml" <<'EOF' +version = 1 +[[annotations]] +path = "**" +SPDX-License-Identifier = "Apache-2.0" +EOF + REPO_ROOT="$tmpd" DIFF_FILE="$FIXTURES/secrets-aws-key.diff" bash "$SCRIPT_DIR/check-license-spdx.sh" < "$FIXTURES/secrets-aws-key.diff" > /tmp/out.json + jq -e '.status == "PASS"' /tmp/out.json + rm -rf "$tmpd" +} + +@test "check-license-spdx: fires BLOCK on new .py without SPDX header (no REUSE.toml)" { + cat > /tmp/no-spdx.diff <<'EOF' +diff --git a/src/new.py b/src/new.py +new file mode 100644 +index 0000000..abc +--- /dev/null ++++ b/src/new.py +@@ -0,0 +1,2 @@ ++def foo(): ++ pass +EOF + tmpd=$(mktemp -d) + REPO_ROOT="$tmpd" DIFF_FILE=/tmp/no-spdx.diff bash "$SCRIPT_DIR/check-license-spdx.sh" < /tmp/no-spdx.diff > /tmp/out.json + jq -e '.status == "BLOCK"' /tmp/out.json + jq -e '.findings | map(.rule) | any(. == "LIC-01")' /tmp/out.json + rm -rf "$tmpd" +} + +@test "check-binding-shape: fires BLOCK_LOCKED on /oauth/token concat" { + LANGUAGE=java DIFF_FILE="$FIXTURES/binding-token-concat.diff" bash "$SCRIPT_DIR/check-binding-shape.sh" < "$FIXTURES/binding-token-concat.diff" > /tmp/out.json + jq -e '.status == "BLOCK"' /tmp/out.json + jq -e '.findings | map(.rule) | any(. == "BND-02")' /tmp/out.json +} + +# --- AST checks --- + +@test "ast-python-checks: EL-01 fires on raising different exception without from e" { + cat > /tmp/bad.py <<'EOF' +def x(): + try: + pass + except ValueError as e: + raise KeyError("nope") +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-01 /tmp/bad.py) + echo "$result" | jq -e '.rule == "PY-EL-01"' + rm -f /tmp/bad.py +} + +@test "ast-python-checks: EL-01 PASSES on bare raise" { + cat > /tmp/good.py <<'EOF' +def x(): + try: + pass + except ValueError: + raise +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-01 /tmp/good.py) + [ -z "$result" ] + rm -f /tmp/good.py +} + +@test "ast-python-checks: EL-01 PASSES on raise X from e" { + cat > /tmp/good.py <<'EOF' +def x(): + try: + pass + except ValueError as e: + raise KeyError("wrapped") from e +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-01 /tmp/good.py) + [ -z "$result" ] + rm -f /tmp/good.py +} + +@test "ast-python-checks: TEL-02 fires on public *Client method without @record_metrics" { + cat > /tmp/client.py <<'EOF' +class MyClient: + def do_thing(self): + pass +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" tel-02 /tmp/client.py) + echo "$result" | jq -e '.rule == "PY-TEL-02"' + rm -f /tmp/client.py +} + +@test "ast-python-checks: TEL-02 PASSES when @record_metrics present" { + cat > /tmp/client.py <<'EOF' +class MyClient: + @record_metrics(Module.X, Operation.Y) + def do_thing(self): + pass +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" tel-02 /tmp/client.py) + [ -z "$result" ] + rm -f /tmp/client.py +} + +@test "ast-python-checks: CON-01 fires on 3× repeated string" { + cat > /tmp/const.py <<'EOF' +def x(): + a = "com.sap.adm.DocumentService" + b = "com.sap.adm.DocumentService" + c = "com.sap.adm.DocumentService" +EOF + result=$(python3 "$SCRIPT_DIR/lib/ast_python_checks.py" con-01 /tmp/const.py) + echo "$result" | jq -e '.rule == "PY-CON-01"' + rm -f /tmp/const.py +} + +# --- Full orchestrator dry-run --- + +@test "orchestrate.sh: dry-run against secrets fixture exits 1 (BLOCK)" { + tmpd=$(mktemp -d) + cp -r "$REPO_ROOT/.claude" "$tmpd/" + cd "$tmpd" + echo "[project]" > pyproject.toml + mkdir -p src/sap_cloud_sdk + set +e + DRY_RUN=true LOCAL_DIFF="$FIXTURES/secrets-aws-key.diff" REPO_ROOT="$tmpd" \ + bash .claude/scripts/orchestrate.sh 999 --dry-run > /dev/null 2>&1 + status=$? + set -e + [ "$status" -eq 1 ] + rm -rf "$tmpd" +} + +@test "orchestrate.sh: dry-run against clean fixture exits 0" { + tmpd=$(mktemp -d) + cp -r "$REPO_ROOT/.claude" "$tmpd/" + cd "$tmpd" + echo "[project]" > pyproject.toml + mkdir -p src/sap_cloud_sdk + DRY_RUN=true LOCAL_DIFF="$FIXTURES/clean.diff" REPO_ROOT="$tmpd" \ + bash .claude/scripts/orchestrate.sh 999 --dry-run + rm -rf "$tmpd" +} diff --git a/uv.lock b/uv.lock index be9e3cfa..4a93df76 100644 --- a/uv.lock +++ b/uv.lock @@ -3713,7 +3713,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.32.0" +version = "0.32.1" source = { editable = "." } dependencies = [ { name = "grpcio" },