9361cd4495
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>
169 lines
6.6 KiB
Groovy
169 lines
6.6 KiB
Groovy
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()
|
|
}
|
|
}
|
|
}
|