From 2752cfb5b268d866fe863b49f4bfc90e58e72c14 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 14 Aug 2026 17:45:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BC=80=E5=8F=91=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E8=BF=9C=E7=A8=8B=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 8 +- Jenkinsfile.service | 11 +- deploy/scripts/deploy-development-service.sh | 109 +++++++++++++++++++ deploy/scripts/deploy-services.sh | 5 + 4 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 deploy/scripts/deploy-development-service.sh diff --git a/Jenkinsfile b/Jenkinsfile index c9a2d0c64..b8adbf11d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -52,7 +52,7 @@ pipeline { booleanParam(name: 'SERVICE_INTEGRATION', defaultValue: false, description: '第三方集成服务(integration-service)') booleanParam(name: 'SERVICE_EVENT_BRIDGE', defaultValue: false, description: '跨区域事件桥(event-bridge-service)') booleanParam(name: 'SERVICE_AUDIT', defaultValue: false, description: '审计服务(audit-service)') - choice(name: 'ENVIRONMENT', choices: ['test', 'staging', 'production'], description: '部署环境') + choice(name: 'ENVIRONMENT', choices: ['test', 'development', 'staging', 'production'], description: '部署环境') booleanParam(name: 'DEPLOY', defaultValue: false, description: '各服务构建成功后是否独立部署') booleanParam(name: 'INITIALIZE_TEST_DATABASE', defaultValue: false, description: '仅 test:调度服务前幂等初始化测试数据库') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试;生产构建不建议勾选') @@ -66,9 +66,9 @@ pipeline { steps { script { String requestedRepository - if (params.ENVIRONMENT == 'test') { - requestedRepository = 'baiyue-firstleg-test' - echo '测试环境使用 Jenkins 节点本机镜像,不登录、不推送远程 Registry' + if (['development', 'test'].contains(params.ENVIRONMENT)) { + requestedRepository = "baiyue-firstleg-${params.ENVIRONMENT}" + echo "${params.ENVIRONMENT} 环境使用本地构建镜像,不登录、不推送远程 Registry" } else { requestedRepository = params.IMAGE_REPOSITORY?.trim() if (!requestedRepository || requestedRepository == 'registry.example.com/your-group/baiyue-firstleg') { diff --git a/Jenkinsfile.service b/Jenkinsfile.service index 9fc9e69b7..438112a60 100644 --- a/Jenkinsfile.service +++ b/Jenkinsfile.service @@ -36,7 +36,7 @@ pipeline { parameters { string(name: 'SOURCE_COMMIT', defaultValue: '', description: '必须构建的 Git commit;通常由总控 Job 传入') string(name: 'IMAGE_TAG', defaultValue: '', description: '不可变镜像标签;通常使用 Git commit 前 12 位') - choice(name: 'ENVIRONMENT', choices: ['test', 'staging', 'production'], description: '部署环境') + choice(name: 'ENVIRONMENT', choices: ['test', 'development', 'staging', 'production'], description: '部署环境') booleanParam(name: 'DEPLOY', defaultValue: false, description: '构建成功后是否部署本服务') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试') string(name: 'IMAGE_REPOSITORY', defaultValue: 'baiyue-firstleg-test', description: '镜像仓库基础路径,不包含服务名和标签') @@ -47,6 +47,9 @@ pipeline { REGISTRY_CREDENTIALS = 'gitea-test-token' MAVEN_SETTINGS = 'deploy/maven/settings-cn.xml' MAVEN_OPTS = '-Dfile.encoding=UTF-8 -Dmaven.wagon.http.retryHandler.count=3' + DEVELOPMENT_SSH_TARGET = 'deploy@172.31.198.8' + DEVELOPMENT_SSH_KEY = '/var/jenkins_home/.ssh/dev_deploy' + DEVELOPMENT_DEPLOY_DIR = '/opt/firstleg/application' } stages { @@ -81,7 +84,7 @@ pipeline { if (!imageRepository || imageRepository.contains('://')) { error('IMAGE_REPOSITORY 不能为空且不能包含 http:// 或 https://') } - if (params.ENVIRONMENT != 'test' && !imageRepository.contains('/')) { + if (!['development', 'test'].contains(params.ENVIRONMENT) && !imageRepository.contains('/')) { error('预发布/生产环境 IMAGE_REPOSITORY 必须使用 host[:port]/owner/image 格式') } @@ -137,8 +140,8 @@ pipeline { sh "mkdir -p '${contextDir}' && cp -f '${jar}' '${contextDir}/app.jar'" sh "docker build -f deploy/docker/Dockerfile --build-arg BASE_IMAGE=${params.DOCKER_BASE_IMAGE} --build-arg JAR_FILE=app.jar -t ${image} '${contextDir}'" - if (params.ENVIRONMENT == 'test') { - echo "测试镜像已保存在当前 Docker 节点:${image}" + if (['development', 'test'].contains(params.ENVIRONMENT)) { + echo "${params.ENVIRONMENT} 镜像已保存在当前 Docker 节点:${image}" } else { String registryHost = env.RESOLVED_IMAGE_REPOSITORY.tokenize('/')[0] withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) { diff --git a/deploy/scripts/deploy-development-service.sh b/deploy/scripts/deploy-development-service.sh new file mode 100644 index 000000000..4803077dd --- /dev/null +++ b/deploy/scripts/deploy-development-service.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env sh +set -eu + +if [ "$#" -ne 4 ]; then + echo "用法: $0 <镜像仓库> <镜像标签> <逗号分隔服务> <环境文件>" >&2 + exit 2 +fi + +IMAGE_REPOSITORY="$1" +IMAGE_TAG="$2" +SERVICE_CSV="$3" +ENV_FILE="$4" + +SSH_TARGET="${DEVELOPMENT_SSH_TARGET:-deploy@172.31.198.8}" +SSH_KEY="${DEVELOPMENT_SSH_KEY:-/var/jenkins_home/.ssh/dev_deploy}" +REMOTE_DIR="${DEVELOPMENT_DEPLOY_DIR:-/opt/firstleg/application}" +COMPOSE_FILE="deploy/docker-compose.yml" + +if [ ! -f "$SSH_KEY" ]; then + echo "开发环境 SSH 私钥不存在: $SSH_KEY" >&2 + exit 2 +fi +if [ ! -f "$ENV_FILE" ]; then + echo "开发环境变量文件不存在: $ENV_FILE" >&2 + exit 2 +fi +if [ ! -f "$COMPOSE_FILE" ]; then + echo "Compose 文件不存在: $COMPOSE_FILE" >&2 + exit 2 +fi + +SERVICES=$(printf '%s' "$SERVICE_CSV" | tr ',' ' ') +for service in $SERVICES; do + case "$service" in + *[!A-Za-z0-9_-]*|'') + echo "非法服务名: $service" >&2 + exit 2 + ;; + esac +done + +run_ssh() { + ssh -i "$SSH_KEY" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=yes \ + -o ConnectTimeout=10 \ + "$SSH_TARGET" "$@" +} + +run_scp() { + scp -i "$SSH_KEY" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=yes \ + -o ConnectTimeout=10 \ + "$@" +} + +run_ssh install -d -m 750 "$REMOTE_DIR" + +SUFFIX="${IMAGE_TAG}.$$.${SERVICE_CSV}" +REMOTE_ENV_TMP="$REMOTE_DIR/.env.$SUFFIX.tmp" +REMOTE_COMPOSE_TMP="$REMOTE_DIR/docker-compose.yml.$SUFFIX.tmp" + +run_scp "$ENV_FILE" "$SSH_TARGET:$REMOTE_ENV_TMP" +run_scp "$COMPOSE_FILE" "$SSH_TARGET:$REMOTE_COMPOSE_TMP" + +run_ssh sh -s -- "$REMOTE_DIR" "$REMOTE_ENV_TMP" "$REMOTE_COMPOSE_TMP" <<'REMOTE_PREPARE' +set -eu +remote_dir="$1" +env_tmp="$2" +compose_tmp="$3" +chmod 600 "$env_tmp" +chmod 644 "$compose_tmp" +mv -f "$env_tmp" "$remote_dir/.env" +mv -f "$compose_tmp" "$remote_dir/docker-compose.yml" +REMOTE_PREPARE + +for service in $SERVICES; do + image="$IMAGE_REPOSITORY/$service:$IMAGE_TAG" + docker image inspect "$image" >/dev/null + echo "传输开发镜像: $image" + docker save "$image" | gzip -1 | run_ssh 'gzip -dc | docker load' +done + +run_ssh sh -s -- "$REMOTE_DIR" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE_CSV" <<'REMOTE_DEPLOY' +set -eu +remote_dir="$1" +image_repository="$2" +image_tag="$3" +service_csv="$4" +services=$(printf '%s' "$service_csv" | tr ',' ' ') + +cd "$remote_dir" +export DEPLOY_ENV=development +export IMAGE_REPOSITORY="$image_repository" +export IMAGE_TAG="$image_tag" + +docker compose \ + --project-name firstleg-development \ + --env-file .env \ + -f docker-compose.yml \ + up -d --remove-orphans --pull never $services + +docker compose \ + --project-name firstleg-development \ + --env-file .env \ + -f docker-compose.yml \ + ps $services +REMOTE_DEPLOY diff --git a/deploy/scripts/deploy-services.sh b/deploy/scripts/deploy-services.sh index d29243ad8..40bdad59d 100644 --- a/deploy/scripts/deploy-services.sh +++ b/deploy/scripts/deploy-services.sh @@ -74,6 +74,11 @@ esac export DEPLOY_ENV IMAGE_REPOSITORY IMAGE_TAG +if [ "$DEPLOY_ENV" = "development" ]; then + exec sh deploy/scripts/deploy-development-service.sh \ + "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE_CSV" "$ENV_FILE" +fi + PULL_POLICY_ARGS="" if [ "$DEPLOY_ENV" = "test" ]; then PULL_POLICY_ARGS="--pull never"