Add production Jenkins pipeline for Azure App Service deployment

Fully manual pipeline (no triggers): build/test → publish → generate
idempotent EF migration SQL (archived as artifact) → apply to Azure SQL
via sqlcmd → ZIP deploy to App Service → smoke test.

Includes jenkins/Dockerfile (adds .NET 8 SDK, Azure CLI, mssql-tools18,
dotnet-ef 8.0.11 to jenkins/jenkins:lts) and .config/dotnet-tools.json
tool manifest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 09:50:52 -04:00
parent 00bf8a4cd0
commit 9361cd4495
3 changed files with 230 additions and 0 deletions
Vendored
+168
View File
@@ -0,0 +1,168 @@
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()
}
}
}