Files
live_recorder/Jenkinsfile
T

379 lines
17 KiB
Groovy

pipeline {
agent { label '构建机1' }
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" --load .
./scripts/ci-docker.sh push "$api_ref"
./scripts/ci-docker.sh rmi "$api_ref" >/dev/null
./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" --load frontend
./scripts/ci-docker.sh push "$web_ref"
./scripts/ci-docker.sh rmi "$web_ref" >/dev/null
'''
}
}
}
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" --load .
./scripts/ci-docker.sh push "$api_ref"
./scripts/ci-docker.sh rmi "$api_ref" >/dev/null
./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" --load frontend
./scripts/ci-docker.sh push "$web_ref"
./scripts/ci-docker.sh rmi "$web_ref" >/dev/null
'''
}
}
}
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)
}
}
}