pipeline { agent { node { label '构建机1' customWorkspace '/home/nanxunai/goujian/workspace/liverecorder-release' } } options { timestamps() disableConcurrentBuilds() skipDefaultCheckout(true) timeout(time: 180, unit: 'MINUTES') buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '20')) } environment { PATH = '/home/nanxunai/.local/bin:/usr/local/bin:/usr/bin:/bin' REGISTRY_URL = 'reg.nxsir.cn' API_IMAGE_REPO = 'reg.nxsir.cn/live_recorder/app-api' WEB_IMAGE_REPO = 'reg.nxsir.cn/live_recorder/app-web' HARBOR_CREDENTIALS = 'harbor_key' OPENLIST_CREDENTIALS = 'openlist_key' NODE_CREDENTIALS = 'bbb939ea-4f01-4b47-aecb-c5ee2a551ef4' OPENLIST_BASE_URL = 'https://openlist.nxsir.cn' OPENLIST_REMOTE_DIR = '/yidongpan/构建产物/liverecorder' FNPACK_BIN = '/home/nanxunai/.local/bin/fnpack' FNPACK_SHA256 = '54b97fa7b70968c4d05c79840f5daeff508957d0bb2062fdb0376d00d9615c93' DOTNET = '/home/nanxunai/.local/bin/dotnet' BUILDER_NAME = "liverecorder-${BUILD_NUMBER}" BUILDKIT_IMAGE = 'docker.m.daocloud.io/moby/buildkit:buildx-stable-1' BINFMT_IMAGE = 'docker.m.daocloud.io/tonistiigi/binfmt:latest' ARM_TEST_IMAGE = 'docker.m.daocloud.io/library/alpine:latest' NODE_BUILD_IMAGE = 'docker.m.daocloud.io/library/node:22-alpine' NGINX_IMAGE = 'docker.m.daocloud.io/library/nginx:1.27-alpine' HTTP_PROXY_URL = 'http://192.168.5.200:7890' NO_PROXY_HOSTS = '127.0.0.1,localhost,reg.nxsir.cn,gitea.nxsir.cn,openlist.nxsir.cn,mirrors.tuna.tsinghua.edu.cn,.tsinghua.edu.cn,mcr.microsoft.com,docker.m.daocloud.io' CI_ROOT = "${WORKSPACE}/.ci" DOCKER_CONFIG = "${WORKSPACE}/.ci/docker" NUGET_PACKAGES = "${WORKSPACE}/.ci/nuget-packages" NUGET_HTTP_CACHE_PATH = "${WORKSPACE}/.ci/nuget-http" NUGET_PLUGINS_CACHE_PATH = "${WORKSPACE}/.ci/nuget-plugins" DOTNET_CLI_HOME = "${WORKSPACE}/.ci/dotnet-home" NPM_CONFIG_CACHE = "${WORKSPACE}/.ci/npm-cache" LIVERECORDER_NUGET_FEED = "${WORKSPACE}/.ci/nuget-feed" LIVERECORDER_BUILD_TMPDIR = "${WORKSPACE}/.ci/fnos-tmp" LIVERECORDER_VERIFY_TMPDIR = "${WORKSPACE}/.ci/fnos-verify" LIVERECORDER_SKIP_NPM_CI = '1' } stages { stage('Checkout') { steps { deleteDir() checkout scm } } stage('Metadata And Preflight') { steps { script { env.APP_VERSION = sh( script: "sed -n 's/^version=//p' fnos/manifest | head -n 1", returnStdout: true ).trim() env.SHORT_SHA = sh(script: 'git rev-parse --short=8 HEAD', returnStdout: true).trim() if (!env.APP_VERSION || !env.SHORT_SHA) { error('Unable to resolve fnOS version or Git commit.') } env.IMMUTABLE_TAG = "${env.APP_VERSION}-b${env.BUILD_NUMBER}-${env.SHORT_SHA}" env.FPK_BASENAME = "liverecorder-${env.IMMUTABLE_TAG}-x86_64.fpk" env.FPK_PATH = "${env.WORKSPACE}/artifacts/ci/${env.FPK_BASENAME}" currentBuild.displayName = "#${env.BUILD_NUMBER} ${env.APP_VERSION} ${env.SHORT_SHA}" currentBuild.description = "${env.IMMUTABLE_TAG}" } withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD' )]) { sh ''' set -euo pipefail test "$(uname -m)" = x86_64 for command_name in git sudo docker curl python3 npm node sha256sum stat tar; do command -v "$command_name" >/dev/null done test -x "$DOTNET" test -x "$FNPACK_BIN" printf '%s %s\n' "$FNPACK_SHA256" "$FNPACK_BIN" | sha256sum --check --status ./scripts/ci-docker.sh version >/dev/null ./scripts/ci-docker.sh buildx version available_kb=$(df -Pk "$WORKSPACE" | awk 'NR == 2 { print $4 }') if [ "$available_kb" -lt 2097152 ]; then echo "At least 2 GiB free workspace disk is required; available KiB: $available_kb" >&2 exit 1 fi mkdir -p \ "$DOCKER_CONFIG" \ "$NUGET_PACKAGES" \ "$NUGET_HTTP_CACHE_PATH" \ "$NUGET_PLUGINS_CACHE_PATH" \ "$DOTNET_CLI_HOME" \ "$NPM_CONFIG_CACHE" \ "$LIVERECORDER_NUGET_FEED" \ "$LIVERECORDER_BUILD_TMPDIR" \ "$LIVERECORDER_VERIFY_TMPDIR" \ "$(dirname -- "$FPK_PATH")" chmod 700 "$DOCKER_CONFIG" ''' } } } stage('Restore And Test') { steps { sh ''' set -euo pipefail npm ci --prefix frontend npm run build --prefix frontend "$DOTNET" restore LiveRecorder.sln --disable-parallel "$DOTNET" test LiveRecorder.sln -c Release --no-restore --logger 'console;verbosity=normal' /maxcpucount:1 ''' } } stage('Build fnOS x64') { steps { sh ''' set -euo pipefail FNPACK="$FNPACK_BIN" ./scripts/build-fnos-package.sh "$FPK_PATH" ./scripts/verify-fnos-package.sh "$FPK_PATH" test -s "$FPK_PATH.sha256" ''' } } stage('Prepare Docker Builder') { steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD' )]) { sh ''' set -euo pipefail if ! ./scripts/ci-docker.sh run --rm --platform linux/arm64 "$ARM_TEST_IMAGE" uname -m 2>/dev/null | grep -q aarch64; then ./scripts/ci-docker.sh run --privileged --rm "$BINFMT_IMAGE" --install arm64 fi ./scripts/ci-docker.sh run --rm --platform linux/arm64 "$ARM_TEST_IMAGE" uname -m | grep -q aarch64 ./scripts/ci-docker.sh buildx rm -f "$BUILDER_NAME" >/dev/null 2>&1 || true ./scripts/ci-docker.sh buildx create \ --name "$BUILDER_NAME" \ --driver docker-container \ --driver-opt "image=$BUILDKIT_IMAGE" \ --driver-opt network=host \ --use ./scripts/ci-docker.sh buildx inspect "$BUILDER_NAME" --bootstrap ''' } } } stage('Prepare Harbor Authentication') { steps { withCredentials([usernamePassword( credentialsId: "${HARBOR_CREDENTIALS}", usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD' )]) { sh ''' set -euo pipefail auth=$(printf '%s:%s' "$HARBOR_USERNAME" "$HARBOR_PASSWORD" | base64 -w0) printf '{"auths":{"%s":{"auth":"%s"}}}\n' "$REGISTRY_URL" "$auth" >"$DOCKER_CONFIG/config.json" chmod 600 "$DOCKER_CONFIG/config.json" ''' } } } stage('Build And Push amd64') { steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD' )]) { sh ''' set -euo pipefail arch=amd64 platform=linux/amd64 api_ref="$API_IMAGE_REPO:$IMMUTABLE_TAG-$arch" web_ref="$WEB_IMAGE_REPO:$IMMUTABLE_TAG-$arch" ./scripts/ci-docker.sh buildx build \ --builder "$BUILDER_NAME" --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 "$api_ref" --push . ./scripts/ci-docker.sh buildx prune \ --builder "$BUILDER_NAME" --all --force ./scripts/ci-docker.sh buildx build \ --builder "$BUILDER_NAME" --platform "$platform" --network host \ --progress=plain --provenance=false --no-cache \ --build-arg NODE_IMAGE="$NODE_BUILD_IMAGE" \ --build-arg NGINX_IMAGE="$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" \ --build-arg VITE_API_BASE_URL=/api \ -f frontend/Dockerfile -t "$web_ref" --push frontend ./scripts/ci-docker.sh buildx prune \ --builder "$BUILDER_NAME" --all --force ''' } } } stage('Build And Push arm64') { steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD' )]) { sh ''' set -euo pipefail arch=arm64 platform=linux/arm64 api_ref="$API_IMAGE_REPO:$IMMUTABLE_TAG-$arch" web_ref="$WEB_IMAGE_REPO:$IMMUTABLE_TAG-$arch" ./scripts/ci-docker.sh buildx build \ --builder "$BUILDER_NAME" --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 "$api_ref" --push . ./scripts/ci-docker.sh buildx prune \ --builder "$BUILDER_NAME" --all --force ./scripts/ci-docker.sh buildx build \ --builder "$BUILDER_NAME" --platform "$platform" --network host \ --progress=plain --provenance=false --no-cache \ --build-arg NODE_IMAGE="$NODE_BUILD_IMAGE" \ --build-arg NGINX_IMAGE="$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" \ --build-arg VITE_API_BASE_URL=/api \ -f frontend/Dockerfile -t "$web_ref" --push frontend ./scripts/ci-docker.sh buildx prune \ --builder "$BUILDER_NAME" --all --force ''' } } } stage('Publish Multi-arch Manifests') { steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD' )]) { sh ''' set -euo pipefail for repository in "$API_IMAGE_REPO" "$WEB_IMAGE_REPO"; do immutable_ref="$repository:$IMMUTABLE_TAG" ./scripts/ci-docker.sh buildx imagetools create \ --tag "$immutable_ref" \ "$immutable_ref-amd64" "$immutable_ref-arm64" manifest=$(./scripts/ci-docker.sh buildx imagetools inspect --raw "$immutable_ref") printf '%s' "$manifest" | grep -q '"architecture"[[:space:]]*:[[:space:]]*"amd64"' printf '%s' "$manifest" | grep -q '"architecture"[[:space:]]*:[[:space:]]*"arm64"' done ./scripts/ci-docker.sh buildx imagetools create \ --tag "$API_IMAGE_REPO:$APP_VERSION" --tag "$API_IMAGE_REPO:latest" \ "$API_IMAGE_REPO:$IMMUTABLE_TAG" ./scripts/ci-docker.sh buildx imagetools create \ --tag "$WEB_IMAGE_REPO:$APP_VERSION" --tag "$WEB_IMAGE_REPO:latest" \ "$WEB_IMAGE_REPO:$IMMUTABLE_TAG" ''' } } } stage('Upload fnOS Artifact') { steps { withCredentials([usernamePassword( credentialsId: "${OPENLIST_CREDENTIALS}", usernameVariable: 'OPENLIST_USERNAME', passwordVariable: 'OPENLIST_PASSWORD' )]) { sh ''' set -euo pipefail ./scripts/upload-openlist-artifact.sh "$FPK_PATH" "$OPENLIST_REMOTE_DIR" ./scripts/upload-openlist-artifact.sh "$FPK_PATH.sha256" "$OPENLIST_REMOTE_DIR" ''' } } } stage('Build Summary') { steps { sh ''' set -euo pipefail printf '%s\n' \ "Commit: $(git rev-parse HEAD)" \ "fnOS: $FPK_BASENAME" \ "Checksum: $(awk '{print $1}' "$FPK_PATH.sha256")" \ "API amd64: $API_IMAGE_REPO:$IMMUTABLE_TAG-amd64" \ "API arm64: $API_IMAGE_REPO:$IMMUTABLE_TAG-arm64" \ "API multi-arch: $API_IMAGE_REPO:$IMMUTABLE_TAG" \ "Web amd64: $WEB_IMAGE_REPO:$IMMUTABLE_TAG-amd64" \ "Web arm64: $WEB_IMAGE_REPO:$IMMUTABLE_TAG-arm64" \ "Web multi-arch: $WEB_IMAGE_REPO:$IMMUTABLE_TAG" \ "OpenList: $OPENLIST_BASE_URL$OPENLIST_REMOTE_DIR/$FPK_BASENAME" ''' } } } post { success { echo "LiveRecorder ${env.IMMUTABLE_TAG} published successfully." } failure { echo 'LiveRecorder pipeline failed; stable Docker tags were only updated if all architecture builds succeeded.' } always { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD' )]) { sh ''' set +e if [ -x ./scripts/ci-docker.sh ] && [ -n "${BUILDER_NAME:-}" ]; then ./scripts/ci-docker.sh buildx rm -f "$BUILDER_NAME" >/dev/null 2>&1 || true fi if [ -x ./scripts/ci-docker.sh ] && [ -n "${IMMUTABLE_TAG:-}" ]; then for repository in "$API_IMAGE_REPO" "$WEB_IMAGE_REPO"; do for suffix in amd64 arm64; do ./scripts/ci-docker.sh rmi "$repository:$IMMUTABLE_TAG-$suffix" >/dev/null 2>&1 || true done ./scripts/ci-docker.sh manifest rm "$repository:$IMMUTABLE_TAG" >/dev/null 2>&1 || true done fi if [ -f "$DOCKER_CONFIG/config.json" ]; then : >"$DOCKER_CONFIG/config.json" fi ''' } cleanWs(deleteDirs: true, notFailBuild: true) } } }