From 506dca898eea75b2cc04c03d4036a2487db4242f Mon Sep 17 00:00:00 2001 From: nanxun Date: Fri, 14 Aug 2026 13:31:15 +0800 Subject: [PATCH] ci: make release artifacts selectable --- Jenkinsfile | 289 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 211 insertions(+), 78 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index f46640d..c90d730 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,6 +14,34 @@ pipeline { buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '20')) } + parameters { + booleanParam( + name: 'RUN_TESTS', + defaultValue: true, + description: '运行前端构建检查和 .NET 全量测试' + ) + booleanParam( + name: 'BUILD_FNOS', + defaultValue: true, + description: '构建 fnOS x86_64 FPK 安装包' + ) + booleanParam( + name: 'UPLOAD_FNOS', + defaultValue: true, + description: '将本次构建的 fnOS 包上传到 OpenList(仅 BUILD_FNOS 生效)' + ) + choice( + name: 'API_IMAGE_PLATFORMS', + choices: ['amd64+arm64', 'amd64', 'arm64', 'none'], + description: '选择需要构建并推送的 API Docker 镜像架构' + ) + choice( + name: 'WEB_IMAGE_PLATFORMS', + choices: ['amd64+arm64', 'amd64', 'arm64', 'none'], + description: '选择需要构建并推送的 Web Docker 镜像架构' + ) + } + environment { PATH = '/home/nanxunai/.local/bin:/usr/local/bin:/usr/bin:/bin' REGISTRY_URL = 'reg.nxsir.cn' @@ -79,6 +107,13 @@ pipeline { stage('Metadata And Preflight') { steps { script { + if (!params.RUN_TESTS && + !params.BUILD_FNOS && + params.API_IMAGE_PLATFORMS == 'none' && + params.WEB_IMAGE_PLATFORMS == 'none') { + error('Select at least one test or build output.') + } + env.APP_VERSION = sh( script: "sed -n 's/^version=//p' fnos/manifest | head -n 1", returnStdout: true @@ -160,6 +195,9 @@ pipeline { } stage('Restore And Test') { + when { + expression { params.RUN_TESTS } + } steps { sh ''' set -euo pipefail @@ -171,7 +209,22 @@ pipeline { } } + stage('Prepare fnOS Dependencies') { + when { + expression { params.BUILD_FNOS && !params.RUN_TESTS } + } + steps { + sh ''' + set -euo pipefail + npm ci --prefix frontend + ''' + } + } + stage('Build fnOS x64') { + when { + expression { params.BUILD_FNOS } + } steps { sh ''' set -euo pipefail @@ -184,6 +237,12 @@ pipeline { } stage('Prepare Docker Builder') { + when { + expression { + params.API_IMAGE_PLATFORMS != 'none' || + params.WEB_IMAGE_PLATFORMS != 'none' + } + } steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", @@ -192,10 +251,15 @@ pipeline { )]) { 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 + if [ "$API_IMAGE_PLATFORMS" = arm64 ] || + [ "$API_IMAGE_PLATFORMS" = amd64+arm64 ] || + [ "$WEB_IMAGE_PLATFORMS" = arm64 ] || + [ "$WEB_IMAGE_PLATFORMS" = amd64+arm64 ]; then + 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 fi - ./scripts/ci-docker.sh run --rm --platform linux/arm64 "$ARM_TEST_IMAGE" uname -m | grep -q aarch64 if ./scripts/ci-docker.sh buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then ./scripts/ci-docker.sh buildx use "$BUILDER_NAME" @@ -218,6 +282,12 @@ pipeline { } stage('Prepare Harbor Authentication') { + when { + expression { + params.API_IMAGE_PLATFORMS != 'none' || + params.WEB_IMAGE_PLATFORMS != 'none' + } + } steps { withCredentials([usernamePassword( credentialsId: "${HARBOR_CREDENTIALS}", @@ -238,6 +308,12 @@ pipeline { } stage('Build And Push amd64') { + when { + expression { + params.API_IMAGE_PLATFORMS.contains('amd64') || + params.WEB_IMAGE_PLATFORMS.contains('amd64') + } + } steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", @@ -251,37 +327,47 @@ pipeline { 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 \ - --build-arg DOTNET_SDK_IMAGE="$DOTNET_SDK_IMAGE" \ - --build-arg DOTNET_RUNTIME_IMAGE="$DOTNET_RUNTIME_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 "$api_ref" --push . - ./scripts/ci-docker.sh buildx build \ - --builder "$BUILDER_NAME" --platform "$platform" --network host \ - --progress=plain --provenance=false \ - --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 + if [ "$API_IMAGE_PLATFORMS" = amd64 ] || [ "$API_IMAGE_PLATFORMS" = amd64+arm64 ]; then + ./scripts/ci-docker.sh buildx build \ + --builder "$BUILDER_NAME" --platform "$platform" --network host \ + --progress=plain --provenance=false \ + --build-arg DOTNET_SDK_IMAGE="$DOTNET_SDK_IMAGE" \ + --build-arg DOTNET_RUNTIME_IMAGE="$DOTNET_RUNTIME_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 "$api_ref" --push . + fi + if [ "$WEB_IMAGE_PLATFORMS" = amd64 ] || [ "$WEB_IMAGE_PLATFORMS" = amd64+arm64 ]; then + ./scripts/ci-docker.sh buildx build \ + --builder "$BUILDER_NAME" --platform "$platform" --network host \ + --progress=plain --provenance=false \ + --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 + fi ''' } } } stage('Build And Push arm64') { + when { + expression { + params.API_IMAGE_PLATFORMS.contains('arm64') || + params.WEB_IMAGE_PLATFORMS.contains('arm64') + } + } steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", @@ -295,37 +381,47 @@ pipeline { 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 \ - --build-arg DOTNET_SDK_IMAGE="$DOTNET_SDK_IMAGE" \ - --build-arg DOTNET_RUNTIME_IMAGE="$DOTNET_RUNTIME_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 "$api_ref" --push . - ./scripts/ci-docker.sh buildx build \ - --builder "$BUILDER_NAME" --platform "$platform" --network host \ - --progress=plain --provenance=false \ - --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 + if [ "$API_IMAGE_PLATFORMS" = arm64 ] || [ "$API_IMAGE_PLATFORMS" = amd64+arm64 ]; then + ./scripts/ci-docker.sh buildx build \ + --builder "$BUILDER_NAME" --platform "$platform" --network host \ + --progress=plain --provenance=false \ + --build-arg DOTNET_SDK_IMAGE="$DOTNET_SDK_IMAGE" \ + --build-arg DOTNET_RUNTIME_IMAGE="$DOTNET_RUNTIME_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 "$api_ref" --push . + fi + if [ "$WEB_IMAGE_PLATFORMS" = arm64 ] || [ "$WEB_IMAGE_PLATFORMS" = amd64+arm64 ]; then + ./scripts/ci-docker.sh buildx build \ + --builder "$BUILDER_NAME" --platform "$platform" --network host \ + --progress=plain --provenance=false \ + --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 + fi ''' } } } stage('Publish Multi-arch Manifests') { + when { + expression { + params.API_IMAGE_PLATFORMS == 'amd64+arm64' || + params.WEB_IMAGE_PLATFORMS == 'amd64+arm64' + } + } steps { withCredentials([usernamePassword( credentialsId: "${NODE_CREDENTIALS}", @@ -334,7 +430,8 @@ pipeline { )]) { sh ''' set -euo pipefail - for repository in "$API_IMAGE_REPO" "$WEB_IMAGE_REPO"; do + publish_manifest() { + repository=$1 immutable_ref="$repository:$IMMUTABLE_TAG" ./scripts/ci-docker.sh buildx imagetools create \ --tag "$immutable_ref" \ @@ -343,20 +440,26 @@ pipeline { 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 "$repository:$APP_VERSION" --tag "$repository:latest" \ + "$immutable_ref" + } - ./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" + if [ "$API_IMAGE_PLATFORMS" = amd64+arm64 ]; then + publish_manifest "$API_IMAGE_REPO" + fi + if [ "$WEB_IMAGE_PLATFORMS" = amd64+arm64 ]; then + publish_manifest "$WEB_IMAGE_REPO" + fi ''' } } } stage('Upload fnOS Artifact') { + when { + expression { params.BUILD_FNOS && params.UPLOAD_FNOS } + } steps { withCredentials([usernamePassword( credentialsId: "${OPENLIST_CREDENTIALS}", @@ -374,29 +477,59 @@ pipeline { stage('Build Summary') { steps { - sh ''' - set -euo pipefail - printf '%s\n' \ - "Commit: $(git rev-parse HEAD)" \ - "Product version: $APP_VERSION" \ - "fnOS package version: $FNOS_VERSION" \ - "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" - ''' + script { + def summary = [ + 'Commit: ' + sh(script: 'git rev-parse HEAD', returnStdout: true).trim(), + 'Product version: ' + env.APP_VERSION, + 'Tests: ' + (params.RUN_TESTS ? 'executed' : 'skipped') + ] + if (params.BUILD_FNOS) { + def checksum = sh( + script: "cut -d ' ' -f 1 '" + env.FPK_PATH + ".sha256'", + returnStdout: true + ).trim() + summary.addAll([ + 'fnOS package version: ' + env.FNOS_VERSION, + 'fnOS: ' + env.FPK_BASENAME, + 'Checksum: ' + checksum + ]) + if (params.UPLOAD_FNOS) { + summary << 'OpenList: ' + env.OPENLIST_BASE_URL + env.OPENLIST_REMOTE_DIR + '/' + env.FPK_BASENAME + } + } + if (params.API_IMAGE_PLATFORMS != 'none') { + summary << 'API platforms: ' + params.API_IMAGE_PLATFORMS + if (params.API_IMAGE_PLATFORMS.contains('amd64')) { + summary << 'API amd64: ' + env.API_IMAGE_REPO + ':' + env.IMMUTABLE_TAG + '-amd64' + } + if (params.API_IMAGE_PLATFORMS.contains('arm64')) { + summary << 'API arm64: ' + env.API_IMAGE_REPO + ':' + env.IMMUTABLE_TAG + '-arm64' + } + if (params.API_IMAGE_PLATFORMS == 'amd64+arm64') { + summary << 'API multi-arch: ' + env.API_IMAGE_REPO + ':' + env.IMMUTABLE_TAG + } + } + if (params.WEB_IMAGE_PLATFORMS != 'none') { + summary << 'Web platforms: ' + params.WEB_IMAGE_PLATFORMS + if (params.WEB_IMAGE_PLATFORMS.contains('amd64')) { + summary << 'Web amd64: ' + env.WEB_IMAGE_REPO + ':' + env.IMMUTABLE_TAG + '-amd64' + } + if (params.WEB_IMAGE_PLATFORMS.contains('arm64')) { + summary << 'Web arm64: ' + env.WEB_IMAGE_REPO + ':' + env.IMMUTABLE_TAG + '-arm64' + } + if (params.WEB_IMAGE_PLATFORMS == 'amd64+arm64') { + summary << 'Web multi-arch: ' + env.WEB_IMAGE_REPO + ':' + env.IMMUTABLE_TAG + } + } + echo summary.join('\n') + } } } } post { success { - echo "LiveRecorder ${env.IMMUTABLE_TAG} published successfully." + echo "LiveRecorder ${env.IMMUTABLE_TAG} selected pipeline completed successfully." } failure { echo 'LiveRecorder pipeline failed; stable Docker tags were only updated if all architecture builds succeeded.'