diff --git a/scripts/jenkins-freestyle.sh b/scripts/jenkins-freestyle.sh new file mode 100755 index 0000000..80b330b --- /dev/null +++ b/scripts/jenkins-freestyle.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +set +x + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname -- "$SCRIPT_DIR")" +cd "$PROJECT_ROOT" + +# Jenkins 页面只需要配置 ACR_REGISTRY 和 DEPLOY_TO_ACK。 +# 其余默认值已与当前 ACK 资源对齐,必要时仍可通过同名环境变量覆盖。 +ACR_REGISTRY="${ACR_REGISTRY:-}" +ACR_NAMESPACE="${ACR_NAMESPACE:-one-feel}" +API_REPOSITORY="${API_REPOSITORY:-wanderq-api}" +ADMIN_REPOSITORY="${ADMIN_REPOSITORY:-wanderq-admin}" +ACK_NAMESPACE="${ACK_NAMESPACE:-wanqu}" +API_DEPLOYMENT="${API_DEPLOYMENT:-wanderq-api}" +API_CONTAINER="${API_CONTAINER:-}" +ADMIN_DEPLOYMENT="${ADMIN_DEPLOYMENT:-wanderq-admin}" +ADMIN_CONTAINER="${ADMIN_CONTAINER:-}" +ADMIN_API_BASE_URL="${ADMIN_API_BASE_URL:-}" +DEPLOY_TO_ACK="${DEPLOY_TO_ACK:-false}" +BUILD_NUMBER="${BUILD_NUMBER:-local}" +DOCKER_BUILDKIT="${DOCKER_BUILDKIT:-1}" +export DOCKER_BUILDKIT + +log() { + printf '\n[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" +} + +fail() { + printf '错误:%s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "Jenkins Agent 缺少命令:$1" +} + +[[ -n "$ACR_REGISTRY" ]] || fail "ACR_REGISTRY 不能为空" +[[ "$ACR_REGISTRY" != *://* ]] || fail "ACR_REGISTRY 只填写域名,不要包含协议" +[[ -n "$ACR_NAMESPACE" ]] || fail "ACR_NAMESPACE 不能为空" +[[ -n "${ACR_USERNAME:-}" ]] || fail "未绑定 Jenkins ACR 用户名变量 ACR_USERNAME" +[[ -n "${ACR_PASSWORD:-}" ]] || fail "未绑定 Jenkins ACR 密码变量 ACR_PASSWORD" + +case "$DEPLOY_TO_ACK" in + true|false) ;; + *) fail "DEPLOY_TO_ACK 只能是 true 或 false" ;; +esac + +require_command git +require_command docker +docker version >/dev/null + +if [[ "$DEPLOY_TO_ACK" == "true" ]]; then + require_command kubectl + [[ -n "${ACK_KUBECONFIG:-}" ]] || fail "未绑定 Jenkins kubeconfig 文件变量 ACK_KUBECONFIG" + [[ -f "$ACK_KUBECONFIG" ]] || fail "ACK_KUBECONFIG 指向的文件不存在" +fi + +GIT_COMMIT="$(git rev-parse --short=12 HEAD)" +IMAGE_TAG="${IMAGE_TAG:-${GIT_COMMIT}-${BUILD_NUMBER}}" +API_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/${API_REPOSITORY}" +ADMIN_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/${ADMIN_REPOSITORY}" + +test_container="" +docker_config="" + +cleanup() { + if [[ -n "$test_container" ]]; then + docker rm --force "$test_container" >/dev/null 2>&1 || true + fi + if [[ -n "$docker_config" && -d "$docker_config" ]]; then + docker logout "$ACR_REGISTRY" >/dev/null 2>&1 || true + rm -rf -- "$docker_config" + fi +} +trap cleanup EXIT + +log "构建 API 镜像:$API_IMAGE:$IMAGE_TAG" +docker build \ + --tag "$API_IMAGE:$IMAGE_TAG" \ + WonderQ-Admin + +log "运行 API 测试" +test_container="$(docker create \ + --entrypoint python \ + "$API_IMAGE:$IMAGE_TAG" \ + -m pytest)" +docker cp WonderQ-Admin/tests/. "$test_container:/app/tests" +docker start --attach "$test_container" +docker rm "$test_container" >/dev/null +test_container="" + +log "构建 Admin UI 镜像:$ADMIN_IMAGE:$IMAGE_TAG" +docker build \ + --build-arg "VITE_API_BASE_URL=$ADMIN_API_BASE_URL" \ + --tag "$ADMIN_IMAGE:$IMAGE_TAG" \ + WonderQ-Admin-UI + +log "检查 Admin UI Nginx 配置" +docker run --rm \ + --entrypoint nginx \ + "$ADMIN_IMAGE:$IMAGE_TAG" \ + -t + +log "登录 ACR 并推送镜像" +docker_config="$(mktemp -d)" +export DOCKER_CONFIG="$docker_config" +printf '%s' "$ACR_PASSWORD" | docker login \ + --username "$ACR_USERNAME" \ + --password-stdin \ + "$ACR_REGISTRY" +docker push "$API_IMAGE:$IMAGE_TAG" +docker push "$ADMIN_IMAGE:$IMAGE_TAG" + +if [[ "$DEPLOY_TO_ACK" != "true" ]]; then + log "镜像已推送,DEPLOY_TO_ACK=false,本次不更新 ACK" + exit 0 +fi + +KUBECTL=( + kubectl + --kubeconfig "$ACK_KUBECONFIG" + --namespace "$ACK_NAMESPACE" +) + +log "检查 ACK Deployment" +"${KUBECTL[@]}" get deployment "$API_DEPLOYMENT" >/dev/null +"${KUBECTL[@]}" get deployment "$ADMIN_DEPLOYMENT" >/dev/null + +# 当前两个 Deployment 均为单容器;自动读取容器名,避免把 Pod 名称或历史拼写写进 Jenkins 参数。 +if [[ -z "$API_CONTAINER" ]]; then + API_CONTAINER="$("${KUBECTL[@]}" get deployment "$API_DEPLOYMENT" \ + -o jsonpath='{.spec.template.spec.containers[0].name}')" +fi +if [[ -z "$ADMIN_CONTAINER" ]]; then + ADMIN_CONTAINER="$("${KUBECTL[@]}" get deployment "$ADMIN_DEPLOYMENT" \ + -o jsonpath='{.spec.template.spec.containers[0].name}')" +fi +[[ -n "$API_CONTAINER" ]] || fail "无法读取 API Deployment 的容器名称" +[[ -n "$ADMIN_CONTAINER" ]] || fail "无法读取 Admin UI Deployment 的容器名称" + +log "更新 API Deployment:$API_DEPLOYMENT/$API_CONTAINER" +"${KUBECTL[@]}" set image \ + "deployment/$API_DEPLOYMENT" \ + "$API_CONTAINER=$API_IMAGE:$IMAGE_TAG" + +if ! "${KUBECTL[@]}" rollout status \ + "deployment/$API_DEPLOYMENT" \ + --timeout=300s; then + "${KUBECTL[@]}" rollout undo "deployment/$API_DEPLOYMENT" || true + fail "API 发布失败,已尝试回滚 Deployment;数据库迁移不会自动回滚" +fi + +log "更新 Admin UI Deployment:$ADMIN_DEPLOYMENT/$ADMIN_CONTAINER" +"${KUBECTL[@]}" set image \ + "deployment/$ADMIN_DEPLOYMENT" \ + "$ADMIN_CONTAINER=$ADMIN_IMAGE:$IMAGE_TAG" + +if ! "${KUBECTL[@]}" rollout status \ + "deployment/$ADMIN_DEPLOYMENT" \ + --timeout=300s; then + "${KUBECTL[@]}" rollout undo "deployment/$ADMIN_DEPLOYMENT" || true + "${KUBECTL[@]}" rollout undo "deployment/$API_DEPLOYMENT" || true + fail "Admin UI 发布失败,已尝试回滚两个 Deployment" +fi + +log "ACK 发布完成,镜像标签:$IMAGE_TAG"