Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 345 additions & 0 deletions .github/workflows/quality-gate.yml
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 18 additions & 4 deletions .github/workflows/validate-schemas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check settings so that it fist on one line

-s src/main/resources/schemas/capability-schema.json \
-d "$file" \
--spec=draft2020 \
--strict-schema=false
done
done

7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
Loading
Loading