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. +![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/farah-t-trigui/50bfcb34f6512cbad2dd4f460bfc6526/raw/framework-coverage.json) +![Bugs](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/farah-t-trigui/50bfcb34f6512cbad2dd4f460bfc6526/raw/framework-bugs.json) +![Trivy](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/farah-t-trigui/50bfcb34f6512cbad2dd4f460bfc6526/raw/framework-trivy.json) +![Gitleaks](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/farah-t-trigui/50bfcb34f6512cbad2dd4f460bfc6526/raw/framework-gitleaks.json) + +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