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:
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "8.0.11",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+168
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Custom Jenkins image for Powder Coating Logix production deployments.
|
||||||
|
# Adds: .NET 8 SDK, Azure CLI, sqlcmd (mssql-tools18), dotnet-ef global tool.
|
||||||
|
#
|
||||||
|
# Build: docker build -t pcl-jenkins ./jenkins
|
||||||
|
# Run: docker run -d -p 8080:8080 -p 50000:50000 \
|
||||||
|
# -v jenkins_home:/var/jenkins_home \
|
||||||
|
# --name pcl-jenkins pcl-jenkins
|
||||||
|
|
||||||
|
FROM jenkins/jenkins:lts
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# ── Base utilities ────────────────────────────────────────────────────────────
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
wget curl gnupg2 apt-transport-https lsb-release zip unzip ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── .NET 8 SDK ────────────────────────────────────────────────────────────────
|
||||||
|
RUN wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \
|
||||||
|
-O /tmp/ms-prod.deb \
|
||||||
|
&& dpkg -i /tmp/ms-prod.deb \
|
||||||
|
&& rm /tmp/ms-prod.deb \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends dotnet-sdk-8.0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── Azure CLI ─────────────────────────────────────────────────────────────────
|
||||||
|
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── mssql-tools18 (sqlcmd) ───────────────────────────────────────────────────
|
||||||
|
RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
|
||||||
|
| gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/microsoft-prod.gpg] \
|
||||||
|
https://packages.microsoft.com/debian/12/prod bookworm main" \
|
||||||
|
> /etc/apt/sources.list.d/mssql-release.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& ACCEPT_EULA=Y apt-get install -y --no-install-recommends \
|
||||||
|
mssql-tools18 unixodbc-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH="$PATH:/opt/mssql-tools18/bin"
|
||||||
|
|
||||||
|
# ── dotnet-ef global tool ─────────────────────────────────────────────────────
|
||||||
|
# Installed into /root/.dotnet/tools (not JENKINS_HOME, which is a volume mount
|
||||||
|
# and would be wiped on first run). A symlink exposes it system-wide.
|
||||||
|
RUN DOTNET_CLI_HOME=/root dotnet tool install --global dotnet-ef --version 8.0.11 \
|
||||||
|
&& ln -s /root/.dotnet/tools/dotnet-ef /usr/local/bin/dotnet-ef
|
||||||
|
|
||||||
|
USER jenkins
|
||||||
Reference in New Issue
Block a user