Jenkinsfile 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. /**
  2. * UAT: Checkout -> Maven -> (optional) push images to Harbor -> deploy jar + docker restart.
  3. *
  4. * Jenkins Job: Pipeline script from SCM
  5. * Script Path: docs/jenkins/uat/Jenkinsfile
  6. *
  7. * Harbor (153.68): when PUSH_TO_HARBOR=true, push e.g.
  8. * 39.105.153.68/alien_cloud/gateway:uat-latest
  9. * Before push: existing uat-latest is archived as uat-build-<BUILD_NUMBER> (same digest).
  10. * New image is pushed only as uat-latest. Prod promote: SOURCE_TAG=uat-latest.
  11. */
  12. /** Normalize GIT_BRANCH: uat-20260202 (not origin/uat-20260202 or refs/heads/...) */
  13. def normalizeGitBranch(String raw) {
  14. def b = (raw ?: 'uat-20260202').trim()
  15. if (!b) {
  16. return 'uat-20260202'
  17. }
  18. while (b.startsWith('refs/heads/')) {
  19. b = b.substring('refs/heads/'.length())
  20. }
  21. while (b.startsWith('origin/')) {
  22. b = b.substring('origin/'.length())
  23. }
  24. return b ?: 'uat-20260202'
  25. }
  26. /** HARBOR_PUSH_SCOPE: all-java-services | <repo>-only */
  27. def filterHarborPushScope(List allServices, String scope) {
  28. def s = (scope ?: 'all-java-services').trim()
  29. if (s == 'all-java-services') {
  30. return allServices
  31. }
  32. if (s.endsWith('-only')) {
  33. def repo = s.substring(0, s.length() - '-only'.length())
  34. def picked = allServices.findAll { it.repo == repo }
  35. if (picked.isEmpty()) {
  36. error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
  37. }
  38. return picked
  39. }
  40. error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
  41. }
  42. def pushOneHarborImage(def script, Map svc, String reg, String proj, String latestTag, String buildTag,
  43. String baseImage, String dockerfile, String workspace) {
  44. def jarName = "${svc.module}-1.0.0.jar"
  45. def imageLatest = "${reg}/${proj}/${svc.repo}:${latestTag}"
  46. def imageBuild = "${reg}/${proj}/${svc.repo}:${buildTag}"
  47. def withLibFlag = svc.withLib ? 'true' : 'false'
  48. // Parallel branches share one Docker daemon; serialize docker ops to avoid containerd CreateDiff/lease races.
  49. script.sh """
  50. set -e
  51. test -f ${workspace}/${svc.module}/target/${jarName}
  52. cd ${workspace}/${svc.module}
  53. rm -rf .jenkins_docker_ctx && mkdir -p .jenkins_docker_ctx/lib
  54. cp -f target/${jarName} .jenkins_docker_ctx/${jarName}
  55. if [ "${withLibFlag}" = "true" ] && [ -d target/lib ]; then
  56. cp -rf target/lib/. .jenkins_docker_ctx/lib/
  57. else
  58. touch .jenkins_docker_ctx/lib/.keep
  59. fi
  60. DOCKER_LOCK=/tmp/jenkins-alien-cloud-docker.lock
  61. flock "\${DOCKER_LOCK}" sh -c '
  62. set -e
  63. cd ${workspace}/${svc.module}/.jenkins_docker_ctx
  64. if docker pull ${imageLatest} 2>/dev/null; then
  65. echo ">>> archive previous ${latestTag} -> ${buildTag}"
  66. docker tag ${imageLatest} ${imageBuild}
  67. docker push ${imageBuild}
  68. docker rmi ${imageBuild} ${imageLatest} 2>/dev/null || true
  69. fi
  70. build_ok=0
  71. for attempt in 1 2 3; do
  72. if docker build -f ${workspace}/${dockerfile} \\
  73. --build-arg BASE_IMAGE=${baseImage} \\
  74. --build-arg JAR_FILE=${jarName} \\
  75. --build-arg SERVER_PORT=${svc.port} \\
  76. --build-arg WITH_LIB=${svc.withLib} \\
  77. -t ${imageLatest} .; then
  78. build_ok=1
  79. break
  80. fi
  81. echo ">>> WARN: docker build ${svc.repo} attempt \${attempt} failed, retrying..."
  82. sleep "\$((attempt * 5))"
  83. done
  84. if [ "\${build_ok}" -ne 1 ]; then
  85. echo ">>> ERROR: docker build ${svc.repo} failed after 3 attempts"
  86. exit 1
  87. fi
  88. docker push ${imageLatest}
  89. echo ">>> pushed ${imageLatest} (archived prior latest as ${buildTag} if any)"
  90. docker rmi ${imageLatest} 2>/dev/null || true
  91. '
  92. cd ${workspace}/${svc.module}
  93. rm -rf .jenkins_docker_ctx
  94. """
  95. }
  96. def deployOneUatService(def script, String moduleName, String dirName, String workspace) {
  97. def sourceJar = "${workspace}/${moduleName}/target/${moduleName}-1.0.0.jar"
  98. def sourceLib = "${workspace}/${moduleName}/target/lib"
  99. def targetDir = "/app_deploy_uat/${dirName}"
  100. script.sh """
  101. set -e
  102. echo ">>> Deploy module: ${moduleName}"
  103. if [ -f "${sourceJar}" ]; then
  104. mkdir -p "${targetDir}"
  105. if [ -d "${sourceLib}" ]; then
  106. rm -rf "${targetDir}/lib"
  107. cp -rf "${sourceLib}" "${targetDir}"
  108. fi
  109. cp -f "${sourceJar}" "${targetDir}/"
  110. if docker ps -a --format '{{.Names}}' | grep -wq "${dirName}"; then
  111. docker restart "${dirName}"
  112. echo ">>> [${dirName}] restarted"
  113. else
  114. echo ">>> [${dirName}] container missing, jar copied only"
  115. fi
  116. else
  117. echo ">>> [${dirName}] jar missing, skip"
  118. fi
  119. """
  120. }
  121. /** Delete oldest uat-build-* tags in Harbor, keep newest KEEP. Never deletes uat-latest or current build tag. */
  122. def pruneHarborUatTags(def script, String reg, String proj, List repoNames, int keepCount, String tagPrefix, String currentBuildTag, String latestTag) {
  123. if (repoNames == null || repoNames.isEmpty() || keepCount < 1) {
  124. return
  125. }
  126. def repos = repoNames.join(' ')
  127. // POSIX sh only (Jenkins sh step); no mapfile / process substitution
  128. script.sh """
  129. set -e
  130. REG='${reg}'
  131. PROJ='${proj}'
  132. KEEP=${keepCount}
  133. PREFIX='${tagPrefix}'
  134. CURRENT='${currentBuildTag}'
  135. LATEST='${latestTag}'
  136. if ! command -v jq >/dev/null 2>&1; then
  137. echo '>>> Harbor prune skipped: jq not installed on Jenkins agent'
  138. exit 0
  139. fi
  140. for repo in ${repos}; do
  141. enc_repo=\$(printf '%s' "\${repo}" | jq -sRr @uri)
  142. tags_file=\$(mktemp)
  143. curl -fsS -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
  144. "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts?page_size=100" \\
  145. | jq -r '.[] | .tags[]? | .name' | grep "^\${PREFIX}" | sort -t- -k3 -n > "\${tags_file}" || true
  146. count=\$(wc -l < "\${tags_file}" | tr -d ' ')
  147. echo ">>> prune \${repo}: \${count} tag(s) matching \${PREFIX}*"
  148. if [ "\${count}" -le "\${KEEP}" ]; then
  149. rm -f "\${tags_file}"
  150. continue
  151. fi
  152. del_count=\$((count - KEEP))
  153. deleted=0
  154. while IFS= read -r t; do
  155. if [ -z "\${t}" ]; then
  156. continue
  157. fi
  158. if [ "\${t}" = "\${CURRENT}" ] || [ "\${t}" = "\${LATEST}" ]; then
  159. continue
  160. fi
  161. if [ "\${deleted}" -ge "\${del_count}" ]; then
  162. break
  163. fi
  164. echo ">>> DELETE Harbor tag \${repo}:\${t}"
  165. if ! curl -fsS -X DELETE -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
  166. "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${t}/tags/\${t}"; then
  167. echo ">>> WARN: delete failed \${repo}:\${t} (check robot delete permission)"
  168. fi
  169. deleted=\$((deleted + 1))
  170. done < "\${tags_file}"
  171. rm -f "\${tags_file}"
  172. done
  173. """
  174. }
  175. pipeline {
  176. agent any
  177. options {
  178. buildDiscarder(logRotator(numToKeepStr: '5'))
  179. disableConcurrentBuilds()
  180. timestamps()
  181. timeout(time: 90, unit: 'MINUTES')
  182. }
  183. parameters {
  184. string(
  185. name: 'GIT_BRANCH',
  186. defaultValue: 'uat-20260202',
  187. trim: true,
  188. description: 'Git branch name only (e.g. uat-20260202). Do not prefix origin/'
  189. )
  190. booleanParam(name: 'FORCE_UPDATE', defaultValue: false, description: 'mvn -U (routine builds leave unchecked for speed)')
  191. booleanParam(name: 'ALLOW_SNAPSHOTS', defaultValue: true, description: 'allow SNAPSHOT deps')
  192. booleanParam(
  193. name: 'PUSH_TO_HARBOR',
  194. defaultValue: true,
  195. description: 'After Maven: docker build + push to Harbor (tags uat-latest and uat-build-<N>). Uncheck for jar-only UAT deploy.'
  196. )
  197. choice(
  198. name: 'HARBOR_PUSH_SCOPE',
  199. choices: [
  200. 'all-java-services',
  201. 'gateway-only',
  202. 'store-only',
  203. 'second-only',
  204. 'store-platform-only',
  205. 'lawyer-only',
  206. 'job-only',
  207. 'dining-only',
  208. ],
  209. description: 'Only when PUSH_TO_HARBOR=true; default=all seven; *-only=one service'
  210. )
  211. string(name: 'HARBOR_REGISTRY', defaultValue: '39.105.153.68', trim: true)
  212. string(name: 'HARBOR_PROJECT', defaultValue: 'alien_cloud', trim: true)
  213. booleanParam(
  214. name: 'HARBOR_PRUNE_OLD_TAGS',
  215. defaultValue: true,
  216. description: 'After push: delete old uat-build-* tags in Harbor, keep last N per repo (never deletes uat-latest)'
  217. )
  218. string(name: 'HARBOR_KEEP_TAG_COUNT', defaultValue: '10', trim: true,
  219. description: 'How many uat-build-* tags to keep per repository')
  220. booleanParam(
  221. name: 'HARBOR_PUSH_PARALLEL',
  222. defaultValue: true,
  223. description: 'Parallel per service (context prep); docker build/push serialized via flock on agent'
  224. )
  225. }
  226. environment {
  227. MAVEN_HOME = tool '3.6.3'
  228. PATH = "${MAVEN_HOME}/bin:${env.PATH}"
  229. GIT_URL = 'http://8.152.195.41:3000/alien/alien_cloud'
  230. GIT_CREDENTIALS = 'zhanghaomimapingzheng'
  231. HARBOR_CREDENTIALS = 'harbor-robot-alien'
  232. UAT_HARBOR_LATEST_TAG = 'uat-latest'
  233. UAT_HARBOR_BUILD_TAG = "uat-build-${env.BUILD_NUMBER}"
  234. DOCKERFILE_JAVA = 'docs/jenkins/produ/docker/Dockerfile.java-service'
  235. MAVEN_LOCAL_REPO = '/var/jenkins_home/.m2/repository'
  236. }
  237. stages {
  238. stage('Checkout') {
  239. steps {
  240. script {
  241. // Do not use env.GIT_BRANCH — Jenkins SCM plugin injects origin/<branch> and overwrites it.
  242. def branch = normalizeGitBranch(params.GIT_BRANCH)
  243. if (!branch) {
  244. error('GIT_BRANCH is required')
  245. }
  246. env.UAT_GIT_BRANCH = branch
  247. if (params.GIT_BRANCH?.trim() != branch) {
  248. echo ">>> GIT_BRANCH normalized: '${params.GIT_BRANCH}' -> '${branch}'"
  249. }
  250. echo ">>> Checkout branch: ${branch}"
  251. sh """
  252. set -e
  253. git fetch origin
  254. git checkout -B ${branch} origin/${branch}
  255. git log -1 --oneline
  256. """
  257. }
  258. }
  259. }
  260. stage('Prepare Maven Settings') {
  261. steps {
  262. script {
  263. writeFile file: 'settings.xml', text: """<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  264. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  265. xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
  266. <mirrors>
  267. <mirror>
  268. <id>aliyun-central</id>
  269. <mirrorOf>central</mirrorOf>
  270. <name>Aliyun Central</name>
  271. <url>https://maven.aliyun.com/repository/central</url>
  272. </mirror>
  273. </mirrors>
  274. <profiles>
  275. <profile>
  276. <id>repo-mix</id>
  277. <repositories>
  278. <repository>
  279. <id>aliyunmaven</id>
  280. <name>Aliyun Maven</name>
  281. <url>https://maven.aliyun.com/repository/public</url>
  282. <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
  283. <snapshots><enabled>false</enabled></snapshots>
  284. </repository>
  285. <repository>
  286. <id>central</id>
  287. <name>Maven Central</name>
  288. <url>https://repo.maven.apache.org/maven2</url>
  289. <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
  290. <snapshots><enabled>false</enabled></snapshots>
  291. </repository>
  292. <repository>
  293. <id>spring-milestones</id>
  294. <name>Spring Milestones</name>
  295. <url>https://repo.spring.io/milestone</url>
  296. <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
  297. <snapshots><enabled>false</enabled></snapshots>
  298. </repository>
  299. <repository>
  300. <id>spring-snapshots</id>
  301. <name>Spring Snapshots</name>
  302. <url>https://repo.spring.io/snapshot</url>
  303. <releases><enabled>false</enabled></releases>
  304. <snapshots><enabled>true</enabled><updatePolicy>daily</updatePolicy></snapshots>
  305. </repository>
  306. </repositories>
  307. <pluginRepositories>
  308. <pluginRepository>
  309. <id>aliyunmaven</id>
  310. <url>https://maven.aliyun.com/repository/public</url>
  311. <releases><enabled>true</enabled></releases>
  312. <snapshots><enabled>false</enabled></snapshots>
  313. </pluginRepository>
  314. <pluginRepository>
  315. <id>central</id>
  316. <url>https://repo.maven.apache.org/maven2</url>
  317. <releases><enabled>true</enabled></releases>
  318. <snapshots><enabled>false</enabled></snapshots>
  319. </pluginRepository>
  320. <pluginRepository>
  321. <id>spring-milestones</id>
  322. <url>https://repo.spring.io/milestone</url>
  323. <releases><enabled>true</enabled></releases>
  324. <snapshots><enabled>false</enabled></snapshots>
  325. </pluginRepository>
  326. </pluginRepositories>
  327. </profile>
  328. </profiles>
  329. <activeProfiles>
  330. <activeProfile>repo-mix</activeProfile>
  331. </activeProfiles>
  332. </settings>
  333. """
  334. }
  335. }
  336. }
  337. stage('Maven Build') {
  338. steps {
  339. script {
  340. def updateFlag = params.FORCE_UPDATE ? '-U' : ''
  341. retry(2) {
  342. sh """
  343. set -e
  344. mvn -version
  345. unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy no_proxy NO_PROXY || true
  346. export MAVEN_OPTS="-Xms512m -Xmx2048m -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
  347. mkdir -p ${MAVEN_LOCAL_REPO}
  348. mvn clean package -DskipTests -s settings.xml ${updateFlag} -e \\
  349. -T 1C -Dmaven.artifact.threads=8 \\
  350. -Dmaven.repo.local=${MAVEN_LOCAL_REPO}
  351. """
  352. }
  353. }
  354. }
  355. }
  356. stage('Push images to Harbor') {
  357. when {
  358. expression { return params.PUSH_TO_HARBOR == true }
  359. }
  360. steps {
  361. script {
  362. def reg = params.HARBOR_REGISTRY.trim()
  363. def proj = params.HARBOR_PROJECT.trim()
  364. def latestTag = env.UAT_HARBOR_LATEST_TAG
  365. def buildTag = env.UAT_HARBOR_BUILD_TAG
  366. def baseImage = "${reg}/${proj}/base/openjdk8-ffmpeg:v1"
  367. def dockerfile = env.DOCKERFILE_JAVA
  368. def allHarborServices = [
  369. [module: 'alien-gateway', repo: 'gateway', port: '8000', withLib: false],
  370. [module: 'alien-store', repo: 'store', port: '50014', withLib: true],
  371. [module: 'alien-second', repo: 'second', port: '50015', withLib: false],
  372. [module: 'alien-store-platform', repo: 'store-platform', port: '50016', withLib: false],
  373. [module: 'alien-lawyer', repo: 'lawyer', port: '50017', withLib: true],
  374. [module: 'alien-job', repo: 'job', port: '50108', withLib: false],
  375. [module: 'alien-dining', repo: 'dining', port: '50019', withLib: false],
  376. ]
  377. def harborServices = filterHarborPushScope(allHarborServices, params.HARBOR_PUSH_SCOPE)
  378. echo ">>> HARBOR_PUSH_SCOPE=${params.HARBOR_PUSH_SCOPE} repos=${harborServices*.repo.join(',')}"
  379. withCredentials([usernamePassword(
  380. credentialsId: env.HARBOR_CREDENTIALS,
  381. usernameVariable: 'HARBOR_USER',
  382. passwordVariable: 'HARBOR_PASS',
  383. )]) {
  384. sh """
  385. set -e
  386. echo "\${HARBOR_PASS}" | docker login ${reg} -u "\${HARBOR_USER}" --password-stdin
  387. echo ">>> docker disk before Harbor push:"
  388. df -h /var/lib/docker 2>/dev/null || df -h / || true
  389. docker image prune -f 2>/dev/null || true
  390. """
  391. if (params.HARBOR_PUSH_PARALLEL) {
  392. def pushBranches = [:]
  393. harborServices.each { svc ->
  394. def s = svc
  395. pushBranches[s.repo] = {
  396. pushOneHarborImage(
  397. this, s, reg, proj, latestTag, buildTag,
  398. baseImage, dockerfile, env.WORKSPACE,
  399. )
  400. }
  401. }
  402. parallel pushBranches
  403. } else {
  404. harborServices.each { svc ->
  405. pushOneHarborImage(
  406. this, svc, reg, proj, latestTag, buildTag,
  407. baseImage, dockerfile, env.WORKSPACE,
  408. )
  409. }
  410. }
  411. sh """
  412. echo ">>> docker disk after Harbor push:"
  413. df -h /var/lib/docker 2>/dev/null || df -h / || true
  414. docker image prune -f 2>/dev/null || true
  415. """
  416. if (params.HARBOR_PRUNE_OLD_TAGS == true) {
  417. def keepN = (params.HARBOR_KEEP_TAG_COUNT ?: '10').trim() as int
  418. catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
  419. pruneHarborUatTags(
  420. this, reg, proj, harborServices*.repo,
  421. keepN, 'uat-build-', buildTag, latestTag,
  422. )
  423. }
  424. }
  425. }
  426. echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}; archived tag this run: ${env.UAT_HARBOR_BUILD_TAG}"
  427. echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
  428. }
  429. }
  430. }
  431. stage('Deploy Services') {
  432. steps {
  433. script {
  434. def services = [
  435. 'alien-gateway:gateway-uat',
  436. 'alien-job:job-uat',
  437. 'alien-lawyer:lawyer-uat',
  438. 'alien-second:second-uat',
  439. 'alien-store:store-uat',
  440. 'alien-dining:dining-uat',
  441. 'alien-store-platform:store-platform-uat',
  442. ]
  443. def deployBranches = [:]
  444. services.each { item ->
  445. def parts = item.split(':')
  446. def moduleName = parts[0]
  447. def dirName = parts[1]
  448. deployBranches[dirName] = {
  449. deployOneUatService(this, moduleName, dirName, env.WORKSPACE)
  450. }
  451. }
  452. parallel deployBranches
  453. }
  454. }
  455. }
  456. }
  457. post {
  458. always {
  459. sh 'rm -f settings.xml || true'
  460. script {
  461. if (!params.PUSH_TO_HARBOR) {
  462. echo '>>> Harbor push SKIPPED: PUSH_TO_HARBOR is false. On "Build with Parameters" check PUSH_TO_HARBOR.'
  463. }
  464. }
  465. }
  466. success {
  467. script {
  468. if (params.PUSH_TO_HARBOR) {
  469. echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}"
  470. echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
  471. }
  472. }
  473. }
  474. }
  475. }