Files
jizhi/Jenkinsfile.backend-docker
T

271 lines
12 KiB
Plaintext

pipeline {
agent {
node {
label '构建机1'
customWorkspace '/home/nanxunai/goujian/workspace/jizhang-backend-docker'
}
}
options {
timestamps()
disableConcurrentBuilds()
skipDefaultCheckout(true)
timeout(time: 120, unit: 'MINUTES')
buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '20'))
}
environment {
PATH = '/usr/local/bin:/usr/bin:/bin'
HTTP_PROXY = 'http://192.168.5.200:7890'
HTTPS_PROXY = 'http://192.168.5.200:7890'
http_proxy = 'http://192.168.5.200:7890'
https_proxy = 'http://192.168.5.200:7890'
NO_PROXY = '127.0.0.1,localhost,192.168.5.8,192.168.5.100,.nxsir.cn'
no_proxy = '127.0.0.1,localhost,192.168.5.8,192.168.5.100,.nxsir.cn'
OPENLIST_CREDENTIALS = 'openlist_key'
OPENLIST_BASE_URL = 'https://openlist.nxsir.cn'
OPENLIST_REMOTE_DIR = '/yidongpan/构建产物/jizhang/backend-docker/linux-amd64'
}
stages {
stage('Checkout') {
steps {
deleteDir()
checkout scm
}
}
stage('Metadata And Preflight') {
steps {
script {
env.SHORT_SHA = sh(script: 'git rev-parse --short=8 HEAD', returnStdout: true).trim()
env.FULL_SHA = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.BUILD_DATE = sh(script: 'date -u +%Y-%m-%dT%H:%M:%SZ', returnStdout: true).trim()
env.BACKEND_VERSION = sh(script: 'date -u +%Y%m%d-%H%M', returnStdout: true).trim()
env.IMMUTABLE_TAG = "${env.BACKEND_VERSION}-b${env.BUILD_NUMBER}-${env.SHORT_SHA}"
env.IMAGE_REF = "jizhi-backend:${env.IMMUTABLE_TAG}"
env.ARCHIVE_BASENAME = "JiZhi-Backend-${env.IMMUTABLE_TAG}-linux-amd64.tar.gz"
env.ARCHIVE_PATH = "${env.WORKSPACE}/artifacts/${env.ARCHIVE_BASENAME}"
env.MANIFEST_PATH = "${env.WORKSPACE}/artifacts/JiZhi-Backend-${env.IMMUTABLE_TAG}-manifest.json"
env.SMOKE_NETWORK = "jizhi-smoke-net-${env.BUILD_NUMBER}"
env.SMOKE_MYSQL = "jizhi-smoke-mysql-${env.BUILD_NUMBER}"
env.SMOKE_APP = "jizhi-smoke-app-${env.BUILD_NUMBER}"
env.DOCKER_CMD = sh(
script: '''
set +x
if docker version >/dev/null 2>&1; then
printf 'docker'
elif sudo -n docker version >/dev/null 2>&1; then
printf 'sudo -n docker'
else
echo 'Jenkins 用户无法访问 Docker;请加入 docker 组或配置免密 sudo docker。' >&2
exit 1
fi
''',
returnStdout: true,
).trim()
currentBuild.displayName = "#${env.BUILD_NUMBER} ${env.IMMUTABLE_TAG}"
currentBuild.description = 'linux/amd64 · Admin Web + ASP.NET API'
}
sh '''
set -euo pipefail
test "$(uname -m)" = x86_64
for command_name in git docker curl gzip sha256sum python3; do
command -v "$command_name" >/dev/null
done
$DOCKER_CMD version >/dev/null
$DOCKER_CMD buildx version
test -f Dockerfile.backend
test -f admin-web/package-lock.json
mkdir -p artifacts
available_kb=$(df -Pk "$WORKSPACE" | awk 'NR == 2 { print $4 }')
# Warm Docker layers consume disk but make incremental builds
# much smaller; keep 4 GiB free for image export and smoke tests.
test "$available_kb" -ge 4194304
'''
}
}
stage('Build linux-amd64 Image') {
steps {
sh '''
set -euo pipefail
for attempt in $(seq 1 6); do
if $DOCKER_CMD buildx build \
--platform linux/amd64 \
--load \
--file Dockerfile.backend \
--build-arg "BUILD_VERSION=$IMMUTABLE_TAG" \
--build-arg "VCS_REF=$FULL_SHA" \
--build-arg "BUILD_DATE=$BUILD_DATE" \
--tag "$IMAGE_REF" \
.; then
break
fi
if [ "$attempt" -ge 6 ]; then
exit 1
fi
sleep $((attempt * 10))
done
'''
sh '''
set -euo pipefail
test "$($DOCKER_CMD image inspect "$IMAGE_REF" --format '{{.Os}}/{{.Architecture}}')" = 'linux/amd64'
test "$($DOCKER_CMD image inspect "$IMAGE_REF" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$FULL_SHA"
$DOCKER_CMD run --rm --entrypoint /bin/sh "$IMAGE_REF" -c \
'test -s /app/MiaoJiZhang.Api.dll && test -s /app/wwwroot/index.html'
'''
}
}
stage('Smoke Test Image') {
steps {
sh '''
set -euo pipefail
$DOCKER_CMD network create "$SMOKE_NETWORK" >/dev/null
$DOCKER_CMD run --detach \
--name "$SMOKE_MYSQL" \
--network "$SMOKE_NETWORK" \
--env MYSQL_DATABASE=jizhi_smoke \
--env MYSQL_USER=jizhi \
--env MYSQL_PASSWORD=jizhi-smoke-password \
--env MYSQL_ROOT_PASSWORD=jizhi-smoke-root-password \
mysql:8.4 >/dev/null
mysql_ready=0
for attempt in $(seq 1 60); do
if $DOCKER_CMD exec "$SMOKE_MYSQL" mysqladmin ping \
--host=127.0.0.1 --protocol=tcp \
--user=jizhi --password=jizhi-smoke-password --silent; then
mysql_ready=1
break
fi
sleep 2
done
if [ "$mysql_ready" != 1 ]; then
$DOCKER_CMD logs "$SMOKE_MYSQL" >&2
exit 1
fi
sleep 2
host_port=$((18080 + BUILD_NUMBER % 1000))
$DOCKER_CMD run --detach \
--name "$SMOKE_APP" \
--network "$SMOKE_NETWORK" \
--publish "127.0.0.1:${host_port}:8080" \
--env ASPNETCORE_ENVIRONMENT=Production \
--env "ConnectionStrings__Default=Server=$SMOKE_MYSQL;Port=3306;Database=jizhi_smoke;User=jizhi;Password=jizhi-smoke-password" \
--env Jwt__Secret=jizhi-smoke-jwt-secret-at-least-32-characters \
--env Jwt__Issuer=MiaoJiZhang.Api \
--env Jwt__Audience=MiaoJiZhang.App \
--env Admin__BootstrapUsername=smoke_admin \
--env Admin__BootstrapPassword=jizhi-smoke-admin-password \
--env Admin__CookieSecure=false \
--env "Build__Version=$IMMUTABLE_TAG" \
"$IMAGE_REF" >/dev/null
app_ready=0
for attempt in $(seq 1 90); do
if curl --fail --silent --show-error \
"http://127.0.0.1:$host_port/api/ping" >artifacts/smoke-ping.json; then
app_ready=1
break
fi
if ! $DOCKER_CMD inspect "$SMOKE_APP" --format '{{.State.Running}}' | grep -qx true; then
$DOCKER_CMD logs "$SMOKE_APP" >&2
exit 1
fi
sleep 2
done
if [ "$app_ready" != 1 ]; then
$DOCKER_CMD logs "$SMOKE_APP" >&2
exit 1
fi
grep -Fq "\"version\":\"$IMMUTABLE_TAG\"" artifacts/smoke-ping.json
curl --fail --silent --show-error "http://127.0.0.1:$host_port/" \
| grep -Fq '<div id="app"></div>'
'''
}
}
stage('Package Image') {
steps {
sh '''
set -euo pipefail
$DOCKER_CMD save "$IMAGE_REF" | gzip -1 >"$ARCHIVE_PATH"
gzip -t "$ARCHIVE_PATH"
gzip -dc "$ARCHIVE_PATH" | tar -tf - | grep -q '^manifest.json$'
sha256sum "$ARCHIVE_PATH" >"$ARCHIVE_PATH.sha256"
image_id=$($DOCKER_CMD image inspect "$IMAGE_REF" --format '{{.Id}}')
archive_sha=$(awk '{print $1}' "$ARCHIVE_PATH.sha256")
export image_id archive_sha
python3 - <<'PY'
import json, os, pathlib
path = pathlib.Path(os.environ['MANIFEST_PATH'])
payload = {
'project': 'jizhang-backend',
'buildNumber': os.environ['BUILD_NUMBER'],
'version': os.environ['IMMUTABLE_TAG'],
'commit': os.environ['FULL_SHA'],
'platform': 'linux/amd64',
'image': os.environ['IMAGE_REF'],
'imageId': os.environ['image_id'],
'archive': pathlib.Path(os.environ['ARCHIVE_PATH']).name,
'archiveSha256': os.environ['archive_sha'],
'builtAt': os.environ['BUILD_DATE'],
'contents': ['admin-web', 'MiaoJiZhang.Api'],
'smokeTests': ['image architecture', '/api/ping', 'admin web index'],
}
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
PY
sha256sum "$MANIFEST_PATH" >"$MANIFEST_PATH.sha256"
'''
}
}
stage('Upload To OpenList') {
steps {
withCredentials([usernamePassword(credentialsId: "${OPENLIST_CREDENTIALS}", usernameVariable: 'OPENLIST_USERNAME', passwordVariable: 'OPENLIST_PASSWORD')]) {
sh '''
set -euo pipefail
for artifact in \
"$ARCHIVE_PATH" \
"$ARCHIVE_PATH.sha256" \
"$MANIFEST_PATH" \
"$MANIFEST_PATH.sha256"; do
./scripts/upload-openlist-artifact.sh "$artifact" "$OPENLIST_REMOTE_DIR"
done
'''
}
}
}
}
post {
always {
script {
if (env.NODE_NAME?.trim() && env.DOCKER_CMD?.trim()) {
sh(
script: '''
$DOCKER_CMD rm --force "$SMOKE_APP" "$SMOKE_MYSQL" >/dev/null 2>&1 || true
$DOCKER_CMD network rm "$SMOKE_NETWORK" >/dev/null 2>&1 || true
$DOCKER_CMD image rm "$IMAGE_REF" >/dev/null 2>&1 || true
''',
returnStatus: true,
)
}
}
}
success {
archiveArtifacts artifacts: 'artifacts/*.json,artifacts/*.sha256', fingerprint: true
}
cleanup {
script {
if (env.NODE_NAME?.trim()) {
cleanWs(deleteDirs: true, notFailBuild: true)
}
}
}
}
}