pipeline { agent any // No triggers — start this pipeline manually from the Jenkins UI only. environment { DOTNET_CLI_HOME = '/tmp/dotnet_cli_home' WEB_PROJECT = 'src/PowderCoating.Web/PowderCoating.Web.csproj' INFRA_PROJECT = 'src/PowderCoating.Infrastructure/PowderCoating.Infrastructure.csproj' PUBLISH_DIR = "${WORKSPACE}/publish" DEPLOY_ZIP = "${WORKSPACE}/deploy_${BUILD_NUMBER}.zip" MIGRATION_SQL = "${WORKSPACE}/migration_${BUILD_NUMBER}.sql" } stages { stage('Checkout') { steps { checkout([ $class: 'GitSCM', branches: [[name: 'refs/heads/master']], userRemoteConfigs: scm.userRemoteConfigs ]) echo "Building commit: ${GIT_COMMIT}" } } stage('Build & Test') { steps { sh 'dotnet restore' sh 'dotnet build --no-restore -c Release' sh ''' dotnet test --no-build -c Release \ --logger "trx;LogFileName=results.trx" \ --results-directory TestResults ''' } post { always { junit testResults: 'TestResults/*.trx', allowEmptyResults: true } } } stage('Publish') { steps { sh """ dotnet publish '${WEB_PROJECT}' \ -c Release --no-build \ -o '${PUBLISH_DIR}' """ } } // Generates an idempotent SQL migration script (no live DB connection required). // The script checks which migrations have already been applied before running each one. stage('Generate Migration Script') { steps { sh """ dotnet ef migrations script \ --idempotent \ --output '${MIGRATION_SQL}' \ --project '${INFRA_PROJECT}' \ --startup-project '${WEB_PROJECT}' \ --context ApplicationDbContext \ --no-build """ archiveArtifacts artifacts: "migration_${BUILD_NUMBER}.sql", fingerprint: true echo "Migration script archived — review it in the Jenkins build artifacts before this pipeline runs next time." } } stage('Apply Migration to Azure SQL') { steps { withCredentials([ string(credentialsId: 'PCL_SQL_SERVER', variable: 'SQL_SERVER'), string(credentialsId: 'PCL_SQL_DATABASE', variable: 'SQL_DATABASE'), string(credentialsId: 'PCL_SQL_USER', variable: 'SQL_USER'), string(credentialsId: 'PCL_SQL_PASSWORD', variable: 'SQL_PASSWORD') ]) { sh ''' echo "Applying migration to ${SQL_SERVER}/${SQL_DATABASE} ..." /opt/mssql-tools18/bin/sqlcmd \ -S "${SQL_SERVER}" \ -d "${SQL_DATABASE}" \ -U "${SQL_USER}" \ -P "${SQL_PASSWORD}" \ -C \ -b \ -i "${MIGRATION_SQL}" echo "Migration applied successfully." ''' } } } stage('Deploy to Azure App Service') { steps { withCredentials([ string(credentialsId: 'PCL_AZURE_CLIENT_ID', variable: 'AZ_CLIENT_ID'), string(credentialsId: 'PCL_AZURE_CLIENT_SECRET', variable: 'AZ_CLIENT_SECRET'), string(credentialsId: 'PCL_AZURE_TENANT_ID', variable: 'AZ_TENANT_ID'), string(credentialsId: 'PCL_AZURE_SUBSCRIPTION_ID', variable: 'AZ_SUBSCRIPTION_ID'), string(credentialsId: 'PCL_AZURE_RESOURCE_GROUP', variable: 'AZ_RG'), string(credentialsId: 'PCL_AZURE_APP_NAME', variable: 'AZ_APP') ]) { sh ''' az login --service-principal \ --username "$AZ_CLIENT_ID" \ --password "$AZ_CLIENT_SECRET" \ --tenant "$AZ_TENANT_ID" \ --output none az account set --subscription "$AZ_SUBSCRIPTION_ID" echo "Packaging deployment artifact ..." cd "$PUBLISH_DIR" zip -r "$DEPLOY_ZIP" . echo "Pushing ZIP to ${AZ_APP} ..." az webapp deployment source config-zip \ --resource-group "$AZ_RG" \ --name "$AZ_APP" \ --src "$DEPLOY_ZIP" az logout echo "Deploy complete." ''' } } } stage('Smoke Test') { steps { withCredentials([ string(credentialsId: 'PCL_AZURE_APP_NAME', variable: 'AZ_APP') ]) { sh ''' APP_URL="https://${AZ_APP}.azurewebsites.net" echo "Smoke-testing ${APP_URL} ..." HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ --max-time 45 --retry 3 --retry-delay 10 \ "${APP_URL}") echo "HTTP status: ${HTTP_STATUS}" # 200 = OK, 302 = redirect to login (both are healthy) if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then echo "SMOKE TEST FAILED — got HTTP ${HTTP_STATUS}" exit 1 fi echo "Smoke test passed." ''' } } } } post { success { echo "Production deployment #${BUILD_NUMBER} (${GIT_COMMIT}) completed successfully." } failure { echo "Pipeline #${BUILD_NUMBER} FAILED — review the stage logs above." } always { cleanWs() } } }