Files
live_recorder/Jenkinsfile
T
nanxunandClaude Opus 4.8 4c6e730a08 fix: correct Harbor project path liverecorder -> live_recorder
The Harbor project is named 'live_recorder' (with underscore), matching
the robot account robot$live_recorder+live. The pipeline was pushing to
'liverecorder' (no underscore) — a different/nonexistent project path —
so the robot's push permission did not apply and every push got 401
despite 'Login Succeeded'.

This was THE root cause of the persistent 401s, not buildx auth
forwarding or token expiry (those were all red herrings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:24:36 +08:00

321 lines
15 KiB
Groovy

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
"""
}
}
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 \
--cache-from type=registry,ref=${API_CACHE_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 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
echo "DEBUG config:" && sudo cat /root/.docker/config.json
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 && echo "[config written]"
sudo docker manifest create "${API_IMAGE_TAGGED}" \
"${API_IMAGE_TAGGED}-amd64" \
"${API_IMAGE_TAGGED}-arm64"
sudo docker manifest create "${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 \
--cache-from type=registry,ref=${WEB_CACHE_IMAGE} \
--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 && echo "[config written]"
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 && echo "[config written]"
sudo docker manifest create "${WEB_IMAGE_TAGGED}" \
"${WEB_IMAGE_TAGGED}-amd64" \
"${WEB_IMAGE_TAGGED}-arm64"
sudo docker manifest create "${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()
}
}
}