diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..ce6c593
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,191 @@
+# ── Table2Taste CI/CD Pipeline ─────────────────────────
+# Builds, tests, runs SonarQube analysis, and pushes Docker images
+#
+# Jenkins instance: jenkins.cabrasky.net (node1)
+# SonarQube: sonar.cabrasky.net
+# Registry: 192.168.1.11:5000
+#
+# ⚠️ Jenkins does NOT have pipeline-utility-steps plugin
+# → Use python3 instead of readJSON
+# ⚠️ 'when { not { ... } }' does NOT work
+# → Use 'when { expression { return !... } }'
+
+pipeline {
+ agent any
+
+ environment {
+ REGISTRY = "192.168.1.11:5000"
+ BACKEND_IMAGE = "${REGISTRY}/table2taste-backend"
+ FRONTEND_IMAGE = "${REGISTRY}/table2taste-frontend"
+ SONAR_TOKEN = credentials('sonarqube-token')
+ SONAR_HOST_URL = "https://sonar.cabrasky.net"
+ }
+
+ parameters {
+ booleanParam(
+ name: 'SKIP_TESTS',
+ defaultValue: false,
+ description: 'Skip tests and Sonar analysis (build only)'
+ )
+ booleanParam(
+ name: 'SKIP_DOCKER',
+ defaultValue: false,
+ description: 'Skip Docker build & push'
+ )
+ }
+
+ stages {
+
+ stage('Checkout') {
+ steps {
+ checkout scm
+ }
+ }
+
+ stage('Read Version') {
+ steps {
+ script {
+ def pkg = sh(
+ script: "python3 -c 'import json; print(json.load(open(\\\"packages/frontend/package.json\\\"))[\\\"version\\\"])'",
+ returnStdout: true
+ ).trim()
+ def gitSha = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
+ env.APP_VERSION = pkg
+ env.IMAGE_TAG = "${pkg}-${gitSha}"
+ echo "Version: ${env.APP_VERSION} (tag: ${env.IMAGE_TAG})"
+ }
+ }
+ }
+
+ stage('Check Image Exists') {
+ when {
+ expression { return !params.SKIP_DOCKER }
+ }
+ steps {
+ script {
+ def skips = []
+ for (img in ['table2taste-backend', 'table2taste-frontend']) {
+ def exists = sh(
+ script: "curl -s -o /dev/null -w '%{http_code}' http://${REGISTRY}/v2/${img}/manifests/${env.IMAGE_TAG}",
+ returnStdout: true
+ ).trim() == "200"
+ if (exists) {
+ echo "✓ ${img}:${env.IMAGE_TAG} already exists — skipping build"
+ skips << img
+ }
+ }
+ if (skips.size() == 2) {
+ env.SKIP_DOCKER_BUILD = 'true'
+ }
+ }
+ }
+ }
+
+ // ── Backend Tests ──────────────────────────────
+ stage('Test Backend') {
+ when {
+ expression { return !params.SKIP_TESTS }
+ }
+ steps {
+ dir('packages/backend') {
+ sh 'chmod +x mvnw'
+ sh './mvnw test jacoco:report -q'
+ }
+ }
+ post {
+ always {
+ junit allowEmptyResults: true,
+ testResults: 'packages/backend/target/surefire-reports/*.xml'
+ }
+ }
+ }
+
+ // ── Frontend Build & Test ──────────────────────
+ stage('Test Frontend') {
+ when {
+ expression { return !params.SKIP_TESTS }
+ }
+ steps {
+ dir('packages/frontend') {
+ sh 'npm ci'
+ sh 'npm test -- --watchAll=false --ci --coverage 2>&1 || true'
+ sh 'npx react-scripts build 2>&1'
+ }
+ }
+ post {
+ always {
+ junit allowEmptyResults: true,
+ testResults: 'packages/frontend/junit.xml'
+ }
+ }
+ }
+
+ // ── SonarQube Analysis ─────────────────────────
+ stage('SonarQube Analysis') {
+ when {
+ expression { return !params.SKIP_TESTS }
+ }
+ steps {
+ dir('packages/backend') {
+ sh """
+ ./mvnw sonar:sonar \
+ -Dsonar.host.url=${SONAR_HOST_URL} \
+ -Dsonar.token=${SONAR_TOKEN} \
+ -Dsonar.projectKey=table2taste \
+ -Dsonar.projectName='Table2Taste' \
+ -Dsonar.sources=src/main/java \
+ -Dsonar.tests=src/test/java \
+ -Dsonar.java.binaries=target/classes \
+ -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml \
+ -Dsonar.exclusions=**/model/dto/**
+ """
+ }
+ }
+ }
+
+ // ── Docker Build & Push ─────────────────────────
+ stage('Docker Build & Push') {
+ when {
+ expression { return !params.SKIP_DOCKER && env.SKIP_DOCKER_BUILD != 'true' }
+ }
+ parallel {
+ stage('Backend Image') {
+ steps {
+ dir('packages/backend') {
+ sh "docker build -t ${BACKEND_IMAGE}:${IMAGE_TAG} ."
+ sh "docker tag ${BACKEND_IMAGE}:${IMAGE_TAG} ${BACKEND_IMAGE}:latest"
+ sh "docker push ${BACKEND_IMAGE}:${IMAGE_TAG}"
+ sh "docker push ${BACKEND_IMAGE}:latest"
+ }
+ }
+ }
+ stage('Frontend Image') {
+ steps {
+ dir('packages/frontend') {
+ sh "docker build -t ${FRONTEND_IMAGE}:${IMAGE_TAG} ."
+ sh "docker tag ${FRONTEND_IMAGE}:${IMAGE_TAG} ${FRONTEND_IMAGE}:latest"
+ sh "docker push ${FRONTEND_IMAGE}:${IMAGE_TAG}"
+ sh "docker push ${FRONTEND_IMAGE}:latest"
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ post {
+ always {
+ sh 'docker image prune -f || true'
+ }
+ failure {
+ echo '❌ Pipeline failed — check logs'
+ }
+ success {
+ echo '✅ Pipeline completed successfully'
+ }
+ unstable {
+ echo '⚠️ Pipeline completed with unstable results'
+ }
+ }
+}
diff --git a/packages/backend/pom.xml b/packages/backend/pom.xml
index 0e025cb..52ec1b3 100644
--- a/packages/backend/pom.xml
+++ b/packages/backend/pom.xml
@@ -128,6 +128,35 @@
org.springframework.boot
spring-boot-maven-plugin
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.12
+
+
+
+ prepare-agent
+
+
+
+ report
+ verify
+
+ report
+
+
+
+
+
+
+
+ org.sonarsource.scanner.maven
+ sonar-maven-plugin
+ 5.1.0.14797
+
+
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..4fc0a60
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,33 @@
+# SonarQube project configuration
+sonar.projectKey=table2taste
+sonar.projectName=Table2Taste
+sonar.projectVersion=1.0.0
+
+# Sources
+sonar.sources=packages/backend/src/main/java,packages/frontend/src
+sonar.tests=packages/backend/src/test/java,packages/frontend/src
+
+# Language
+sonar.language=java,ts
+
+# Java
+sonar.java.binaries=packages/backend/target/classes
+sonar.java.test.binaries=packages/backend/target/test-classes
+sonar.java.libraries=packages/backend/target/dependency/*
+sonar.java.source=17
+
+# TypeScript / JavaScript
+sonar.javascript.lcov.reportPaths=packages/frontend/coverage/lcov.info
+
+# Test coverage
+sonar.coverage.jacoco.xmlReportPaths=packages/backend/target/site/jacoco/jacoco.xml
+
+# Exclusions
+sonar.exclusions=packages/frontend/src/**/*.test.tsx,packages/frontend/src/**/*.test.ts,packages/frontend/src/reportWebVitals.ts
+sonar.test.exclusions=packages/frontend/src/**/*.test.tsx,packages/frontend/src/**/*.test.ts
+
+# Encoding
+sonar.sourceEncoding=UTF-8
+
+# Docker
+sonar.docker.filePatterns=Dockerfile*,Dockerfile.*