diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml
new file mode 100644
index 0000000..fdd88fb
--- /dev/null
+++ b/.github/workflows/quality-gate.yml
@@ -0,0 +1,345 @@
+name: Quality Gate
+
+on:
+ push:
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+jobs:
+ test-and-analyze:
+ name: Unit Tests + JaCoCo + SonarQube + Security
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Init summary
+ run: |
+ echo "# PR Quality Report — $(date '+%Y-%m-%d %H:%M') UTC" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Tool | Description |" >> $GITHUB_STEP_SUMMARY
+ echo "|------|-------------|" >> $GITHUB_STEP_SUMMARY
+ echo "| JaCoCo | Unit test coverage |" >> $GITHUB_STEP_SUMMARY
+ echo "| SonarQube | Code quality & bugs |" >> $GITHUB_STEP_SUMMARY
+ echo "| Trivy | CVE vulnerabilities & secrets |" >> $GITHUB_STEP_SUMMARY
+ echo "| Gitleaks | Secrets in git history |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "---" >> $GITHUB_STEP_SUMMARY
+
+ - name: Run tests + JaCoCo coverage
+ run: mvn clean test --no-transfer-progress
+
+ - name: Publish JaCoCo summary
+ if: always()
+ run: |
+ CSV="target/site/jacoco/jacoco.csv"
+ {
+ echo ""
+ echo "## JaCoCo Coverage"
+ echo ""
+ if [ ! -f "$CSV" ]; then
+ echo "> No coverage report found."
+ else
+ MISSED_LINES=$(tail -n +2 "$CSV" | awk -F',' '{sum+=$8} END {print sum+0}')
+ COVERED_LINES=$(tail -n +2 "$CSV" | awk -F',' '{sum+=$9} END {print sum+0}')
+ TOTAL=$((MISSED_LINES + COVERED_LINES))
+ PCT=$([ "$TOTAL" -gt 0 ] && awk "BEGIN {printf \"%.1f\", ($COVERED_LINES/$TOTAL)*100}" || echo "0.0")
+ MISSED_B=$(tail -n +2 "$CSV" | awk -F',' '{sum+=$6} END {print sum+0}')
+ COVERED_B=$(tail -n +2 "$CSV" | awk -F',' '{sum+=$7} END {print sum+0}')
+ TOTAL_B=$((MISSED_B + COVERED_B))
+ PCT_B=$([ "$TOTAL_B" -gt 0 ] && awk "BEGIN {printf \"%.1f\", ($COVERED_B/$TOTAL_B)*100}" || echo "0.0")
+ echo "| Metric | Value |"
+ echo "|--------|-------|"
+ echo "| Line Coverage | **${PCT}%** (${COVERED_LINES} / ${TOTAL}) |"
+ echo "| Branch Coverage | **${PCT_B}%** (${COVERED_B} / ${TOTAL_B}) |"
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Upload JaCoCo report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: jacoco-report
+ path: target/site/jacoco/
+ retention-days: 14
+
+ - name: Start SonarQube
+ run: |
+ docker run -d --name sonarqube \
+ -p 9000:9000 \
+ -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
+ sonarqube:community
+
+ - name: Wait for SonarQube
+ run: |
+ for i in $(seq 1 40); do
+ STATUS=$(curl -s http://localhost:9000/api/system/status 2>/dev/null \
+ | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
+ echo "Attempt $i — status: $STATUS"
+ [ "$STATUS" = "UP" ] && echo "SonarQube is UP!" && break
+ sleep 10 # Wait before retry to allow startup completion
+ done
+
+ - name: Generate SonarQube token
+ run: |
+ curl -s -u admin:admin -X POST \
+ "http://localhost:9000/api/user_tokens/generate" \
+ -d "name=ci-token&type=GLOBAL_ANALYSIS_TOKEN" \
+ | grep -o '"token":"[^"]*"' | cut -d'"' -f4 > /tmp/sonar_token.txt
+
+ - name: SonarQube Scan
+ run: |
+ SONAR_TOKEN=$(cat /tmp/sonar_token.txt | tr -d '[:space:]')
+ docker run --rm \
+ --network="host" \
+ -e SONAR_TOKEN="$SONAR_TOKEN" \
+ -v "${{ github.workspace }}:/usr/src" \
+ sonarsource/sonar-scanner-cli \
+ -Dsonar.projectKey=io.naftiko:framework \
+ -Dsonar.sources=src/main/java \
+ -Dsonar.java.binaries=target/classes \
+ -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml \
+ -Dsonar.host.url=http://localhost:9000
+
+ - name: Export + Publish SonarQube summary
+ if: always()
+ run: |
+ # Wait 15s for SonarQube to process the analysis results
+ sleep 15
+ mkdir -p sonar-report
+ AUTH="admin:admin"
+ PROJECT="io.naftiko:framework"
+ BASE="http://localhost:9000"
+
+ curl -s -u $AUTH "$BASE/api/measures/component?component=$PROJECT&metricKeys=bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density,ncloc" -o sonar-report/measures.json
+ curl -s -u $AUTH "$BASE/api/qualitygates/project_status?projectKey=$PROJECT" -o sonar-report/quality-gate.json
+ curl -s -u $AUTH "$BASE/api/issues/search?componentKeys=$PROJECT&ps=500" -o sonar-report/issues.json
+
+ echo "=== measures.json ===" && cat sonar-report/measures.json
+ echo "=== quality-gate.json ===" && cat sonar-report/quality-gate.json
+
+ QG=$(python3 -c "import json; d=json.load(open('sonar-report/quality-gate.json')); print(d['projectStatus']['status'])" 2>/dev/null || echo "NONE")
+ BUGS=$(python3 -c "import json; d=json.load(open('sonar-report/measures.json')); print(next((m['value'] for m in d['component']['measures'] if m['metric']=='bugs'), '0'))" 2>/dev/null || echo "0")
+ VULNS=$(python3 -c "import json; d=json.load(open('sonar-report/measures.json')); print(next((m['value'] for m in d['component']['measures'] if m['metric']=='vulnerabilities'), '0'))" 2>/dev/null || echo "0")
+ SMELLS=$(python3 -c "import json; d=json.load(open('sonar-report/measures.json')); print(next((m['value'] for m in d['component']['measures'] if m['metric']=='code_smells'), '0'))" 2>/dev/null || echo "0")
+ COVERAGE=$(python3 -c "import json; d=json.load(open('sonar-report/measures.json')); print(next((m['value'] for m in d['component']['measures'] if m['metric']=='coverage'), 'N/A'))" 2>/dev/null || echo "N/A")
+ DUPL=$(python3 -c "import json; d=json.load(open('sonar-report/measures.json')); print(next((m['value'] for m in d['component']['measures'] if m['metric']=='duplicated_lines_density'), 'N/A'))" 2>/dev/null || echo "N/A")
+ NCLOC=$(python3 -c "import json; d=json.load(open('sonar-report/measures.json')); print(next((m['value'] for m in d['component']['measures'] if m['metric']=='ncloc'), 'N/A'))" 2>/dev/null || echo "N/A")
+ echo "Parsed: QG=$QG BUGS=$BUGS VULNS=$VULNS SMELLS=$SMELLS COVERAGE=$COVERAGE"
+
+ [ "$QG" = "OK" ] && QG_BADGE="✅ PASSED" || QG_BADGE="❌ FAILED"
+
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "## SonarQube Analysis" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**Quality Gate: ${QG_BADGE}**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
+ echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Bugs | ${BUGS:-0} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Vulnerabilities | ${VULNS:-0} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Code Smells | ${SMELLS:-0} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Coverage | ${COVERAGE:-N/A}% |" >> $GITHUB_STEP_SUMMARY
+ echo "| Duplications | ${DUPL:-N/A}% |" >> $GITHUB_STEP_SUMMARY
+ echo "| Lines of Code | ${NCLOC:-N/A} |" >> $GITHUB_STEP_SUMMARY
+
+ cat > /tmp/sonar_metrics.env << EOF
+ QG=$QG
+ BUGS=$BUGS
+ VULNS=$VULNS
+ SMELLS=$SMELLS
+ EOF
+
+ - name: Upload SonarQube report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: sonarqube-report
+ path: sonar-report/
+ retention-days: 14
+
+ - name: Run Trivy scan
+ run: |
+ docker run --rm \
+ -v "${{ github.workspace }}:/project" \
+ aquasec/trivy:latest fs /project \
+ --scanners vuln,secret,misconfig \
+ --format json \
+ --output /project/trivy-report.json \
+ --exit-code 0
+
+ - name: Publish Trivy summary
+ if: always()
+ run: |
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "## Trivy Security Scan" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ if [ ! -f trivy-report.json ]; then
+ echo "> No report found." >> $GITHUB_STEP_SUMMARY
+ cat > /tmp/trivy_metrics.env << EOF
+ TRIVY_CRITICAL=0
+ TRIVY_HIGH=0
+ TRIVY_MEDIUM=0
+ TRIVY_LOW=0
+ EOF
+ exit 0
+ fi
+
+ CRITICAL=$(grep -o '"Severity":"CRITICAL"' trivy-report.json | wc -l | tr -d ' ')
+ HIGH=$(grep -o '"Severity":"HIGH"' trivy-report.json | wc -l | tr -d ' ')
+ MEDIUM=$(grep -o '"Severity":"MEDIUM"' trivy-report.json | wc -l | tr -d ' ')
+ LOW=$(grep -o '"Severity":"LOW"' trivy-report.json | wc -l | tr -d ' ')
+
+ [ "$CRITICAL" -gt 0 ] && STATUS="❌ ${CRITICAL} critical issue(s)" \
+ || { [ "$HIGH" -gt 0 ] && STATUS="⚠️ ${HIGH} high issue(s)" || STATUS="✅ No critical or high issues"; }
+
+ echo "**${STATUS}**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
+ echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Critical | ${CRITICAL} |" >> $GITHUB_STEP_SUMMARY
+ echo "| High | ${HIGH} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Medium | ${MEDIUM} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Low | ${LOW} |" >> $GITHUB_STEP_SUMMARY
+
+ cat > /tmp/trivy_metrics.env << EOF
+ TRIVY_CRITICAL=$CRITICAL
+ TRIVY_HIGH=$HIGH
+ TRIVY_MEDIUM=$MEDIUM
+ TRIVY_LOW=$LOW
+ EOF
+
+ - name: Upload Trivy report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: trivy-report
+ path: trivy-report.json
+ retention-days: 14
+
+ - name: Run Gitleaks scan
+ run: |
+ docker run --rm \
+ -v "${{ github.workspace }}:/path" \
+ zricethezav/gitleaks:latest detect \
+ --source /path \
+ --gitleaks-ignore-path /path/.gitleaksignore \
+ --report-format json \
+ --report-path /path/gitleaks-report.json \
+ --exit-code 0
+
+ - name: Publish Gitleaks summary
+ if: always()
+ run: |
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "## Gitleaks Secret Scan" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ if [ ! -f gitleaks-report.json ]; then
+ echo "✅ No secrets detected." >> $GITHUB_STEP_SUMMARY
+ echo "LEAKS_COUNT=0" > /tmp/gitleaks_metrics.env
+ exit 0
+ fi
+
+ COUNT=$(grep -o '"RuleID"' gitleaks-report.json | wc -l | tr -d ' ')
+
+ if [ "$COUNT" -eq 0 ]; then
+ echo "✅ No secrets detected." >> $GITHUB_STEP_SUMMARY
+ else
+ echo "❌ **${COUNT} secret(s) detected in git history!**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Rule | File | Commit |" >> $GITHUB_STEP_SUMMARY
+ echo "|------|------|--------|" >> $GITHUB_STEP_SUMMARY
+ python3 -c "
+ import json, sys
+ with open('gitleaks-report.json') as f:
+ data = json.load(f)
+ if isinstance(data, list):
+ for item in data[:10]:
+ rule = item.get('RuleID', 'N/A')
+ file = item.get('File', 'N/A')
+ commit = item.get('Commit', 'N/A')[:8]
+ print(f'| {rule} | {file} | {commit} |')
+ " >> $GITHUB_STEP_SUMMARY
+ fi
+
+ echo "LEAKS_COUNT=$COUNT" > /tmp/gitleaks_metrics.env
+
+ - name: Upload Gitleaks report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: gitleaks-report
+ path: gitleaks-report.json
+ retention-days: 14
+
+ - name: Publish badges to Gist
+ if: always()
+ env:
+ GIST_ID: 50bfcb34f6512cbad2dd4f460bfc6526
+ GIST_TOKEN: ${{ secrets.GIST_SECRET }}
+ run: |
+ source /tmp/sonar_metrics.env 2>/dev/null || true
+ source /tmp/trivy_metrics.env 2>/dev/null || true
+ source /tmp/gitleaks_metrics.env 2>/dev/null || true
+
+ # Load JaCoCo coverage
+ CSV="target/site/jacoco/jacoco.csv"
+ if [ -f "$CSV" ]; then
+ MISSED=$(tail -n +2 "$CSV" | awk -F',' '{sum+=$8} END {print sum+0}')
+ COVERED=$(tail -n +2 "$CSV" | awk -F',' '{sum+=$9} END {print sum+0}')
+ TOTAL=$((MISSED + COVERED))
+ COV_PCT=$([ "$TOTAL" -gt 0 ] && awk "BEGIN {printf \"%.0f\", ($COVERED/$TOTAL)*100}" || echo "0")
+ [ "$COV_PCT" -ge 80 ] && COV_COLOR="brightgreen" \
+ || { [ "$COV_PCT" -ge 50 ] && COV_COLOR="yellow" || COV_COLOR="red"; }
+ else
+ COV_PCT="N/A"; COV_COLOR="lightgrey"
+ fi
+
+ # Sonar Quality Gate badge
+ [ "${QG:-NONE}" = "OK" ] && QG_MSG="passed" && QG_COLOR="brightgreen" \
+ || QG_MSG="failed" && QG_COLOR="red"
+
+ # Sonar bugs badge
+ [ "${BUGS:-0}" = "0" ] && BUGS_COLOR="brightgreen" || BUGS_COLOR="red"
+
+ # Trivy badge
+ CRIT="${TRIVY_CRITICAL:-0}"; HIGH_CNT="${TRIVY_HIGH:-0}"
+ if [ "$CRIT" -gt 0 ]; then
+ TRIVY_MSG="${CRIT} critical"; TRIVY_COLOR="red"
+ elif [ "$HIGH_CNT" -gt 0 ]; then
+ TRIVY_MSG="${HIGH_CNT} high"; TRIVY_COLOR="orange"
+ else
+ TRIVY_MSG="clean"; TRIVY_COLOR="brightgreen"
+ fi
+
+ # Gitleaks badge
+ LC="${LEAKS_COUNT:-0}"
+ [ "$LC" -eq 0 ] && LEAKS_MSG="no secrets" && LEAKS_COLOR="brightgreen" \
+ || LEAKS_MSG="${LC} secrets" && LEAKS_COLOR="red"
+
+ publish_badge() {
+ local filename="$1" label="$2" message="$3" color="$4"
+ local payload="{\"files\":{\"${filename}\":{\"content\":\"{\\\"schemaVersion\\\":1,\\\"label\\\":\\\"${label}\\\",\\\"message\\\":\\\"${message}\\\",\\\"color\\\":\\\"${color}\\\"}\"}}}"
+ curl -s -X PATCH \
+ -H "Authorization: token ${GIST_TOKEN}" \
+ -H "Content-Type: application/json" \
+ "https://api.github.com/gists/${GIST_ID}" \
+ -d "$payload" > /dev/null
+ echo "Published: $filename → $message ($color)"
+ }
+
+ publish_badge "framework-coverage.json" "coverage" "${COV_PCT}%" "$COV_COLOR"
+ publish_badge "framework-quality-gate.json" "quality gate" "$QG_MSG" "$QG_COLOR"
+ publish_badge "framework-bugs.json" "bugs" "${BUGS:-0}" "$BUGS_COLOR"
+ publish_badge "framework-trivy.json" "trivy" "$TRIVY_MSG" "$TRIVY_COLOR"
+ publish_badge "framework-gitleaks.json" "gitleaks" "$LEAKS_MSG" "$LEAKS_COLOR"
\ No newline at end of file
diff --git a/.github/workflows/validate-schemas.yml b/.github/workflows/validate-schemas.yml
index f0bf8b4..21ad155 100644
--- a/.github/workflows/validate-schemas.yml
+++ b/.github/workflows/validate-schemas.yml
@@ -4,9 +4,11 @@ on:
push:
paths:
- 'src/main/resources/schemas/**'
+ - 'src/test/resources/**'
pull_request:
paths:
- 'src/main/resources/schemas/**'
+ - 'src/test/resources/**'
workflow_dispatch:
jobs:
@@ -33,9 +35,21 @@ jobs:
- name: Validate YAML examples
working-directory: ${{ github.workspace }}
run: |
- for file in src/main/resources/schemas/{tutorial,examples}/*.{yaml,yml}; do
- [ -f "$file" ] || continue
- echo "Validating $file..."
- npx --prefix .github ajv validate -s src/main/resources/schemas/capability-schema.json -d "$file" --spec=draft2020 --strict-schema=false
+ PATHS=(
+ "src/main/resources/schemas/tutorial"
+ "src/main/resources/schemas/examples"
+ "src/test/resources"
+ )
+
+ for dir in "${PATHS[@]}"; do
+ for file in "$dir"/*.{yaml,yml}; do
+ [ -f "$file" ] || continue
+ echo "Validating $file..."
+ npx --prefix .github ajv validate \
+ -s src/main/resources/schemas/capability-schema.json \
+ -d "$file" \
+ --spec=draft2020 \
+ --strict-schema=false
+ done
done
diff --git a/README.md b/README.md
index 6f1a66d..b148d54 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
# Naftiko Framework
-Welcome to Naftiko Framework, the first Open Source project for **Spec-Driven AI Integration**. It reinvents API integration for the AI era with a governed and versatile platform based on capabilities that streamlines API sprawl created by the massive SaaS growth and microservices.
+
+
+
+
+
+Welcome to Naftiko Framework, the first Open Source project for **Spec-Driven Integration**. It reinvents API integration for the AI era with a governed and versatile platform based on capabilities that streamlines API sprawl created by the massive SaaS growth and microservices.
Each capability is a coarse piece of domain that consumes existing HTTP-based APIs, converts into JSON data formats like Protocol Buffer, XML, YAML, CSV and Avro, enabling better Context Engineering and API reusability critical to AI integration.
diff --git a/gitleaksignore b/gitleaksignore
new file mode 100644
index 0000000..647e5ae
--- /dev/null
+++ b/gitleaksignore
@@ -0,0 +1,2 @@
+# Ignore CI workflow files — admin:admin is a local ephemeral SonarQube instance
+[".github/workflows"]
diff --git a/pom.xml b/pom.xml
index 72ce6f4..9d1e744 100644
--- a/pom.xml
+++ b/pom.xml
@@ -128,6 +128,7 @@
json-structure
0.5.5
+
org.junit.jupiter
junit-jupiter
@@ -141,24 +142,51 @@
org.apache.maven.plugins
maven-compiler-plugin
+ 3.13.0
-
- info.picocli
- picocli-codegen
- 4.7.6
-
+
+ info.picocli
+ picocli-codegen
+ 4.7.6
+
- -Aproject=${project.groupId}/${project.artifactId}
+ -Aproject=${project.groupId}/${project.artifactId}
+
org.apache.maven.plugins
maven-surefire-plugin
- 3.1.2
+ 3.2.5
+
+ false
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.11
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
org.apache.maven.plugins
maven-shade-plugin
@@ -177,7 +205,8 @@
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
io.naftiko.Capability
-
+
@@ -204,7 +233,8 @@
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
io.naftiko.Cli
-
+
@@ -222,7 +252,6 @@
-
@@ -254,4 +283,5 @@
+
\ No newline at end of file
diff --git a/scripts/pr-check-mac-linux.sh b/scripts/pr-check-mac-linux.sh
new file mode 100644
index 0000000..a83c360
--- /dev/null
+++ b/scripts/pr-check-mac-linux.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+# ============================================================
+# pr-check.sh — Run all checks before opening a PR
+# Compatible: Mac, Linux (apt/yum/dnf/brew)
+# ============================================================
+
+set -euo pipefail
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+PASS=0
+FAIL=0
+
+run_step() {
+ local label="$1"
+ shift
+ echo ""
+ echo "------------------------------------------------------------"
+ echo ">>> $label"
+ echo "------------------------------------------------------------"
+ if "$@"; then
+ echo -e "${GREEN}[PASS] $label${NC}"
+ PASS=$((PASS + 1))
+ else
+ echo -e "${RED}[FAIL] $label${NC}"
+ FAIL=$((FAIL + 1))
+ fi
+}
+
+install_hint() {
+ local tool="$1"
+ echo -e "${YELLOW}[SKIP] $tool not installed. Install it with:${NC}"
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ echo " brew install $tool"
+ elif command -v apt-get &>/dev/null; then
+ echo " sudo apt-get install -y $tool"
+ elif command -v dnf &>/dev/null; then
+ echo " sudo dnf install -y $tool"
+ elif command -v yum &>/dev/null; then
+ echo " sudo yum install -y $tool"
+ elif command -v brew &>/dev/null; then
+ echo " brew install $tool"
+ else
+ echo " Please install $tool manually: https://github.com/$tool"
+ fi
+}
+
+run_step "Maven clean + unit tests + JaCoCo" \
+ mvn clean test --no-transfer-progress
+
+if command -v trivy &>/dev/null; then
+ run_step "Trivy - vulnerabilities + secrets + misconfig" \
+ trivy fs . --scanners vuln,secret,misconfig --format table --exit-code 0
+ trivy fs . --scanners vuln,secret,misconfig --format table > trivy-report.txt 2>&1 || true
+ echo -e "${YELLOW}Report saved: trivy-report.txt${NC}"
+else
+ install_hint "trivy"
+ echo " Or visit: https://aquasecurity.github.io/trivy/latest/getting-started/installation/"
+fi
+
+if command -v gitleaks &>/dev/null; then
+ run_step "Gitleaks - secrets in git history" \
+ gitleaks detect --source . --report-format json --report-path gitleaks-report.json --exit-code 0
+ echo -e "${YELLOW}Report saved: gitleaks-report.json${NC}"
+else
+ install_hint "gitleaks"
+ echo " Or visit: https://github.com/gitleaks/gitleaks#installing"
+fi
+
+echo ""
+echo "============================================================"
+echo " SUMMARY"
+echo "============================================================"
+echo -e " ${GREEN}PASSED: $PASS${NC}"
+if [ $FAIL -gt 0 ]; then
+ echo -e " ${RED}FAILED: $FAIL${NC}"
+ echo ""
+ echo -e "${RED}Fix the issues above before opening a PR.${NC}"
+ exit 1
+else
+ echo -e " ${RED}FAILED: 0${NC}"
+ echo ""
+ echo -e "${GREEN}All checks passed. Ready to open a PR!${NC}"
+ exit 0
+fi
\ No newline at end of file
diff --git a/scripts/pr-check-wind.ps1 b/scripts/pr-check-wind.ps1
new file mode 100644
index 0000000..21731e4
--- /dev/null
+++ b/scripts/pr-check-wind.ps1
@@ -0,0 +1,69 @@
+# ============================================================
+# pr-check.ps1 — Run all checks before opening a PR (Windows)
+# Usage: .\scripts\pr-check.ps1
+# Requirements: maven, trivy, gitleaks (winget or choco)
+# ============================================================
+
+$PASS = 0
+$FAIL = 0
+
+function Run-Step {
+ param([string]$Label, [scriptblock]$Command)
+ Write-Host ""
+ Write-Host "------------------------------------------------------------"
+ Write-Host ">>> $Label"
+ Write-Host "------------------------------------------------------------"
+ try {
+ & $Command
+ if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq $null) {
+ Write-Host "[PASS] $Label" -ForegroundColor Green
+ $script:PASS++
+ } else {
+ Write-Host "[FAIL] $Label" -ForegroundColor Red
+ $script:FAIL++
+ }
+ } catch {
+ Write-Host "[FAIL] $Label — $_" -ForegroundColor Red
+ $script:FAIL++
+ }
+}
+
+Run-Step "Maven clean + unit tests + JaCoCo" {
+ mvn clean test --no-transfer-progress
+}
+
+if (Get-Command trivy -ErrorAction SilentlyContinue) {
+ Run-Step "Trivy - vulnerabilities + secrets + misconfig" {
+ trivy fs . --scanners vuln,secret,misconfig --format table --exit-code 0
+ }
+ trivy fs . --scanners vuln,secret,misconfig --format table 2>&1 | Out-File -FilePath trivy-report.txt
+ Write-Host "Report saved: trivy-report.txt" -ForegroundColor Yellow
+} else {
+ Write-Host "[SKIP] Trivy not installed — run: winget install aquasecurity.trivy" -ForegroundColor Yellow
+}
+
+if (Get-Command gitleaks -ErrorAction SilentlyContinue) {
+ Run-Step "Gitleaks - secrets in git history" {
+ gitleaks detect --source . --report-format json --report-path gitleaks-report.json --exit-code 0
+ }
+ Write-Host "Report saved: gitleaks-report.json" -ForegroundColor Yellow
+} else {
+ Write-Host "[SKIP] Gitleaks not installed — run: winget install zricethezav.gitleaks" -ForegroundColor Yellow
+}
+
+Write-Host ""
+Write-Host "============================================================"
+Write-Host " SUMMARY"
+Write-Host "============================================================"
+Write-Host " PASSED: $PASS" -ForegroundColor Green
+if ($FAIL -gt 0) {
+ Write-Host " FAILED: $FAIL" -ForegroundColor Red
+ Write-Host ""
+ Write-Host "Fix the issues above before opening a PR." -ForegroundColor Red
+ exit 1
+} else {
+ Write-Host " FAILED: $FAIL" -ForegroundColor Red
+ Write-Host ""
+ Write-Host "All checks passed. Ready to open a PR!" -ForegroundColor Green
+ exit 0
+}
\ No newline at end of file