pipeline { agent { label '\u6784\u5efa\u673a1' } options { timestamps() disableConcurrentBuilds() skipDefaultCheckout(true) } environment { REGISTRY_URL = 'reg.nxsir.cn' API_IMAGE_NAME = 'live_recorder/app-api' WEB_IMAGE_NAME = 'live_recorder/app-web' IMAGE_TAG = "${env.BUILD_ID}" DOCKER_CREDS = 'harbor_key' TARGET_PLATFORMS = 'linux/amd64,linux/arm64' BUILDER_NAME = 'liverecorder-buildx' WEB_NODE_IMAGE = 'docker.m.daocloud.io/library/node:22-alpine' WEB_NGINX_IMAGE = 'docker.m.daocloud.io/library/nginx:1.27-alpine' HTTP_PROXY_URL = 'http://192.168.5.200:7890' // Tsinghua/daocloud/mcr hosts are reachable directly from the builder — routing them // through the 7890 proxy caused intermittent '502 Bad Gateway' during apt-get install. NO_PROXY_HOSTS = '127.0.0.1,localhost,reg.nxsir.cn,gitea.nxsir.cn,mirrors.tuna.tsinghua.edu.cn,.tsinghua.edu.cn,mcr.microsoft.com,docker.m.daocloud.io' GIT_REPO_URL = 'https://gitea.nxsir.cn/nanxun/live_recorder.git' GIT_BRANCH = 'main' GIT_CREDS = '' API_IMAGE_TAGGED = "${REGISTRY_URL}/${API_IMAGE_NAME}:${IMAGE_TAG}" API_IMAGE_LATEST = "${REGISTRY_URL}/${API_IMAGE_NAME}:latest" WEB_IMAGE_TAGGED = "${REGISTRY_URL}/${WEB_IMAGE_NAME}:${IMAGE_TAG}" WEB_IMAGE_LATEST = "${REGISTRY_URL}/${WEB_IMAGE_NAME}:latest" API_CACHE_IMAGE = "${REGISTRY_URL}/${API_IMAGE_NAME}:buildcache" WEB_CACHE_IMAGE = "${REGISTRY_URL}/${WEB_IMAGE_NAME}:buildcache" } stages { stage('Checkout') { steps { script { def userRemoteConfig = [url: env.GIT_REPO_URL] if (env.GIT_CREDS?.trim()) { userRemoteConfig.credentialsId = env.GIT_CREDS.trim() } checkout([ $class: 'GitSCM', branches: [[name: "*/${env.GIT_BRANCH}"]], userRemoteConfigs: [userRemoteConfig] ]) } } } stage('Prepare Buildx') { steps { sh """ set -e sudo docker buildx version # Register ARM64 binfmt. Docker Hub is unreachable from this builder, # so try daocloud mirror first; fall back to apt-installed qemu-user-static. if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ] && [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64-static ]; then echo 'Registering ARM64 emulation (binfmt not present)...' if ! sudo docker run --privileged --rm docker.m.daocloud.io/tonistiigi/binfmt --install arm64 2>/dev/null; then echo 'daocloud mirror failed, falling back to qemu-user-static via apt...' sudo apt-get update -qq sudo apt-get install -y -qq qemu-user-static fi else echo 'ARM64 binfmt already registered, skipping.' fi # Verify emulation works before proceeding sudo docker run --rm --platform linux/arm64 docker.m.daocloud.io/library/alpine:latest uname -m # Use the default docker builder (docker driver) — not a separate # docker-container instance — so we don't need to pull moby/buildkit # from unreachable Docker Hub. sudo docker buildx use default 2>/dev/null || true sudo docker buildx inspect --bootstrap >/dev/null """ } } stage('Build And Push API Image') { steps { withCredentials([ usernamePassword( credentialsId: "${DOCKER_CREDS}", usernameVariable: 'DOCKER_USERNAME', passwordVariable: 'DOCKER_PASSWORD' ) ]) { sh """ set -e run_with_heartbeat() { log_file="\$1" build_label="\$2" shift 2 rm -f "\$log_file" : > "\$log_file" "\$@" >"\$log_file" 2>&1 & cmd_pid=\$! cmd_status=0 last_size=0 while kill -0 "\$cmd_pid" >/dev/null 2>&1; do echo "[heartbeat] \${build_label} still running at \$(date -u +%Y-%m-%dT%H:%M:%SZ)" if [ -f "\$log_file" ]; then current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0) if [ "\$current_size" -gt "\$last_size" ]; then start_byte=\$((last_size + 1)) tail -c +"\$start_byte" "\$log_file" || true last_size=\$current_size fi fi sleep 20 done wait "\$cmd_pid" || cmd_status=\$? if [ -f "\$log_file" ]; then current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0) if [ "\$current_size" -gt "\$last_size" ]; then start_byte=\$((last_size + 1)) tail -c +"\$start_byte" "\$log_file" || true fi fi if [ "\$cmd_status" -ne 0 ]; then echo "[heartbeat] \${build_label} failed with exit code \$cmd_status" fi return "\$cmd_status" } # ── Build each platform separately, push with docker CLI, # then assemble a multi-arch manifest. # buildx --push does not forward auth reliably on this # builder; per-platform --load + docker push + manifest # bypasses that entirely. for PLATFORM in linux/amd64 linux/arm64; do ARCH_TAG="\${PLATFORM##*/}" # amd64 / arm64 PLAT_REF="${API_IMAGE_TAGGED}-\${ARCH_TAG}" PLAT_LATEST="${API_IMAGE_LATEST}-\${ARCH_TAG}" echo '' echo "=== Building API for \$PLATFORM -> \$PLAT_REF ===" run_with_heartbeat /tmp/live-recorder-api-\${ARCH_TAG}.log "API \${ARCH_TAG} build" \ sudo docker buildx build \ --platform "\$PLATFORM" \ --network host \ --progress=plain \ --provenance=false \ --no-cache \ --build-arg HTTP_PROXY=${HTTP_PROXY_URL} \ --build-arg HTTPS_PROXY=${HTTP_PROXY_URL} \ --build-arg NO_PROXY=${NO_PROXY_HOSTS} \ --build-arg http_proxy=${HTTP_PROXY_URL} \ --build-arg https_proxy=${HTTP_PROXY_URL} \ --build-arg no_proxy=${NO_PROXY_HOSTS} \ -f src/LiveRecorder.WebApi/Dockerfile \ -t "\$PLAT_REF" \ -t "\$PLAT_LATEST" \ --load \ . echo "=== Pushing \$PLAT_REF ===" # Write Harbor auth directly to config.json (bypass docker # login entirely) — then push with fresh baked-in credentials. AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0) printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null sudo docker push "\$PLAT_REF" 2>&1 | tail -5 sudo docker push "\$PLAT_LATEST" 2>&1 | tail -5 # Free disk space before the next platform build sudo docker rmi "\$PLAT_REF" "\$PLAT_LATEST" 2>/dev/null || true done echo '' echo "=== Creating multi-arch manifest: ${API_IMAGE_TAGGED} ===" AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0) && printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null sudo docker manifest create "${API_IMAGE_TAGGED}" \ "${API_IMAGE_TAGGED}-amd64" \ "${API_IMAGE_TAGGED}-arm64" sudo docker manifest create --amend "${API_IMAGE_LATEST}" \ "${API_IMAGE_LATEST}-amd64" \ "${API_IMAGE_LATEST}-arm64" echo "=== Pushing manifests ===" sudo docker manifest push "${API_IMAGE_TAGGED}" 2>&1 | tail -3 sudo docker manifest push "${API_IMAGE_LATEST}" 2>&1 | tail -3 # (Optionally clean up per-platform tags from registry, but harmless to leave them) echo '' echo "API multi-arch images pushed successfully:" echo " ${API_IMAGE_TAGGED}" echo " ${API_IMAGE_LATEST}" """ } } } stage('Build And Push Web Image') { steps { withCredentials([ usernamePassword( credentialsId: "${DOCKER_CREDS}", usernameVariable: 'DOCKER_USERNAME', passwordVariable: 'DOCKER_PASSWORD' ) ]) { sh """ set -e run_with_heartbeat() { log_file="\$1" build_label="\$2" shift 2 rm -f "\$log_file" : > "\$log_file" "\$@" >"\$log_file" 2>&1 & cmd_pid=\$! cmd_status=0 last_size=0 while kill -0 "\$cmd_pid" >/dev/null 2>&1; do echo "[heartbeat] \${build_label} still running at \$(date -u +%Y-%m-%dT%H:%M:%SZ)" if [ -f "\$log_file" ]; then current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0) if [ "\$current_size" -gt "\$last_size" ]; then start_byte=\$((last_size + 1)) tail -c +"\$start_byte" "\$log_file" || true last_size=\$current_size fi fi sleep 20 done wait "\$cmd_pid" || cmd_status=\$? if [ -f "\$log_file" ]; then current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0) if [ "\$current_size" -gt "\$last_size" ]; then start_byte=\$((last_size + 1)) tail -c +"\$start_byte" "\$log_file" || true fi fi if [ "\$cmd_status" -ne 0 ]; then echo "[heartbeat] \${build_label} failed with exit code \$cmd_status" fi return "\$cmd_status" } for PLATFORM in linux/amd64 linux/arm64; do ARCH_TAG="\${PLATFORM##*/}" PLAT_REF="${WEB_IMAGE_TAGGED}-\${ARCH_TAG}" PLAT_LATEST="${WEB_IMAGE_LATEST}-\${ARCH_TAG}" echo '' echo "=== Building Web for \$PLATFORM -> \$PLAT_REF ===" run_with_heartbeat /tmp/live-recorder-web-\${ARCH_TAG}.log "Web \${ARCH_TAG} build" \ sudo docker buildx build \ --platform "\$PLATFORM" \ --network host \ --progress=plain \ --provenance=false \ --no-cache \ --build-arg NODE_IMAGE=${WEB_NODE_IMAGE} \ --build-arg NGINX_IMAGE=${WEB_NGINX_IMAGE} \ --build-arg HTTP_PROXY=${HTTP_PROXY_URL} \ --build-arg HTTPS_PROXY=${HTTP_PROXY_URL} \ --build-arg NO_PROXY=${NO_PROXY_HOSTS} \ --build-arg http_proxy=${HTTP_PROXY_URL} \ --build-arg https_proxy=${HTTP_PROXY_URL} \ --build-arg no_proxy=${NO_PROXY_HOSTS} \ -f frontend/Dockerfile \ --build-arg VITE_API_BASE_URL=/api \ -t "\$PLAT_REF" \ -t "\$PLAT_LATEST" \ --load \ frontend echo "=== Pushing \$PLAT_REF ===" AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0) && printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null sudo docker push "\$PLAT_REF" 2>&1 | tail -3 sudo docker push "\$PLAT_LATEST" 2>&1 | tail -3 sudo docker rmi "\$PLAT_REF" "\$PLAT_LATEST" 2>/dev/null || true done echo '' echo "=== Creating multi-arch manifest: ${WEB_IMAGE_TAGGED} ===" AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0) && printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null sudo docker manifest create "${WEB_IMAGE_TAGGED}" \ "${WEB_IMAGE_TAGGED}-amd64" \ "${WEB_IMAGE_TAGGED}-arm64" sudo docker manifest create --amend "${WEB_IMAGE_LATEST}" \ "${WEB_IMAGE_LATEST}-amd64" \ "${WEB_IMAGE_LATEST}-arm64" echo "=== Pushing manifests ===" sudo docker manifest push "${WEB_IMAGE_TAGGED}" 2>&1 | tail -3 sudo docker manifest push "${WEB_IMAGE_LATEST}" 2>&1 | tail -3 echo '' echo "Web multi-arch images pushed successfully:" echo " ${WEB_IMAGE_TAGGED}" echo " ${WEB_IMAGE_LATEST}" """ } } } } post { success { echo "Pipeline completed successfully." } failure { echo "Pipeline failed. Please check the build log." } always { sh """ set +e sudo docker logout ${REGISTRY_URL} >/dev/null 2>&1 || true true """ deleteDir() } } }